@@ -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
25212537subcommands:
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
25382556subcommands:
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