Skip to content

Commit cf9b2d2

Browse files
committed
Complete coverage for CliSettingsSource.
1 parent ce4586b commit cf9b2d2

File tree

2 files changed

+51
-16
lines changed

2 files changed

+51
-16
lines changed

pydantic_settings/sources/providers/cli.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,9 +1305,6 @@ def _serialized_args(self, model: PydanticModel, _is_submodel: bool = False) ->
13051305
value = self._update_alias_path_only_default(arg_name, value, field_info, alias_path_only_defaults)
13061306

13071307
if _CliPositionalArg in field_info.metadata:
1308-
if arg.is_alias_path_only:
1309-
positional_args.append(value)
1310-
continue
13111308
for value in model_default if isinstance(model_default, list) else [model_default]:
13121309
value = json.dumps(value) if isinstance(value, (dict, list, set)) else str(value)
13131310
positional_args.append(value)

tests/test_source_cli.py

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ class Cfg(BaseSettings, cli_avoid_json=avoid_json):
281281
alias_choice_w_only_path: str = Field(validation_alias=AliasChoices(AliasPath('path1', 1)))
282282
alias_choice_no_path: str = Field(validation_alias=AliasChoices('b', 'c'))
283283
alias_path: str = Field(validation_alias=AliasPath('path2', 'deep', 1))
284+
alias_extra_deep: str = Field(validation_alias=AliasPath('path3', 'deep', 'extra', 'deep', 1))
284285
alias_str: str = Field(validation_alias='str')
285286

286287
cfg = CliApp.run(
@@ -298,13 +299,16 @@ class Cfg(BaseSettings, cli_avoid_json=avoid_json):
298299
'a1,b1,c1',
299300
'--path2',
300301
'{"deep": ["a2","b2","c2"]}',
302+
'--path3',
303+
'{"deep": {"extra": {"deep": ["a3","b3","c3"]}}}',
301304
],
302305
)
303306
assert cfg.model_dump() == {
304307
'alias_choice_w_path': 'a',
305308
'alias_choice_w_only_path': 'b1',
306309
'alias_choice_no_path': 'b',
307310
'alias_path': 'b2',
311+
'alias_extra_deep': 'b3',
308312
'alias_str': 'str',
309313
}
310314

@@ -318,6 +322,8 @@ class Cfg(BaseSettings, cli_avoid_json=avoid_json):
318322
'b',
319323
'--path2',
320324
'{"deep": ["", "b2"]}',
325+
'--path3',
326+
'{"deep": {"extra": {"deep": ["", "b3"]}}}',
321327
'--str',
322328
'str',
323329
]
@@ -331,6 +337,7 @@ class Nested(BaseModel):
331337
alias_choice_w_only_path: str = Field(validation_alias=AliasChoices(AliasPath('path1', 1)))
332338
alias_choice_no_path: str = Field(validation_alias=AliasChoices('b', 'c'))
333339
alias_path: str = Field(validation_alias=AliasPath('path2', 'deep', 1))
340+
alias_extra_deep: str = Field(validation_alias=AliasPath('path3', 'deep', 'extra', 'deep', 1))
334341
alias_str: str = Field(validation_alias='str')
335342

336343
class Cfg(BaseSettings, cli_avoid_json=avoid_json):
@@ -351,6 +358,8 @@ class Cfg(BaseSettings, cli_avoid_json=avoid_json):
351358
'["a1","b1","c1"]',
352359
'--nest.path2',
353360
'{"deep": ["a2","b2","c2"]}',
361+
'--nest.path3',
362+
'{"deep": {"extra": {"deep": ["a3","b3","c3"]}}}',
354363
],
355364
)
356365
assert cfg.model_dump() == {
@@ -359,6 +368,7 @@ class Cfg(BaseSettings, cli_avoid_json=avoid_json):
359368
'alias_choice_w_only_path': 'b1',
360369
'alias_choice_no_path': 'b',
361370
'alias_path': 'b2',
371+
'alias_extra_deep': 'b3',
362372
'alias_str': 'str',
363373
}
364374
}
@@ -373,6 +383,8 @@ class Cfg(BaseSettings, cli_avoid_json=avoid_json):
373383
'b',
374384
'--nest.path2',
375385
'{"deep": ["", "b2"]}',
386+
'--nest.path3',
387+
'{"deep": {"extra": {"deep": ["", "b3"]}}}',
376388
'--nest.str',
377389
'str',
378390
]
@@ -2468,10 +2480,12 @@ class DeepSubModel(BaseModel):
24682480

24692481
class SubModel(BaseModel):
24702482
sub_subcmd: CliSubCommand[DeepSubModel]
2483+
sub_other_subcmd: CliSubCommand[DeepSubModel]
24712484
sub_arg: str
24722485

24732486
class Root(BaseModel):
24742487
root_subcmd: CliSubCommand[SubModel]
2488+
other_subcmd: CliSubCommand[SubModel]
24752489
root_arg: str
24762490

24772491
root = CliApp.run(
@@ -2487,9 +2501,11 @@ class Root(BaseModel):
24872501
)
24882502
assert root.model_dump() == {
24892503
'root_arg': 'hi',
2504+
'other_subcmd': None,
24902505
'root_subcmd': {
24912506
'sub_arg': 'hello',
24922507
'sub_subcmd': {'deep_pos_arg': 'hey', 'deep_arg': 'bye'},
2508+
'sub_other_subcmd': None,
24932509
},
24942510
}
24952511

@@ -2512,15 +2528,16 @@ class Root(BaseModel):
25122528
CliApp.run(Root)
25132529
assert (
25142530
capsys.readouterr().out
2515-
== f"""usage: example.py [-h] --root-arg str {{root-subcmd}} ...
2531+
== f"""usage: example.py [-h] --root-arg str {{root-subcmd,other-subcmd}} ...
25162532
25172533
{ARGPARSE_OPTIONS_TEXT}:
2518-
-h, --help show this help message and exit
2519-
--root-arg str (required)
2534+
-h, --help show this help message and exit
2535+
--root-arg str (required)
25202536
25212537
subcommands:
2522-
{{root-subcmd}}
2538+
{{root-subcmd,other-subcmd}}
25232539
root-subcmd
2540+
other-subcmd
25242541
"""
25252542
)
25262543

@@ -2529,15 +2546,17 @@ class Root(BaseModel):
25292546
CliApp.run(Root)
25302547
assert (
25312548
capsys.readouterr().out
2532-
== f"""usage: example.py root-subcmd [-h] --sub-arg str {{sub-subcmd}} ...
2549+
== f"""usage: example.py root-subcmd [-h] --sub-arg str
2550+
{{sub-subcmd,sub-other-subcmd}} ...
25332551
25342552
{ARGPARSE_OPTIONS_TEXT}:
2535-
-h, --help show this help message and exit
2536-
--sub-arg str (required)
2553+
-h, --help show this help message and exit
2554+
--sub-arg str (required)
25372555
25382556
subcommands:
2539-
{{sub-subcmd}}
2557+
{{sub-subcmd,sub-other-subcmd}}
25402558
sub-subcmd
2559+
sub-other-subcmd
25412560
"""
25422561
)
25432562

@@ -2786,37 +2805,56 @@ def test_cli_decoding():
27862805
class PathsDecode(BaseSettings):
27872806
path_a: Path = Field(validation_alias=AliasPath('paths', 0))
27882807
path_b: Path = Field(validation_alias=AliasPath('paths', 1))
2808+
num_a: int = Field(validation_alias=AliasPath('nums', 0))
2809+
num_b: int = Field(validation_alias=AliasPath('nums', 1))
27892810

2790-
assert CliApp.run(PathsDecode, cli_args=['--paths', PATH_A_STR, '--paths', PATH_B_STR]).model_dump() == {
2811+
assert CliApp.run(
2812+
PathsDecode, cli_args=['--paths', PATH_A_STR, '--paths', PATH_B_STR, '--nums', '1', '--nums', '2']
2813+
).model_dump() == {
27912814
'path_a': Path(PATH_A_STR),
27922815
'path_b': Path(PATH_B_STR),
2816+
'num_a': 1,
2817+
'num_b': 2,
27932818
}
27942819

27952820
class PathsListNoDecode(BaseSettings):
27962821
paths: Annotated[list[Path], NoDecode]
2822+
nums: Annotated[list[int], NoDecode]
27972823

27982824
@field_validator('paths', mode='before')
27992825
@classmethod
28002826
def decode_path_a(cls, paths: str) -> list[Path]:
28012827
return [Path(p) for p in paths.split(',')]
28022828

2803-
assert CliApp.run(PathsListNoDecode, cli_args=['--paths', f'{PATH_A_STR},{PATH_B_STR}']).model_dump() == {
2804-
'paths': [Path(PATH_A_STR), Path(PATH_B_STR)]
2805-
}
2829+
@field_validator('nums', mode='before')
2830+
@classmethod
2831+
def decode_nums(cls, nums: str) -> list[int]:
2832+
return [int(n) for n in nums.split(',')]
2833+
2834+
assert CliApp.run(
2835+
PathsListNoDecode, cli_args=['--paths', f'{PATH_A_STR},{PATH_B_STR}', '--nums', '1,2']
2836+
).model_dump() == {'paths': [Path(PATH_A_STR), Path(PATH_B_STR)], 'nums': [1, 2]}
28062837

28072838
class PathsAliasNoDecode(BaseSettings):
28082839
path_a: Annotated[Path, NoDecode] = Field(validation_alias=AliasPath('paths', 0))
28092840
path_b: Annotated[Path, NoDecode] = Field(validation_alias=AliasPath('paths', 1))
2841+
num_a: Annotated[int, NoDecode] = Field(validation_alias=AliasPath('nums', 0))
2842+
num_b: Annotated[int, NoDecode] = Field(validation_alias=AliasPath('nums', 1))
28102843

28112844
@model_validator(mode='before')
28122845
@classmethod
28132846
def intercept_kwargs(cls, data: Any) -> Any:
28142847
data['paths'] = [Path(p) for p in data['paths'].split(',')]
2848+
data['nums'] = [int(n) for n in data['nums'].split(',')]
28152849
return data
28162850

2817-
assert CliApp.run(PathsAliasNoDecode, cli_args=['--paths', f'{PATH_A_STR},{PATH_B_STR}']).model_dump() == {
2851+
assert CliApp.run(
2852+
PathsAliasNoDecode, cli_args=['--paths', f'{PATH_A_STR},{PATH_B_STR}', '--nums', '1,2']
2853+
).model_dump() == {
28182854
'path_a': Path(PATH_A_STR),
28192855
'path_b': Path(PATH_B_STR),
2856+
'num_a': 1,
2857+
'num_b': 2,
28202858
}
28212859

28222860
with pytest.raises(

0 commit comments

Comments
 (0)