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.

Classes:

Name Description
AutoParamType

A Click type that accepts the sentinel AUTO_STR or a base numeric/bool value.

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.

AutoParamType(base_type)

Bases: ParamType

A Click type that accepts the sentinel AUTO_STR or a base numeric/bool value.

Used for Auto*Param fields (AutoIntParam, AutoFloatParam, AutoBoolParam) so that --flag auto and --flag 2 both work. The --help display shows integer|auto, float|auto, or boolean|auto instead of the generic TEXT label.

Parameters:

Name Type Description Default
base_type ParamType

The underlying Click type (click.INT, click.FLOAT, or click.BOOL) used to parse and validate non-AUTO_STR values.

required

Methods:

Name Description
convert

Convert the raw CLI value to AUTO_STR or the base numeric/bool type.

Source code in src/nemo_safe_synthesizer/configurator/pydantic_click_options.py
def __init__(self, base_type: click.ParamType) -> None:
    self.base_type = base_type
    self.name = f"{base_type.name}|{AUTO_STR}"

convert(value, param, ctx)

Convert the raw CLI value to AUTO_STR or the base numeric/bool type.

The value parameter is typed str to match the parent click.ParamType.convert stub signature; in practice Click may also pass through default values of any type, but the equality check and delegated base_type.convert both handle that correctly.

Parameters:

Name Type Description Default
value str

Raw value from the CLI or the option default.

required
param Parameter | None

The Click parameter object (passed through to the base type).

required
ctx Context | None

The Click context (passed through to the base type).

required

Returns:

Type Description
str | int | float | bool

AUTO_STR if value equals it, otherwise the result of

str | int | float | bool

delegating to self.base_type.convert().

Source code in src/nemo_safe_synthesizer/configurator/pydantic_click_options.py
def convert(
    self,
    value: str,
    param: click.Parameter | None,
    ctx: click.Context | None,
) -> str | int | float | bool:
    """Convert the raw CLI value to ``AUTO_STR`` or the base numeric/bool type.

    The ``value`` parameter is typed ``str`` to match the parent
    ``click.ParamType.convert`` stub signature; in practice Click may also
    pass through default values of any type, but the equality check and
    delegated ``base_type.convert`` both handle that correctly.

    Args:
        value: Raw value from the CLI or the option default.
        param: The Click parameter object (passed through to the base type).
        ctx: The Click context (passed through to the base type).

    Returns:
        ``AUTO_STR`` if ``value`` equals it, otherwise the result of
        delegating to ``self.base_type.convert()``.
    """
    if value == AUTO_STR:
        return AUTO_STR
    return self.base_type.convert(value, param, ctx)

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