inherit¶
Hierarchical Configuration Inheritance Pattern
This module provides a DRY (Don’t Repeat Yourself) solution for configuration management by implementing inheritance patterns similar to object-oriented programming, but for JSON-like data structures.
Problem It Solves
When managing configurations for multiple environments (dev, staging, prod), you often need to repeat common settings across environments. This leads to duplication and maintenance overhead.
Solution
Use a special _shared section to define default values that automatically
inherit to other sections, while allowing environment-specific overrides.
How It Works
The _shared section contains JSON path patterns that specify where default
values should be applied. Values are only set if they don’t already exist
(no overwriting).
Basic Example
Input configuration:
{
"_shared": {
"*.username": "root", # Apply to all environments
"*.memory": 2 # Default memory allocation
},
"dev": {
"password": "dev123" # Dev-specific setting
},
"prod": {
"password": "prod456", # Prod-specific setting
"memory": 8 # Override default memory
}
}
After applying inheritance, becomes:
{
"dev": {
"username": "root", # Inherited from _shared
"password": "dev123", # Original value
"memory": 2 # Inherited from _shared
},
"prod": {
"username": "root", # Inherited from _shared
"password": "prod456", # Original value
"memory": 8 # Override (not replaced)
}
}
JSON Path Patterns
*.field: Apply to all top-level keys (except _shared)env.field: Apply to specific environment*.db.*.port: Apply to nested structures with wildcardsenv.services.port: Apply to specific nested path
Key Features
Non-destructive: Existing values are never overwritten
Recursive: Supports nested _shared sections for fine-grained control
Flexible: Works with dictionaries and lists of dictionaries
Order-aware: Evaluation order matters for overlapping patterns
- configcraft.inherit.SHARED = '_shared'¶
Special key used to define shared inheritance rules in configuration data.
- configcraft.inherit.make_type_error(prefix: str, key: str) TypeError[source]¶
Create a descriptive TypeError when trying to set a value on incompatible data types.
This helper creates user-friendly error messages when the inheritance process encounters data that isn’t a dict or list of dicts, which are the only structures that support key assignment.
- Parameters:
prefix – The JSON path prefix where the error occurred
key – The key we were trying to set
- Raises:
TypeError – with descriptive message about the invalid operation
- configcraft.inherit.inherit_value(path: str, value: Any, data: Dict[str, Any] | List[Dict[str, Any]], _prefix: str | None = None) None[source]¶
Apply a default value to a JSON path pattern, preserving existing values.
This is the core inheritance mechanism that implements setdefault-like behavior for nested configuration structures. Like dict.setdefault(), it only sets values where keys don’t already exist, never overwriting existing configuration.
What it does
Follows JSON path patterns like
"*.username"or"dev.database.port"Sets values only where they’re missing (non-destructive)
Handles wildcards (*) to apply to multiple targets
Works with nested dicts and lists of dicts
Examples
Path
"*.memory"-> Setsmemory=2in all top-level environmentsPath
"dev.db.port"-> Setsport=5432only indev.dbPath
"*.servers.*.cpu"-> Setscpu=1in all servers across all environments
- Parameters:
path – JSON path pattern (e.g.,
"*.username","dev.db.port")value – The default value to set
data – Configuration dict/list to modify in-place
_prefix – Internal recursion parameter (do not use)
- Raises:
ValueError – If path ends with “*” (incomplete path)
TypeError – If trying to set values on incompatible data types
- Returns:
None
Important
The input param
datawill be modified in-place. If you want to keep the original data, do this before calling this function:import copy new_data = copy.deepcopy(data) inherit_value(path, value, new_data)
- configcraft.inherit.apply_inheritance(data: dict[str, Any]) None[source]¶
Transform configuration data by applying all
_sharedinheritance rules.This is the main entry point that processes an entire configuration structure, finding all _shared sections and applying their inheritance rules to create the final resolved configuration.
What it does:
Recursively processes nested _shared sections (deeper ones override shallower ones)
Applies each JSON path pattern in the _shared section in definition order
Removes all _shared sections from the final output
Modifies the input data in-place
Path Execution Order Within Same _shared:
Within a single _shared section, paths are processed from top to bottom. If multiple paths affect the same node, the earlier path takes effect due to setdefault behavior. This enables powerful exception-then-default patterns.
Example - setting defaults with specific exceptions:
{ "_shared": { "*.servers.blue.cpu": 4, # Exception: blue gets 4 CPU "*.servers.*.cpu": 2 # Default: all others get 2 CPU }, "env": { "servers": { "blue": {}, # Gets cpu=4 (from first rule) "green": {} # Gets cpu=2 (from second rule) } } }
The exception must be defined before the wildcard pattern to take effect.
Child _shared Overrides Parent _shared:
Each nested object can have its own _shared section. When both parent and child _shared sections would affect the same node, the child wins due to processing order (children processed before parents).
Example - nested inheritance hierarchy:
{ "_shared": { "*.servers.*.memory": 1024 # Parent default }, "env": { "servers": { "_shared": { "*.memory": 2048 # Child override }, "web": {} # Gets memory=2048 (child wins) } } }
This design allows fine-grained control where specific sections can override broader defaults while maintaining the inheritance hierarchy.
Basic Example:
>>> data = { ... "_shared": { ... "*.memory": 2 ... }, ... "dev": {}, ... "prod": { ... "memory": 8 ... } ... } >>> apply_inheritance(data) >>> data { "dev": {"memory": 2}, # Inherited default "prod": {"memory": 8} # Kept existing value }
- Parameters:
data – Configuration dictionary with _shared sections to process
Important
The input param
datawill be modified in-place, all _shared sections will be removed and their rules applied. If you want to keep the original data, do this before calling this function:import copy new_data = copy.deepcopy(data) apply_inheritance(new_data)