ADR-0010: Unified Configuration via @configured and ContextConfig¶
Status: Accepted
Context¶
Before v2.0.0, Pico-IoC offered two parallel mechanisms for configuration binding: @configuration for flat sources and @configured for nested (tree) sources. Each required a different initialization argument (config vs tree_config), duplicating both user cognitive load and internal logic. Inconsistent ordering, normalization, and documentation made the developer experience error-prone.
Since adoption of v2.0.0 was still limited, we took the opportunity to introduce a breaking but cleaner unification, simplifying the configuration model for the long term.
Rationale for Value: During testing and real-world deployments, we observed recurring cases where a few configuration fields needed to be statically fixed regardless of the environment (e.g., embedded secrets in CI, non-configurable defaults, or deterministic test fixtures). The previous system required extra patching or post-processing. Introducing Annotated[..., Value(...)] provides an explicit, type-safe, and framework-agnostic mechanism to declare immutable constants inside @configured classes, ensuring reproducibility and clarity while still coexisting with dynamic configuration sources.
Decision¶
Configuration is unified around a single decorator (@configured) and a single runtime entry point (configuration(...) → ContextConfig → init(config=...)).
-
Remove
@configuration. Only@configuredremains. -
Enhance
@configured: -
Signature:
* Auto-detection rules:- If any field is a dataclass, list, dict, or
Union→ treat astree. - If all fields are primitives (
str,int,float,bool) → treat asflat. - Explicit
mappingalways overrides auto-detection.
- If any field is a dataclass, list, dict, or
-
Introduce
configuration(...)→ContextConfig: -
Accepts an ordered list of configuration sources (environment, YAML, JSON, dicts, CLI adapters, etc.).
- Supports
overridesand explicit values for final patching. -
Defines deterministic precedence:
Value(...)>overrides> sources (left to right). -
Normalize casing and key mapping:
-
Internal attributes use
snake_case. - Environment variables use
UPPER_CASE. - Tree mapping uses
__as a path separator (e.g.,APP_DB__HOST). -
Flat mapping uses
PREFIX_FIELD. -
Unify coercion and discriminator logic across flat and tree modes via
config_runtime. -
Add Inline Field Overrides via
Annotated[..., Value(...)]: -
Valuecan be applied as metadata to any field to force a constant value. - When present, it short-circuits lookup, bypassing all configuration sources.
-
Used for secrets templating in tests, static defaults, or environment-independent fallbacks.
-
Single entry point:
Consequences¶
Positive¶
-
Cleaner Mental Model: One decorator (
@configured) and one initialization path (configuration(...) → ContextConfig) for all configuration types. -
Deterministic Configuration: Explicit, documented precedence across heterogeneous sources, with
Value(...)providing the top-level override. -
Inline Overrides:
Annotated[..., Value(...)]offers a standard Pythonic way to hard-code constants or test values without relying on environment setup. -
First-Class ENV Integration: Environment sources remain intuitive, with predictable normalization rules (
APP_DB_URL→app.db.url). -
Unified Mapping Strategy: Flat vs. tree behavior becomes a runtime mode, not a separate decorator, reducing conceptual duplication.
-
Improved Testability: The immutable, preprocessed
ContextConfigmakes configuration deterministic and easy to mock or inspect.
Negative¶
-
Breaking Change: Removes
@configurationand the oldconfigargument frominit(). Users must migrate to the unified model. -
Migration Effort: Existing classes must adopt
@configured, update field names to match conventions, and use theconfiguration(...)builder. -
Learning Curve: Developers must understand the
mappingmodes,ContextConfig, and the newAnnotated[..., Value(...)]mechanism. -
Convention Reliance:
"auto"mode relies on consistent naming and type hints for accurate mapping inference.
Alternatives Considered¶
- Keep both decorators: Rejected — duplicates logic and confuses users.
- Force tree-mode only: Rejected — flat ENV/CLI setups would become cumbersome.
- Detect mode by field name casing: Rejected — type-shape inference is more robust.
Implementation Sketch¶
- Add
ContextConfigandconfiguration(...)inconfig_builder.py. - Extend
@configuredto recordprefixandmappinginPICO_META. - For
flatmode: query normalized keys (PREFIX_FIELD). - For
treemode: deep-merge sources into a composite tree and instantiate viaconfig_runtime. - Implement and document
Value(...)andDiscriminator(...)metadata inconfig_runtime.
Migration¶
- Remove
@configurationand thetree_configargument frominit(). - Replace existing usages with
@configured. - Update documentation and provide examples demonstrating
configuration(...)andValue(...). - Optional: provide a script to rewrite decorators automatically.
Examples¶
from dataclasses import dataclass
from typing import Annotated
from pico_ioc import configured, configuration, EnvSource, Value, init
@configured(prefix="APP_", mapping="auto")
@dataclass
class HttpCfg:
host: str
port: int
debug: bool = False
timeout: Annotated[int, Value(60)] # Inline override
ctx = configuration(EnvSource(prefix=""))
c = init(modules=[__name__], config=ctx)
from dataclasses import dataclass
from typing import Annotated, Union
from pico_ioc import configured, configuration, EnvSource, Discriminator, init
@dataclass
class Postgres:
kind: str
host: str
port: int
@dataclass
class Sqlite:
kind: str
path: str
@configured(prefix="DB_", mapping="tree")
@dataclass
class DbCfg:
model: Annotated[Union[Postgres, Sqlite], Discriminator("kind")]
ctx = configuration(EnvSource(prefix=""))
c = init(modules=[__name__], config=ctx)