diff --git a/docs/index.md b/docs/index.md index cc58bd2..640cd3a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1326,6 +1326,42 @@ print(Settings().model_dump()) #> {'my_arg': 'hi'} ``` +#### Suppressing Fields from CLI Help Text + +To suppress a field from the CLI help text, the `CliSuppress` annotation can be used for field types, or the +`CLI_SUPPRESS` string constant can be used for field descriptions. + +```py +import sys + +from pydantic import Field + +from pydantic_settings import CLI_SUPPRESS, BaseSettings, CliSuppress + + +class Settings(BaseSettings, cli_parse_args=True): + """Suppress fields from CLI help text.""" + + field_a: CliSuppress[int] = 0 + field_b: str = Field(default=1, description=CLI_SUPPRESS) + + +try: + sys.argv = ['example.py', '--help'] + Settings() +except SystemExit as e: + print(e) + #> 0 +""" +usage: example.py [-h] + +Suppress fields from CLI help text. + +options: + -h, --help show this help message and exit +""" +``` + ### Integrating with Existing Parsers A CLI settings source can be integrated with existing parsers by overriding the default CLI settings source with a user diff --git a/pydantic_settings/__init__.py b/pydantic_settings/__init__.py index 3e1c8f8..fd42d3e 100644 --- a/pydantic_settings/__init__.py +++ b/pydantic_settings/__init__.py @@ -1,11 +1,13 @@ from .main import BaseSettings, CliApp, SettingsConfigDict from .sources import ( + CLI_SUPPRESS, AzureKeyVaultSettingsSource, CliExplicitFlag, CliImplicitFlag, CliPositionalArg, CliSettingsSource, CliSubCommand, + CliSuppress, DotEnvSettingsSource, EnvSettingsSource, InitSettingsSource, @@ -27,6 +29,8 @@ 'CliApp', 'CliSettingsSource', 'CliSubCommand', + 'CliSuppress', + 'CLI_SUPPRESS', 'CliPositionalArg', 'CliExplicitFlag', 'CliImplicitFlag', diff --git a/pydantic_settings/sources.py b/pydantic_settings/sources.py index 91e004a..ef2d774 100644 --- a/pydantic_settings/sources.py +++ b/pydantic_settings/sources.py @@ -156,6 +156,8 @@ def error(self, message: str) -> NoReturn: _CliBoolFlag = TypeVar('_CliBoolFlag', bound=bool) CliImplicitFlag = Annotated[_CliBoolFlag, _CliImplicitFlag] CliExplicitFlag = Annotated[_CliBoolFlag, _CliExplicitFlag] +CLI_SUPPRESS = SUPPRESS +CliSuppress = Annotated[T, CLI_SUPPRESS] def get_subcommand( @@ -1647,7 +1649,7 @@ def _add_parser_args( ) is_parser_submodel = sub_models and not is_append_action kwargs: dict[str, Any] = {} - kwargs['default'] = SUPPRESS + kwargs['default'] = CLI_SUPPRESS kwargs['help'] = self._help_format(field_name, field_info, model_default) kwargs['metavar'] = self._metavar_format(field_info.annotation) kwargs['required'] = ( @@ -1816,7 +1818,7 @@ def _add_parser_alias_paths( else f'{arg_prefix.replace(subcommand_prefix, "", 1)}{name}' ) kwargs: dict[str, Any] = {} - kwargs['default'] = SUPPRESS + kwargs['default'] = CLI_SUPPRESS kwargs['help'] = 'pydantic alias path' kwargs['dest'] = f'{arg_prefix}{name}' if metavar == 'dict' or is_nested_alias_path: @@ -1883,6 +1885,9 @@ def _metavar_format(self, obj: Any) -> str: def _help_format(self, field_name: str, field_info: FieldInfo, model_default: Any) -> str: _help = field_info.description if field_info.description else '' + if _help == CLI_SUPPRESS or CLI_SUPPRESS in field_info.metadata: + return CLI_SUPPRESS + if field_info.is_required() and model_default in (PydanticUndefined, None): if _CliPositionalArg not in field_info.metadata: ifdef = 'ifdef: ' if model_default is None else '' diff --git a/tests/test_source_cli.py b/tests/test_source_cli.py index 517ae7a..7101d68 100644 --- a/tests/test_source_cli.py +++ b/tests/test_source_cli.py @@ -30,11 +30,13 @@ SettingsConfigDict, ) from pydantic_settings.sources import ( + CLI_SUPPRESS, CliExplicitFlag, CliImplicitFlag, CliPositionalArg, CliSettingsSource, CliSubCommand, + CliSuppress, SettingsError, get_subcommand, ) @@ -1648,10 +1650,10 @@ def test_cli_flag_prefix_char(): class Cfg(BaseSettings, cli_flag_prefix_char='+'): my_var: str = Field(validation_alias=AliasChoices('m', 'my-var')) - cfg = Cfg(_cli_parse_args=['++my-var=hello']) + cfg = CliApp.run(Cfg, cli_args=['++my-var=hello']) assert cfg.model_dump() == {'my_var': 'hello'} - cfg = Cfg(_cli_parse_args=['+m=hello']) + cfg = CliApp.run(Cfg, cli_args=['+m=hello']) assert cfg.model_dump() == {'my_var': 'hello'} @@ -2017,3 +2019,24 @@ def cli_cmd(self) -> None: CliApp.run_subcommand(self) CliApp.run(Root, cli_args=['child', '--val=hello']) + + +def test_cli_suppress(capsys, monkeypatch): + class Settings(BaseSettings, cli_parse_args=True): + field_a: CliSuppress[int] = 0 + field_b: str = Field(default=1, description=CLI_SUPPRESS) + + with monkeypatch.context() as m: + m.setattr(sys, 'argv', ['example.py', '--help']) + + with pytest.raises(SystemExit): + CliApp.run(Settings) + + assert ( + capsys.readouterr().out + == f"""usage: example.py [-h] + +{ARGPARSE_OPTIONS_TEXT}: + -h, --help show this help message and exit +""" + )