Views

A view projects a config tree into a different namespace — exposing a curated subset of fields under new names, auto-generating aliases for every field in a group of configs, or enforcing that two config fields always hold the same value.

Views are useful when:

  • the internal config structure (e.g. deeply nested sub-configs) is more complex than what a consumer needs to see;

  • the same underlying fields must appear under different names in different contexts;

  • two separate config fields must always stay in sync.

The examples on this page use these shared config classes:

from cfx import Config, Float, Int, String, Bool

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)
    verbose = Bool(False, "Enable verbose logging")

class FormatConfig(Config):
    confid = "format"
    precision = Int(6, "Decimal places in numeric output")
    encoding = String("utf-8", "Output file encoding")

class PipelineConfig(Config, components=[ProcessingConfig, FormatConfig]):
    confid = "pipeline"
    run_id = String("run_01", "Run identifier")
    dry_run = Bool(False, "Validate only; skip writes")

ConfigView and Alias

ConfigView is the base class for hand-written projections. Subclass it and declare Alias descriptors to expose selected fields under whatever names suit the consumer:

from cfx import ConfigView, Alias

class RunSummary(ConfigView):
    n_iter   = Alias(PipelineConfig.processing.iterations)
    decimals = Alias(PipelineConfig.processing.threshold)
    label    = Alias(PipelineConfig.run_id)

Bind a view to an existing config instance at construction. All reads and writes delegate through to the underlying config — the view carries no data of its own:

cfg = PipelineConfig()
cfg.processing.iterations = 250

v = RunSummary(cfg)
v.n_iter    # 250 - reads cfg.processing.iterations
v.decimals  # 0.5 - reads cfg.processing.threshold

v.n_iter = 500
cfg.processing.iterations  # 500 - write went through

The Alias argument is a FieldRef obtained by class-level attribute access on a Config. Plain dotpath strings are also accepted (Alias("processing.iterations")), but the object-reference form keeps the path refactorable via IDE rename and go-to-definition.

A view can span multiple sub-configs by binding to a common parent:

class RunSummary(ConfigView):
    n_iter    = Alias(PipelineConfig.processing.iterations)
    decimals  = Alias(PipelineConfig.processing.threshold)
    precision = Alias(PipelineConfig.format.precision)

cfg = PipelineConfig()
v = RunSummary(cfg)
v.n_iter     # cfg.processing.iterations
v.precision  # cfg.format.precision

Serialization

to_dict() returns the aliased fields and their current values:

v.to_dict()   # {'n_iter': 500, 'decimals': 0.5, 'precision': 6}

from_dict() applies values from a dict through the view’s aliases and returns a bound view:

v2 = RunSummary.from_dict({"n_iter": 100, "decimals": 0.3}, cfg)
cfg.processing.iterations  # 100
cfg.processing.threshold   # 0.3

Unknown keys in the dict are silently ignored.

Display

repr(view) shows the class name and current alias values. ViewClass._repr_html_() (called on the class, not an instance) renders an HTML table mapping alias names to their dotpaths — useful for inspecting a view’s schema in a Jupyter notebook.

AliasedView

AliasedView auto-generates Alias descriptors for every field in each declared component. Unlike ConfigView, an AliasedView owns its component instances — construction takes no arguments:

from cfx import AliasedView

class JobView(AliasedView, components=[ProcessingConfig, FormatConfig]):
    pass

v = JobView()
v.processing_iterations  # 100 - alias generated as "{confid}_{field}"
v.format_precision       # 6

v.processing_iterations = 300
v.processing.iterations  # 300 - write went through to the component

By default each alias is prefixed with the component’s confid, so fields from different components never conflict.

Custom prefixes

Pass aliases= to override the prefix for each component:

class JobView(AliasedView,
              components=[ProcessingConfig, FormatConfig],
              aliases=["proc", "fmt"]):
    pass

v = JobView()
v.proc_iterations   # 100
v.fmt_precision     # 6

Pass None as a prefix to expose that component’s fields without any prefix:

class JobView(AliasedView,
              components=[ProcessingConfig],
              aliases=[None]):
    pass

v = JobView()
v.iterations   # 100 - no prefix

A ValueError is raised at class-definition time if two auto-generated aliases collide — whether from the same prefix or from two None-prefix components with a shared field name:

class StorageConfig(Config):
    confid = "storage"
    encoding = String("utf-8", "Storage encoding")   # same name as FormatConfig.encoding

class BadView(AliasedView,
              components=[FormatConfig, StorageConfig],
              aliases=[None, None]):
    pass
# ValueError: Name conflict 'encoding' among auto-generated aliases ...

Serialization

to_dict() and from_dict() work the same as for ConfigView, except from_dict takes only the dict argument (no separate config) and constructs a fresh view:

v = JobView()
v.to_dict()   # {'processing_iterations': 100, 'processing_threshold': 0.5, ...}

v2 = JobView.from_dict({"processing_iterations": 200})
v2.processing.iterations  # 200

FlatView

FlatView is an AliasedView with all prefixes implicitly None — every component field is exposed directly by its own name:

from cfx import FlatView

class FlatWorker(FlatView, components=[ProcessingConfig]):
    pass

w = FlatWorker()
w.iterations = 200
w.verbose = True
w.processing.iterations  # 200

If two components in a FlatView share a field name, a ValueError is raised at class-definition time:

class ExtraConfig(Config):
    confid = "extra"
    iterations = Int(0, "Also has iterations")

class BadFlat(FlatView, components=[ProcessingConfig, ExtraConfig]):
    pass
# ValueError: Name conflict 'iterations' among auto-generated aliases ...

Use AliasedView with explicit prefixes to resolve the conflict.

Mirror

Mirror is a Config field descriptor (not a view) that keeps two or more dotpaths in sync. A write fans out to every path; a read asserts that all paths agree and returns the shared value:

from cfx import Mirror

class SyncedConfig(Config, components=[ProcessingConfig, FormatConfig]):
    confid = "synced"
    detail = Mirror("processing.iterations", "format.precision")

cfg = SyncedConfig()
cfg.detail = 8
cfg.processing.iterations  # 8
cfg.format.precision       # 8

cfg.detail   # 8 - paths agree

If the paths diverge (e.g. one is set independently), reading the mirror raises ValueError:

cfg.processing.iterations = 10
cfg.detail   # ValueError: Mirror 'detail' paths disagree: ...

Note

Unlike Alias (which walks from _alias_root), Mirror walks from the config instance it is declared on. Paths must be relative to that config, not to any component class. Use dotpath strings ("processing.iterations") rather than bare FieldRef objects (ProcessingConfig.iterations) — the latter would produce path "iterations" which does not reach the component field on the parent config.