Skip to content

pydantic_click_options

pydantic_click_options

Generate Click CLI options from a Pydantic model.

Used by cli/run.py and cli/config.py to expose every SafeSynthesizerParameters field as a --field_name CLI option. Nested BaseModel fields are flattened with a separator (e.g. --data__holdout). Fields typed as SomeModel | None also get a --no-<field> is-flag that sets the field to None.

The companion parse_overrides() reverses the flattening at runtime, converting Click's flat {key: value} dict back into the nested structure Pydantic expects. The field_sep argument to parse_overrides must match the field_separator passed to pydantic_options; otherwise nested keys like data__holdout will not be reconstructed correctly.

Functions:

Name Description
parse_overrides

Parse Click kwargs into a nested override dict.

pydantic_options

Decorate a Click command with options derived from a Pydantic model.

LeafParam(name, field) dataclass

A scalar CLI option backed by a Pydantic FieldInfo.

FlagParam(name, field_name) dataclass

A --no-<field> is-flag that sets the named field to None.

parse_overrides(values=None, field_sep='__')

Parse Click kwargs into a nested override dict.

no_<field>=True injects {field: None} to disable a nullable-model field. no_<field>=False (unset is-flag) is silently dropped. None values (unset regular options) are also dropped.

Parameters:

Name Type Description Default
values dict[str, Any] | None

Flat dictionary of command line arguments from Click. (None-valued keys are dropped).

None
field_sep str

Separator used to reconstruct nesting. For example, {"data__holdout": 0.1} becomes {"data": {"holdout": 0.1}}.

'__'

Returns:

Type Description
dict[str, Any]

A nested dict suitable for model_validate() or for merging

dict[str, Any]

with a loaded config via merge_dicts().

Raises:

Type Description
ValueError

If a key contains empty segments (e.g. consecutive separators like a____b).

Source code in src/nemo_safe_synthesizer/configurator/pydantic_click_options.py
def parse_overrides(values: dict[str, Any] | None = None, field_sep: str = "__") -> dict[str, Any]:
    """Parse Click kwargs into a nested override dict.

    ``no_<field>=True`` injects ``{field: None}`` to disable a nullable-model
    field.  ``no_<field>=False`` (unset is-flag) is silently dropped.
    ``None`` values (unset regular options) are also dropped.

    Args:
        values: Flat dictionary of command line arguments from Click. (``None``-valued keys are dropped).
        field_sep: Separator used to reconstruct nesting.  For example, ``{"data__holdout": 0.1}`` becomes ``{"data": {"holdout": 0.1}}``.

    Returns:
        A nested dict suitable for ``model_validate()`` or for merging
        with a loaded config via ``merge_dicts()``.

    Raises:
        ValueError: If a key contains empty segments (e.g. consecutive
            separators like ``a____b``).
    """
    if not values:
        return {}
    overrides: dict[str, Any] = {}
    for k, v in values.items():
        if k.startswith("no_") and isinstance(v, bool):
            if v:
                overrides[k[3:]] = None
            continue
        if v is None:
            continue
        match k.split(field_sep):
            case [key]:
                overrides[key] = v
            case [first, *rest, last] if all(rest) and last:
                target = overrides
                if not isinstance(target.get(first), dict):
                    target[first] = {}
                target = target[first]
                for part in rest:
                    if not isinstance(target.get(part), dict):
                        target[part] = {}
                    target = target[part]
                target[last] = v
            case _:
                raise ValueError(f"Invalid override key: {k!r}")
    return overrides

pydantic_options(model_class, field_separator='__')

Decorate a Click command with options derived from a Pydantic model.

Recurses into nested sub-models, flattening their fields into top-level CLI options separated by field_separator. Fields typed as SomeModel | None also get a --no-<field> is-flag that sets the field to None when passed. Field types are mapped to Click types via _CLICK_TYPE_PRIORITY; help text is pulled from Field(description=...).

Parameters:

Name Type Description Default
model_class type[BaseModel]

The Pydantic model to generate options from (typically SafeSynthesizerParameters).

required
field_separator str

String used to join parent and child field names in the CLI option (default "__").

'__'

Returns:

Type Description

A Click decorator that attaches the generated options to a command.

Source code in src/nemo_safe_synthesizer/configurator/pydantic_click_options.py
def pydantic_options(model_class: type[BaseModel], field_separator: str = "__"):
    """Decorate a Click command with options derived from a Pydantic model.

    Recurses into nested sub-models, flattening their fields into top-level
    CLI options separated by ``field_separator``.  Fields typed as
    ``SomeModel | None`` also get a ``--no-<field>`` is-flag that sets the
    field to ``None`` when passed.  Field types are mapped to Click types
    via ``_CLICK_TYPE_PRIORITY``; help text is pulled from
    ``Field(description=...)``.

    Args:
        model_class: The Pydantic model to generate options from
            (typically ``SafeSynthesizerParameters``).
        field_separator: String used to join parent and child field names
            in the CLI option (default ``"__"``).

    Returns:
        A Click decorator that attaches the generated options to a command.
    """

    def decorator(f):
        for param in sorted(_collect_params(model_class), key=lambda p: p.name):
            match param:
                case FlagParam(field_name=field_name):
                    # Flags use standard CLI dashes (--no-replace-pii) while
                    # LeafParam options preserve underscores (--training__learning_rate)
                    # because the separator/field structure is autogenerated from Pydantic.
                    nested_name = field_name.replace(".", field_separator)
                    if field_separator != ".":
                        parts = nested_name.split(field_separator)
                        nested_name = field_separator.join(p.replace("_", "-") for p in parts)
                    else:
                        nested_name = nested_name.replace("_", "-")
                    flag_cli = f"--no-{nested_name}"
                    f = click.option(
                        flag_cli,
                        is_flag=True,
                        default=False,
                        help=f"Disable {field_name.replace('_', '-')} entirely.",
                    )(f)
                case LeafParam(field=field):
                    names = _option_names(param.name, field_separator)
                    f = click.option(
                        *names,
                        type=_click_type(field.annotation),
                        help=field.description or "",
                    )(f)
        return f

    return decorator