diff --git a/pydantic_settings/main.py b/pydantic_settings/main.py index 8a6e2f2e..d2338495 100644 --- a/pydantic_settings/main.py +++ b/pydantic_settings/main.py @@ -559,6 +559,7 @@ def run( if not issubclass(model_cls, BaseSettings): class CliAppBaseSettings(BaseSettings, model_cls): # type: ignore + __doc__ = model_cls.__doc__ model_config = SettingsConfigDict( nested_model_default_partial_update=True, case_sensitive=True, diff --git a/pydantic_settings/sources/providers/cli.py b/pydantic_settings/sources/providers/cli.py index 775a7cea..26d17e4d 100644 --- a/pydantic_settings/sources/providers/cli.py +++ b/pydantic_settings/sources/providers/cli.py @@ -908,7 +908,9 @@ def _add_parser_submodels( preferred_alias = alias_names[0] if not self.cli_avoid_json: added_args.append(arg_names[0]) - kwargs['help'] = f'set {arg_names[0]} from JSON string' + kwargs['nargs'] = '?' + kwargs['const'] = '{}' + kwargs['help'] = 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: diff --git a/tests/test_source_cli.py b/tests/test_source_cli.py index 681e9743..8a50091e 100644 --- a/tests/test_source_cli.py +++ b/tests/test_source_cli.py @@ -499,14 +499,14 @@ class Car(BaseSettings, cli_parse_args=True): Car() assert ( capsys.readouterr().out - == f"""usage: example.py [-h] [--driver JSON] [--driver.meow str] [--driver.bark str] - [--driver.caww str] [--driver.tweet str] + == f"""usage: example.py [-h] [--driver [JSON]] [--driver.meow str] + [--driver.bark str] [--driver.caww str] [--driver.tweet str] {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit driver options: - --driver JSON set driver from JSON string + --driver [JSON] set driver from JSON string (default: {{}}) --driver.meow str (default: purr) --driver.bark str (default: bark) --driver.caww str (default: caww) @@ -540,17 +540,17 @@ class Settings(BaseSettings, cli_parse_args=True): Settings() assert ( capsys.readouterr().out - == f"""usage: example.py [-h] [--flag bool] [--sub_model JSON] - [--sub_model.flag bool] [--sub_model.deep JSON] + == f"""usage: example.py [-h] [--flag bool] [--sub_model [JSON]] + [--sub_model.flag bool] [--sub_model.deep [JSON]] [--sub_model.deep.flag bool] - [--sub_model.deep.deeper {{JSON,null}}] + [--sub_model.deep.deeper [{{JSON,null}}]] [--sub_model.deep.deeper.flag bool] - [--opt_model {{JSON,null}}] [--opt_model.flag bool] - [--opt_model.deeper {{JSON,null}}] - [--opt_model.deeper.flag bool] [--fact_model JSON] - [--fact_model.flag bool] [--fact_model.deep JSON] + [--opt_model [{{JSON,null}}]] [--opt_model.flag bool] + [--opt_model.deeper [{{JSON,null}}]] + [--opt_model.deeper.flag bool] [--fact_model [JSON]] + [--fact_model.flag bool] [--fact_model.deep [JSON]] [--fact_model.deep.flag bool] - [--fact_model.deep.deeper {{JSON,null}}] + [--fact_model.deep.deeper [{{JSON,null}}]] [--fact_model.deep.deeper.flag bool] {ARGPARSE_OPTIONS_TEXT}: @@ -558,21 +558,22 @@ class Settings(BaseSettings, cli_parse_args=True): --flag bool (default: True) sub_model options: - --sub_model JSON set sub_model from JSON string + --sub_model [JSON] set sub_model from JSON string (default: {{}}) --sub_model.flag bool (default: False) sub_model.deep options: - --sub_model.deep JSON - set sub_model.deep from JSON string + --sub_model.deep [JSON] + set sub_model.deep from JSON string (default: {{}}) --sub_model.deep.flag bool (default: True) sub_model.deep.deeper options: default: null (undefined) - --sub_model.deep.deeper {{JSON,null}} - set sub_model.deep.deeper from JSON string + --sub_model.deep.deeper [{{JSON,null}}] + set sub_model.deep.deeper from JSON string (default: + {{}}) --sub_model.deep.deeper.flag bool (ifdef: required) @@ -580,33 +581,34 @@ class Settings(BaseSettings, cli_parse_args=True): default: null (undefined) Group Doc - --opt_model {{JSON,null}} - set opt_model from JSON string + --opt_model [{{JSON,null}}] + set opt_model from JSON string (default: {{}}) --opt_model.flag bool (ifdef: required) opt_model.deeper options: default: null (undefined) - --opt_model.deeper {{JSON,null}} - set opt_model.deeper from JSON string + --opt_model.deeper [{{JSON,null}}] + set opt_model.deeper from JSON string (default: {{}}) --opt_model.deeper.flag bool (ifdef: required) fact_model options: - --fact_model JSON set fact_model from JSON string + --fact_model [JSON] set fact_model from JSON string (default: {{}}) --fact_model.flag bool (default factory: ) fact_model.deep options: - --fact_model.deep JSON - set fact_model.deep from JSON string + --fact_model.deep [JSON] + set fact_model.deep from JSON string (default: {{}}) --fact_model.deep.flag bool (default factory: ) fact_model.deep.deeper options: - --fact_model.deep.deeper {{JSON,null}} - set fact_model.deep.deeper from JSON string + --fact_model.deep.deeper [{{JSON,null}}] + set fact_model.deep.deeper from JSON string (default: + {{}}) --fact_model.deep.deeper.flag bool (default factory: ) """ @@ -1529,13 +1531,13 @@ class Settings(BaseSettings): assert ( capsys.readouterr().out - == f"""usage: example.py [-h] [--sub_model JSON] [--sub_model.v1 int] + == f"""usage: example.py [-h] [--sub_model [JSON]] [--sub_model.v1 int] {ARGPARSE_OPTIONS_TEXT}: -h, --help show this help message and exit sub_model options: - --sub_model JSON set sub_model from JSON string + --sub_model [JSON] set sub_model from JSON string (default: {{}}) --sub_model.v1 int (required) """ ) @@ -1573,13 +1575,13 @@ class Settings(BaseSettings): assert ( capsys.readouterr().out - == f"""usage: example.py [-h] [--sub_model JSON] + == f"""usage: example.py [-h] [--sub_model [JSON]] {ARGPARSE_OPTIONS_TEXT}: - -h, --help show this help message and exit + -h, --help show this help message and exit sub_model options: - --sub_model JSON set sub_model from JSON string + --sub_model [JSON] set sub_model from JSON string (default: {{}}) """ ) @@ -1653,7 +1655,7 @@ class Settings(BaseSettings): assert ( capsys.readouterr().out - == f"""usage: example.py [-h] [--sub_model JSON] [--sub_model.v1 int] + == f"""usage: example.py [-h] [--sub_model [JSON]] [--sub_model.v1 int] My application help text. @@ -1663,7 +1665,7 @@ class Settings(BaseSettings): sub_model options: The help text from the field description - --sub_model JSON set sub_model from JSON string + --sub_model [JSON] set sub_model from JSON string (default: {{}}) --sub_model.v1 int (required) """ ) @@ -1673,7 +1675,7 @@ class Settings(BaseSettings): assert ( capsys.readouterr().out - == f"""usage: example.py [-h] [--sub_model JSON] [--sub_model.v1 int] + == f"""usage: example.py [-h] [--sub_model [JSON]] [--sub_model.v1 int] My application help text. @@ -1683,7 +1685,7 @@ class Settings(BaseSettings): sub_model options: The help text from the class docstring - --sub_model JSON set sub_model from JSON string + --sub_model [JSON] set sub_model from JSON string (default: {{}}) --sub_model.v1 int (required) """ ) @@ -2411,3 +2413,17 @@ class Root(BaseModel): --deep-arg str (required) """ ) + + +def test_cli_json_optional_default(): + class Nested(BaseModel): + foo: int = 1 + bar: int = 2 + + class Options(BaseSettings): + nested: Nested = Nested(foo=3, bar=4) + + assert CliApp.run(Options, cli_args=[]).model_dump() == {'nested': {'foo': 3, 'bar': 4}} + assert CliApp.run(Options, cli_args=['--nested']).model_dump() == {'nested': {'foo': 1, 'bar': 2}} + assert CliApp.run(Options, cli_args=['--nested={}']).model_dump() == {'nested': {'foo': 1, 'bar': 2}} + assert CliApp.run(Options, cli_args=['--nested.foo=5']).model_dump() == {'nested': {'foo': 5, 'bar': 2}}