Skip to content

Commit c14aada

Browse files
committed
Add kebab case to subcommands and positionals.
1 parent 07d9b5a commit c14aada

File tree

3 files changed

+78
-39
lines changed

3 files changed

+78
-39
lines changed

docs/index.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,9 +1131,6 @@ print(Settings().model_dump())
11311131

11321132
Change whether CLI arguments should use kebab case by enabling `cli_kebab_case`.
11331133

1134-
!!! note
1135-
CLI kebab case does not apply to subcommand or positional arguments, which must still use aliasing.
1136-
11371134
```py
11381135
import sys
11391136

pydantic_settings/sources.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1618,7 +1618,9 @@ def _add_parser_args(
16181618
preferred_alias = alias_names[0]
16191619
if _CliSubCommand in field_info.metadata:
16201620
for model in sub_models:
1621-
subcommand_alias = model.__name__ if len(sub_models) > 1 else preferred_alias
1621+
subcommand_alias = self._check_kebab_name(
1622+
model.__name__ if len(sub_models) > 1 else preferred_alias
1623+
)
16221624
subcommand_name = f'{arg_prefix}{subcommand_alias}'
16231625
subcommand_dest = f'{arg_prefix}{preferred_alias}'
16241626
self._cli_subcommands[f'{arg_prefix}:subcommand'][subcommand_name] = subcommand_dest
@@ -1692,7 +1694,7 @@ def _add_parser_args(
16921694
self._cli_dict_args[kwargs['dest']] = field_info.annotation
16931695

16941696
if _CliPositionalArg in field_info.metadata:
1695-
kwargs['metavar'] = preferred_alias.upper()
1697+
kwargs['metavar'] = self._check_kebab_name(preferred_alias.upper())
16961698
arg_names = [kwargs['dest']]
16971699
del kwargs['dest']
16981700
del kwargs['required']
@@ -1731,6 +1733,11 @@ def _add_parser_args(
17311733
self._add_parser_alias_paths(parser, alias_path_args, added_args, arg_prefix, subcommand_prefix, group)
17321734
return parser
17331735

1736+
def _check_kebab_name(self, name: str) -> str:
1737+
if self.cli_kebab_case:
1738+
return name.replace('_', '-')
1739+
return name
1740+
17341741
def _convert_bool_flag(self, kwargs: dict[str, Any], field_info: FieldInfo, model_default: Any) -> None:
17351742
if kwargs['metavar'] == 'bool':
17361743
default = None
@@ -1758,13 +1765,11 @@ def _get_arg_names(
17581765
arg_names: list[str] = []
17591766
for prefix in [arg_prefix] + alias_prefixes:
17601767
for name in alias_names:
1761-
arg_name = (
1768+
arg_name = self._check_kebab_name(
17621769
f'{prefix}{name}'
17631770
if subcommand_prefix == self.env_prefix
17641771
else f'{prefix.replace(subcommand_prefix, "", 1)}{name}'
17651772
)
1766-
if self.cli_kebab_case:
1767-
arg_name = arg_name.replace('_', '-')
17681773
if arg_name not in added_args:
17691774
arg_names.append(arg_name)
17701775
return arg_names

tests/test_source_cli.py

Lines changed: 68 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2270,51 +2270,88 @@ class MySettings(BaseSettings):
22702270
)
22712271

22722272

2273-
def test_cli_kebab_case(env, capsys, monkeypatch):
2273+
def test_cli_kebab_case(capsys, monkeypatch):
2274+
class DeepSubModel(BaseModel):
2275+
deep_submodel_positional_arg: CliPositionalArg[str]
2276+
deep_submodel_arg: str
2277+
22742278
class SubModel(BaseModel):
2275-
v1: str = 'default'
2276-
v2: bytes = b'hello'
2277-
v3: int
2279+
submodel_subcommand: CliSubCommand[DeepSubModel]
2280+
submodel_arg: str
22782281

2279-
class Settings(BaseSettings):
2280-
model_config = SettingsConfigDict(
2281-
env_prefix='MYTEST_',
2282-
env_nested_delimiter='__',
2283-
nested_model_default_partial_update=True,
2284-
cli_parse_args=True,
2285-
cli_kebab_case=True,
2286-
)
2282+
class Root(BaseModel):
2283+
root_subcommand: CliSubCommand[SubModel]
2284+
root_arg: str
22872285

2288-
v0: str = 'ok'
2289-
sub_model: SubModel = SubModel(v1='top default', v3=33)
2286+
assert CliApp.run(
2287+
Root,
2288+
cli_args=[
2289+
'--root-arg=hi',
2290+
'root-subcommand',
2291+
'--submodel-arg=hello',
2292+
'submodel-subcommand',
2293+
'hey',
2294+
'--deep-submodel-arg=bye',
2295+
],
2296+
).model_dump() == {
2297+
'root_arg': 'hi',
2298+
'root_subcommand': {
2299+
'submodel_arg': 'hello',
2300+
'submodel_subcommand': {'deep_submodel_positional_arg': 'hey', 'deep_submodel_arg': 'bye'},
2301+
},
2302+
}
22902303

22912304
with monkeypatch.context() as m:
22922305
m.setattr(sys, 'argv', ['example.py', '--help'])
2293-
22942306
with pytest.raises(SystemExit):
2295-
CliApp.run(Settings)
2307+
CliApp.run(Root)
2308+
assert (
2309+
capsys.readouterr().out
2310+
== f"""usage: example.py [-h] --root-arg str {{root-subcommand}} ...
2311+
2312+
{ARGPARSE_OPTIONS_TEXT}:
2313+
-h, --help show this help message and exit
2314+
--root-arg str (required)
22962315
2316+
subcommands:
2317+
{{root-subcommand}}
2318+
root-subcommand
2319+
"""
2320+
)
2321+
2322+
m.setattr(sys, 'argv', ['example.py', 'root-subcommand', '--help'])
2323+
with pytest.raises(SystemExit):
2324+
CliApp.run(Root)
22972325
assert (
22982326
capsys.readouterr().out
2299-
== f"""usage: example.py [-h] [--v0 str] [--sub-model JSON] [--sub-model.v1 str]
2300-
[--sub-model.v2 bytes] [--sub-model.v3 int]
2327+
== f"""usage: example.py root-subcommand [-h] --submodel-arg str
2328+
{{submodel-subcommand}} ...
23012329
23022330
{ARGPARSE_OPTIONS_TEXT}:
23032331
-h, --help show this help message and exit
2304-
--v0 str (default: ok)
2332+
--submodel-arg str (required)
23052333
2306-
sub-model options:
2307-
--sub-model JSON set sub-model from JSON string
2308-
--sub-model.v1 str (default: top default)
2309-
--sub-model.v2 bytes (default: b'hello')
2310-
--sub-model.v3 int (default: 33)
2334+
subcommands:
2335+
{{submodel-subcommand}}
2336+
submodel-subcommand
23112337
"""
23122338
)
23132339

2314-
env.set('MYTEST_V0', 'env with prefix')
2315-
env.set('MYTEST_SUB_MODEL__V1', 'env with prefix')
2316-
env.set('MYTEST_SUB_MODEL__V2', 'env with prefix')
2317-
assert CliApp.run(Settings, cli_args=['--sub-model.v1=cli']).model_dump() == {
2318-
'v0': 'env with prefix',
2319-
'sub_model': {'v1': 'cli', 'v2': b'env with prefix', 'v3': 33},
2320-
}
2340+
m.setattr(sys, 'argv', ['example.py', 'root-subcommand', 'submodel-subcommand', '--help'])
2341+
with pytest.raises(SystemExit):
2342+
CliApp.run(Root)
2343+
assert (
2344+
capsys.readouterr().out
2345+
== f"""usage: example.py root-subcommand submodel-subcommand [-h]
2346+
--deep-submodel-arg str
2347+
DEEP-SUBMODEL-POSITIONAL-ARG
2348+
2349+
positional arguments:
2350+
DEEP-SUBMODEL-POSITIONAL-ARG
2351+
2352+
{ARGPARSE_OPTIONS_TEXT}:
2353+
-h, --help show this help message and exit
2354+
--deep-submodel-arg str
2355+
(required)
2356+
"""
2357+
)

0 commit comments

Comments
 (0)