From 1775221b09d434b323b5f2bc42355be5b7d71655 Mon Sep 17 00:00:00 2001 From: Kyle Schwab Date: Sun, 13 Apr 2025 08:25:37 -0600 Subject: [PATCH 1/4] CLI submodel suppress. --- pydantic_settings/sources/providers/cli.py | 19 +++++++++++---- tests/test_source_cli.py | 28 ++++++++++++++++++++-- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/pydantic_settings/sources/providers/cli.py b/pydantic_settings/sources/providers/cli.py index 775a7cea..359828ec 100644 --- a/pydantic_settings/sources/providers/cli.py +++ b/pydantic_settings/sources/providers/cli.py @@ -665,6 +665,7 @@ def _add_parser_args( group: Any, alias_prefixes: list[str], model_default: Any, + is_model_suppressed: bool = False, ) -> ArgumentParser: subparsers: Any = None alias_path_args: dict[str, str] = {} @@ -738,7 +739,7 @@ def _add_parser_args( is_parser_submodel = sub_models and not is_append_action kwargs: dict[str, Any] = {} kwargs['default'] = CLI_SUPPRESS - kwargs['help'] = self._help_format(field_name, field_info, model_default) + kwargs['help'] = self._help_format(field_name, field_info, model_default, is_model_suppressed) kwargs['metavar'] = self._metavar_format(field_info.annotation) kwargs['required'] = ( self.cli_enforce_required and field_info.is_required() and model_default is PydanticUndefined @@ -782,6 +783,7 @@ def _add_parser_args( field_info, alias_names, model_default=model_default, + is_model_suppressed=is_model_suppressed, ) elif not is_alias_path_only: if group is not None: @@ -869,6 +871,7 @@ def _add_parser_submodels( field_info: FieldInfo, alias_names: tuple[str, ...], model_default: Any, + is_model_suppressed: bool, ) -> None: if issubclass(model, CliMutuallyExclusiveGroup): # Argparse has deprecated "calling add_argument_group() or add_mutually_exclusive_group() on a @@ -906,9 +909,10 @@ def _add_parser_submodels( model_group_kwargs['description'] = desc_header preferred_alias = alias_names[0] + is_model_suppressed = self._is_field_suppressed(field_info) or is_model_suppressed if not self.cli_avoid_json: added_args.append(arg_names[0]) - kwargs['help'] = f'set {arg_names[0]} from JSON string' + kwargs['help'] = CLI_SUPPRESS if is_model_suppressed else f'set {arg_names[0]} from JSON string' model_group = self._add_group(parser, **model_group_kwargs) self._add_argument(model_group, *(f'{flag_prefix}{name}' for name in arg_names), **kwargs) for model in sub_models: @@ -921,6 +925,7 @@ def _add_parser_submodels( group=model_group if model_group else model_group_kwargs, alias_prefixes=[f'{arg_prefix}{name}.' for name in alias_names[1:]], model_default=model_default, + is_model_suppressed=is_model_suppressed, ) def _add_parser_alias_paths( @@ -1013,9 +1018,11 @@ def _metavar_format_recurse(self, obj: Any) -> str: def _metavar_format(self, obj: Any) -> str: return self._metavar_format_recurse(obj).replace(', ', ',') - def _help_format(self, field_name: str, field_info: FieldInfo, model_default: Any) -> str: + def _help_format( + self, field_name: str, field_info: FieldInfo, model_default: Any, is_model_suppressed: bool + ) -> str: _help = field_info.description if field_info.description else '' - if _help == CLI_SUPPRESS or CLI_SUPPRESS in field_info.metadata: + if is_model_suppressed or self._is_field_suppressed(field_info): return CLI_SUPPRESS if field_info.is_required() and model_default in (PydanticUndefined, None): @@ -1035,3 +1042,7 @@ def _help_format(self, field_name: str, field_info: FieldInfo, model_default: An default = f'(default factory: {self._metavar_format(field_info.default_factory)})' _help += f' {default}' if _help else default return _help.replace('%', '%%') if issubclass(type(self._root_parser), ArgumentParser) else _help + + def _is_field_suppressed(self, field_info: FieldInfo) -> bool: + _help = field_info.description if field_info.description else '' + return _help == CLI_SUPPRESS or CLI_SUPPRESS in field_info.metadata diff --git a/tests/test_source_cli.py b/tests/test_source_cli.py index 681e9743..f2175436 100644 --- a/tests/test_source_cli.py +++ b/tests/test_source_cli.py @@ -2137,9 +2137,25 @@ def cli_cmd(self) -> None: def test_cli_suppress(capsys, monkeypatch): + class DeepHiddenSubModel(BaseModel): + deep_hidden_a: int + deep_hidden_b: int + + class HiddenSubModel(BaseModel): + hidden_a: int + hidden_b: int + deep_hidden_obj: DeepHiddenSubModel + + class SubModel(BaseModel): + visible_a: int + visible_b: int + deep_hidden_obj: CliSuppress[DeepHiddenSubModel] + class Settings(BaseSettings, cli_parse_args=True): field_a: CliSuppress[int] = 0 field_b: str = Field(default=1, description=CLI_SUPPRESS) + hidden_obj: CliSuppress[HiddenSubModel] + visible_obj: SubModel with monkeypatch.context() as m: m.setattr(sys, 'argv', ['example.py', '--help']) @@ -2149,10 +2165,18 @@ class Settings(BaseSettings, cli_parse_args=True): assert ( capsys.readouterr().out - == f"""usage: example.py [-h] + == f"""usage: example.py [-h] [--visible_obj JSON] [--visible_obj.visible_a int] + [--visible_obj.visible_b int] {ARGPARSE_OPTIONS_TEXT}: - -h, --help show this help message and exit + -h, --help show this help message and exit + +visible_obj options: + --visible_obj JSON set visible_obj from JSON string + --visible_obj.visible_a int + (required) + --visible_obj.visible_b int + (required) """ ) From f4547a01336be34a8519db83a9cc047517b82b57 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Mon, 14 Apr 2025 18:59:04 +0330 Subject: [PATCH 2/4] Update pydantic_settings/sources/providers/cli.py --- pydantic_settings/sources/providers/cli.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pydantic_settings/sources/providers/cli.py b/pydantic_settings/sources/providers/cli.py index dddc7995..15773852 100644 --- a/pydantic_settings/sources/providers/cli.py +++ b/pydantic_settings/sources/providers/cli.py @@ -914,7 +914,9 @@ def _add_parser_submodels( added_args.append(arg_names[0]) kwargs['nargs'] = '?' kwargs['const'] = '{}' - kwargs['help'] = kwargs['help'] = CLI_SUPPRESS if is_model_suppressed else f'set {arg_names[0]} from JSON string (default: {{}})' + kwargs['help'] = kwargs['help'] = ( + CLI_SUPPRESS if is_model_suppressed else f'set {arg_names[0]} from JSON string (default: {{}})' + ) model_group = self._add_group(parser, **model_group_kwargs) self._add_argument(model_group, *(f'{flag_prefix}{name}' for name in arg_names), **kwargs) for model in sub_models: From c3d1ec723c62fdfa4b2d3b940f0cc04936679fc6 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Mon, 14 Apr 2025 19:01:33 +0330 Subject: [PATCH 3/4] Update tests/test_source_cli.py --- tests/test_source_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_source_cli.py b/tests/test_source_cli.py index e6b2c694..fa297018 100644 --- a/tests/test_source_cli.py +++ b/tests/test_source_cli.py @@ -2167,7 +2167,7 @@ class Settings(BaseSettings, cli_parse_args=True): assert ( capsys.readouterr().out - == f"""usage: example.py [-h] [--visible_obj JSON] [--visible_obj.visible_a int] + == f"""usage: example.py [-h] [--visible_obj [JSON]] [--visible_obj.visible_a int] [--visible_obj.visible_b int] {ARGPARSE_OPTIONS_TEXT}: From c2a75dde5745b0e968163300f07a5761a843f3e1 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Mon, 14 Apr 2025 19:01:53 +0330 Subject: [PATCH 4/4] Update tests/test_source_cli.py --- tests/test_source_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_source_cli.py b/tests/test_source_cli.py index fa297018..c6b25d6e 100644 --- a/tests/test_source_cli.py +++ b/tests/test_source_cli.py @@ -2174,7 +2174,7 @@ class Settings(BaseSettings, cli_parse_args=True): -h, --help show this help message and exit visible_obj options: - --visible_obj JSON set visible_obj from JSON string + --visible_obj [JSON] set visible_obj from JSON string (default: {{}}) --visible_obj.visible_a int (required) --visible_obj.visible_b int