Config composition¶
Inheritance¶
Standard Python inheritance works naturally. Fields are collected from the full MRO automatically, so a child class has all parent fields plus any it adds or overrides:
from cfx import Config, Int, Float, Options
class ProcessingConfig(Config):
confid = "processing"
iterations = Int(100, "Number of iterations", minval=1)
threshold = Float(0.5, "Acceptance threshold", minval=0.0, maxval=1.0)
mode = Options(("fast", "balanced", "thorough"), "Processing mode")
class ChildConfig(ProcessingConfig):
confid = "child"
mode = Options(("fast", "balanced", "thorough"), "Processing mode",
default_value="balanced") # changed default
max_size = Int(1000, "Maximum item count", minval=1)
cfg = ChildConfig()
cfg.iterations # 100 - inherited
cfg.mode # 'balanced' - default changed by child
cfg.max_size # 1000 - new field added by child
Composing with components=¶
Pass a list of config classes as components= to assemble them into a
larger config:
from cfx import Config, Int, String
class FormatConfig(Config):
confid = "format"
precision = Int(6, "Decimal precision")
encoding = String("utf-8", "Output encoding")
class PipelineConfig(Config, components=[ProcessingConfig, FormatConfig]):
pass
cfg = PipelineConfig()
cfg.processing.iterations # 100
cfg.format.precision # 6
Each component becomes a sub-object accessible by its confid.
The confid attribute
Every config class has a confid string identifier. If you do not set it
explicitly, it defaults to the lowercase class name:
class QuickConfig(Config):
field1 = Int(1, "A field")
QuickConfig.confid # 'quickconfig'
Set confid explicitly whenever a class will be used as a component, so
the resulting attribute name is predictable:
class FormatConfig(Config):
confid = "format" # accessible as parent_cfg.format
precision = Int(6, "Decimal precision")
confid becomes the attribute name in nested composition and the key in
nested serialization.
Nested sub-configs¶
Each component becomes a fresh sub-object on every parent instance,
accessible as an attribute named after its confid:
cfg = PipelineConfig()
cfg.processing.iterations = 200
cfg.format.precision = 3
print(cfg.processing) # full table for ProcessingConfig
print(cfg.format) # full table for FormatConfig
Each parent instance gets its own independent sub-configs. Mutating
cfg1.processing has no effect on cfg2.processing.
to_dict() produces a nested dict keyed by confid; from_dict() routes
each sub-dict back to its component class:
cfg.to_dict()
# {'processing': {'iterations': 200, 'threshold': 0.5, ...},
# 'format': {'precision': 3, 'encoding': 'utf-8'}}
cfg2 = PipelineConfig.from_dict(cfg.to_dict())
Duplicate ``confid`` values across components are not allowed. If two
classes in the components= list share a confid, a ValueError is
raised at class-definition time.
Mixed own fields and components¶
A config that uses components= can also declare its own flat fields
alongside the sub-configs:
from cfx import String, Bool
class PipelineConfig(Config, components=[ProcessingConfig, FormatConfig]):
confid = "pipeline"
run_id = String("run_01", "Run identifier")
dry_run = Bool(False, "Validate only; skip writes")
cfg = PipelineConfig()
cfg.run_id # 'run_01'
cfg.processing.iterations # 100
cfg.format.precision # 6
Own fields appear before sub-config fields in the display table. to_dict
produces a flat key for each own field and a nested sub-dict for each
component:
cfg.to_dict()
# {'run_id': 'run_01', 'dry_run': False,
# 'processing': {...}, 'format': {...}}
For projecting a config tree into a different namespace, or exposing a curated subset of fields under new names, see Views.