Skip to content

Config Module

nanocli.config.option

option(
    default: Any = MISSING, *, help: str = "", **kwargs: Any
) -> Any

Dataclass field wrapper with help text for CLI.

Use this instead of field() to add help text that appears in CLI help.

Parameters:

Name Type Description Default
default Any

Default value for the field.

MISSING
help str

Help text shown in CLI.

''
**kwargs Any

Additional arguments passed to dataclasses.field().

{}

Returns:

Type Description
Any

A dataclass field with metadata.

Examples:

>>> from dataclasses import dataclass
>>> @dataclass
... class Config:
...     epochs: int = option(100, help="Number of epochs")
...     lr: float = option(0.001, help="Learning rate")
>>> cfg = Config()
>>> cfg.epochs
100
Source code in src/nanocli/config.py
def option(
    default: Any = MISSING,
    *,
    help: str = "",
    **kwargs: Any,
) -> Any:
    """Dataclass field wrapper with help text for CLI.

    Use this instead of `field()` to add help text that appears in CLI help.

    Args:
        default: Default value for the field.
        help: Help text shown in CLI.
        **kwargs: Additional arguments passed to `dataclasses.field()`.

    Returns:
        A dataclass field with metadata.

    Examples:
        >>> from dataclasses import dataclass
        >>> @dataclass
        ... class Config:
        ...     epochs: int = option(100, help="Number of epochs")
        ...     lr: float = option(0.001, help="Learning rate")
        >>> cfg = Config()
        >>> cfg.epochs
        100
    """
    metadata = kwargs.pop("metadata", {})
    metadata["help"] = help
    return field(default=default, metadata=metadata, **kwargs)

nanocli.config.compile_config

compile_config(
    base: DictConfig | None = None,
    overrides: list[str] | None = None,
    schema: type[T] | None = None,
) -> DictConfig | T

Compile a config from base + overrides.

This is the core function: pure tree rewrite. Priority: schema defaults < base < overrides

Parameters:

Name Type Description Default
base DictConfig | None

Base config tree (from YAML).

None
overrides list[str] | None

List of override strings (key=value, key=@file).

None
schema type[T] | None

Optional dataclass for type validation.

None

Returns:

Type Description
DictConfig | T

Compiled config. Typed if schema provided, else DictConfig.

Examples:

>>> from dataclasses import dataclass
>>> @dataclass
... class Config:
...     name: str = "default"
...     count: int = 1
>>> cfg = compile_config(schema=Config)
>>> cfg.name
'default'
>>> cfg = compile_config(schema=Config, overrides=["name=custom"])
>>> cfg.name
'custom'
Source code in src/nanocli/config.py
def compile_config(
    base: DictConfig | None = None,
    overrides: list[str] | None = None,
    schema: type[T] | None = None,
) -> DictConfig | T:
    """Compile a config from base + overrides.

    This is the core function: pure tree rewrite.
    Priority: schema defaults < base < overrides

    Args:
        base: Base config tree (from YAML).
        overrides: List of override strings (`key=value`, `key=@file`).
        schema: Optional dataclass for type validation.

    Returns:
        Compiled config. Typed if schema provided, else DictConfig.

    Examples:
        >>> from dataclasses import dataclass
        >>> @dataclass
        ... class Config:
        ...     name: str = "default"
        ...     count: int = 1
        >>> cfg = compile_config(schema=Config)
        >>> cfg.name
        'default'
        >>> cfg = compile_config(schema=Config, overrides=["name=custom"])
        >>> cfg.name
        'custom'
    """
    # Build config: schema defaults -> base -> overrides
    if schema is not None:
        cfg = OmegaConf.structured(schema)
        if base is not None:
            cfg = OmegaConf.merge(cfg, base)
    else:
        cfg = base if base is not None else OmegaConf.create({})

    # Apply overrides (tree rewrite)
    if overrides:
        override_cfg = parse_overrides(overrides)
        try:
            cfg = OmegaConf.merge(cfg, override_cfg)
        except Exception as e:
            # Extract the key from OmegaConf error message
            error_msg = str(e)
            if "Key" in error_msg and "not in" in error_msg:
                # Parse: Key 'typer' not in 'ModelConfig'
                import re

                match = re.search(r"Key '(\w+)' not in '(\w+)'", error_msg)
                if match:
                    key, cls = match.groups()
                    raise ConfigError(
                        f"Invalid config key '{key}' in '{cls}'. Check for typos in your overrides."
                    ) from None
            # Re-raise with friendlier message
            raise ConfigError(f"Config error: {error_msg}") from None

    # Convert to typed object if schema provided
    if schema is not None:
        return OmegaConf.to_object(cfg)  # type: ignore[return-value]

    return cfg  # type: ignore[no-any-return]

nanocli.config.load_yaml

load_yaml(path: str | Path) -> DictConfig

Load a YAML file into a DictConfig.

Parameters:

Name Type Description Default
path str | Path

Path to the YAML file.

required

Returns:

Type Description
DictConfig

DictConfig containing the parsed YAML.

Raises:

Type Description
ConfigError

If the file does not exist.

Examples:

>>> import tempfile
>>> from pathlib import Path
>>> with tempfile.NamedTemporaryFile(suffix=".yml", delete=False, mode="w") as f:
...     _ = f.write("name: test\ncount: 42")
...     path = f.name
>>> cfg = load_yaml(path)
>>> cfg.name
'test'
>>> Path(path).unlink()
Source code in src/nanocli/config.py
def load_yaml(path: str | Path) -> DictConfig:
    """Load a YAML file into a DictConfig.

    Args:
        path: Path to the YAML file.

    Returns:
        DictConfig containing the parsed YAML.

    Raises:
        ConfigError: If the file does not exist.

    Examples:
        >>> import tempfile
        >>> from pathlib import Path
        >>> with tempfile.NamedTemporaryFile(suffix=".yml", delete=False, mode="w") as f:
        ...     _ = f.write("name: test\\ncount: 42")
        ...     path = f.name
        >>> cfg = load_yaml(path)
        >>> cfg.name
        'test'
        >>> Path(path).unlink()
    """
    path = Path(path)
    if not path.exists():
        raise ConfigError(f"Config file not found: {path}")
    return OmegaConf.load(path)  # type: ignore[return-value]

nanocli.config.to_yaml

to_yaml(config: Any) -> str

Convert config to YAML string.

Parameters:

Name Type Description Default
config Any

Config object (dataclass, dict, or DictConfig).

required

Returns:

Type Description
str

YAML string representation.

Examples:

>>> from dataclasses import dataclass
>>> @dataclass
... class Config:
...     name: str = "test"
>>> yaml_str = to_yaml(Config())
>>> "name: test" in yaml_str
True
Source code in src/nanocli/config.py
def to_yaml(config: Any) -> str:
    """Convert config to YAML string.

    Args:
        config: Config object (dataclass, dict, or DictConfig).

    Returns:
        YAML string representation.

    Examples:
        >>> from dataclasses import dataclass
        >>> @dataclass
        ... class Config:
        ...     name: str = "test"
        >>> yaml_str = to_yaml(Config())
        >>> "name: test" in yaml_str
        True
    """
    if is_dataclass(config) and not isinstance(config, type):
        cfg = OmegaConf.structured(config)
    elif isinstance(config, DictConfig):
        cfg = config
    else:
        cfg = OmegaConf.create(config)

    return OmegaConf.to_yaml(cfg)

nanocli.config.save_yaml

save_yaml(config: Any, path: str | Path) -> None

Save config to YAML file.

Parameters:

Name Type Description Default
config Any

Config object to save.

required
path str | Path

Path to write the YAML file.

required

Examples:

>>> import tempfile
>>> from dataclasses import dataclass
>>> @dataclass
... class Config:
...     name: str = "test"
>>> with tempfile.NamedTemporaryFile(suffix=".yml", delete=False) as f:
...     save_yaml(Config(), f.name)
...     content = open(f.name).read()
>>> "name: test" in content
True
Source code in src/nanocli/config.py
def save_yaml(config: Any, path: str | Path) -> None:
    """Save config to YAML file.

    Args:
        config: Config object to save.
        path: Path to write the YAML file.

    Examples:
        >>> import tempfile
        >>> from dataclasses import dataclass
        >>> @dataclass
        ... class Config:
        ...     name: str = "test"
        >>> with tempfile.NamedTemporaryFile(suffix=".yml", delete=False) as f:
        ...     save_yaml(Config(), f.name)
        ...     content = open(f.name).read()
        >>> "name: test" in content
        True
    """
    Path(path).write_text(to_yaml(config))

nanocli.config.parse_overrides

parse_overrides(overrides: list[str]) -> DictConfig

Parse CLI overrides into a config tree.

Supports three types of overrides: - key=value - Scalar override - key.path=value - Nested override - key=@file.yml - Subtree replacement from file

Parameters:

Name Type Description Default
overrides list[str]

List of override strings.

required

Returns:

Type Description
DictConfig

DictConfig with parsed overrides.

Raises:

Type Description
ConfigError

If an override doesn't contain '='.

Examples:

>>> cfg = parse_overrides(["name=test", "count=42"])
>>> cfg.name
'test'
>>> cfg.count
42
>>> cfg = parse_overrides(["model.layers=24"])
>>> cfg.model.layers
24
Source code in src/nanocli/config.py
def parse_overrides(overrides: list[str]) -> DictConfig:
    """Parse CLI overrides into a config tree.

    Supports three types of overrides:
    - `key=value` - Scalar override
    - `key.path=value` - Nested override
    - `key=@file.yml` - Subtree replacement from file

    Args:
        overrides: List of override strings.

    Returns:
        DictConfig with parsed overrides.

    Raises:
        ConfigError: If an override doesn't contain '='.

    Examples:
        >>> cfg = parse_overrides(["name=test", "count=42"])
        >>> cfg.name
        'test'
        >>> cfg.count
        42
        >>> cfg = parse_overrides(["model.layers=24"])
        >>> cfg.model.layers
        24
    """
    result: dict[str, Any] = {}

    for override in overrides:
        if "=" not in override:
            raise ConfigError(f"Invalid override: '{override}'. Expected 'key=value' format.")

        key, value = override.split("=", 1)
        key = key.strip()
        value = value.strip()

        # Handle @file syntax for subtree replacement
        if value.startswith("@"):
            file_path = value[1:]
            parsed = OmegaConf.to_container(load_yaml(file_path))
        else:
            parsed = _parse_value(value)

        # Build nested dict from dot notation
        _set_nested(result, key.split("."), parsed)

    return OmegaConf.create(result)

nanocli.config.ConfigError

Bases: Exception

Configuration-related errors.

Raised when config files are missing, overrides are invalid, etc.

Source code in src/nanocli/config.py
class ConfigError(Exception):
    """Configuration-related errors.

    Raised when config files are missing, overrides are invalid, etc.
    """