@@ -1157,30 +1157,67 @@ def _get_sub_models(self, model: type[BaseModel], field_name: str, field_info: F
11571157 sub_models .append (type_ ) # type: ignore
11581158 return sub_models
11591159
1160- def _sort_arg_fields (self , model : type [BaseModel ]) -> list [tuple [str , str , FieldInfo ]]:
1160+ def _get_resolved_names (
1161+ self , field_name : str , field_info : FieldInfo , alias_path_args : dict [str , str ]
1162+ ) -> tuple [tuple [str , ...], bool ]:
1163+ resolved_names : list [str ] = []
1164+ is_alias_path_only : bool = True
1165+ if not any ((field_info .alias , field_info .validation_alias )):
1166+ resolved_names += [field_name ]
1167+ is_alias_path_only = False
1168+ else :
1169+ new_alias_paths : list [AliasPath ] = []
1170+ for alias in (field_info .alias , field_info .validation_alias ):
1171+ if alias is None :
1172+ continue
1173+ elif isinstance (alias , str ):
1174+ resolved_names .append (alias )
1175+ is_alias_path_only = False
1176+ elif isinstance (alias , AliasChoices ):
1177+ for name in alias .choices :
1178+ if isinstance (name , str ):
1179+ resolved_names .append (name )
1180+ is_alias_path_only = False
1181+ else :
1182+ new_alias_paths .append (name )
1183+ else :
1184+ new_alias_paths .append (alias )
1185+ for alias_path in new_alias_paths :
1186+ name = cast (str , alias_path .path [0 ])
1187+ name = name .lower () if not self .case_sensitive else name
1188+ alias_path_args [name ] = 'dict' if len (alias_path .path ) > 2 else 'list'
1189+ if not resolved_names and is_alias_path_only :
1190+ resolved_names .append (name )
1191+ if not self .case_sensitive :
1192+ resolved_names = [resolved_name .lower () for resolved_name in resolved_names ]
1193+ return tuple (dict .fromkeys (resolved_names )), is_alias_path_only
1194+
1195+ def _sort_arg_fields (self , model : type [BaseModel ]) -> list [tuple [str , FieldInfo ]]:
11611196 positional_args , subcommand_args , optional_args = [], [], []
11621197 fields = model .__pydantic_fields__ if is_pydantic_dataclass (model ) else model .model_fields
11631198 for field_name , field_info in fields .items ():
1164- resolved_name = field_name if field_info .alias is None else field_info .alias
1165- resolved_name = resolved_name .lower () if not self .case_sensitive else resolved_name
11661199 if _CliSubCommand in field_info .metadata :
11671200 if not field_info .is_required ():
11681201 raise SettingsError (f'subcommand argument { model .__name__ } .{ field_name } has a default value' )
1202+ elif any ((field_info .alias , field_info .validation_alias )):
1203+ raise SettingsError (f'subcommand argument { model .__name__ } .{ field_name } has an alias' )
11691204 else :
11701205 field_types = [type_ for type_ in get_args (field_info .annotation ) if type_ is not type (None )]
11711206 if len (field_types ) != 1 :
11721207 raise SettingsError (f'subcommand argument { model .__name__ } .{ field_name } has multiple types' )
11731208 elif not is_model_class (field_types [0 ]):
11741209 raise SettingsError (
1175- f'subcommand argument { model .__name__ } .{ resolved_name } is not derived from BaseModel'
1210+ f'subcommand argument { model .__name__ } .{ field_name } is not derived from BaseModel'
11761211 )
1177- subcommand_args .append ((field_name , resolved_name , field_info ))
1212+ subcommand_args .append ((field_name , field_info ))
11781213 elif _CliPositionalArg in field_info .metadata :
11791214 if not field_info .is_required ():
11801215 raise SettingsError (f'positional argument { model .__name__ } .{ field_name } has a default value' )
1181- positional_args .append ((field_name , resolved_name , field_info ))
1216+ elif any ((field_info .alias , field_info .validation_alias )):
1217+ raise SettingsError (f'positional argument { model .__name__ } .{ field_name } has an alias' )
1218+ positional_args .append ((field_name , field_info ))
11821219 else :
1183- optional_args .append ((field_name , resolved_name , field_info ))
1220+ optional_args .append ((field_name , field_info ))
11841221 return positional_args + subcommand_args + optional_args
11851222
11861223 @property
@@ -1251,6 +1288,7 @@ def _connect_root_parser(
12511288 arg_prefix = self .env_prefix ,
12521289 subcommand_prefix = self .env_prefix ,
12531290 group = None ,
1291+ alias_prefixes = [],
12541292 )
12551293
12561294 def _add_parser_args (
@@ -1261,18 +1299,20 @@ def _add_parser_args(
12611299 arg_prefix : str ,
12621300 subcommand_prefix : str ,
12631301 group : Any ,
1302+ alias_prefixes : list [str ],
12641303 ) -> ArgumentParser :
12651304 subparsers : Any = None
1266- for field_name , resolved_name , field_info in self ._sort_arg_fields (model ):
1305+ alias_path_args : dict [str , str ] = {}
1306+ for field_name , field_info in self ._sort_arg_fields (model ):
12671307 sub_models : list [type [BaseModel ]] = self ._get_sub_models (model , field_name , field_info )
12681308 if _CliSubCommand in field_info .metadata :
12691309 if subparsers is None :
12701310 subparsers = self ._add_subparsers (
12711311 parser , title = 'subcommands' , dest = f'{ arg_prefix } :subcommand' , required = self .cli_enforce_required
12721312 )
1273- self ._cli_subcommands [f'{ arg_prefix } :subcommand' ] = [f'{ arg_prefix } { resolved_name } ' ]
1313+ self ._cli_subcommands [f'{ arg_prefix } :subcommand' ] = [f'{ arg_prefix } { field_name } ' ]
12741314 else :
1275- self ._cli_subcommands [f'{ arg_prefix } :subcommand' ].append (f'{ arg_prefix } { resolved_name } ' )
1315+ self ._cli_subcommands [f'{ arg_prefix } :subcommand' ].append (f'{ arg_prefix } { field_name } ' )
12761316 if hasattr (subparsers , 'metavar' ):
12771317 metavar = ',' .join (self ._cli_subcommands [f'{ arg_prefix } :subcommand' ])
12781318 subparsers .metavar = f'{{{ metavar } }}'
@@ -1281,23 +1321,25 @@ def _add_parser_args(
12811321 self ._add_parser_args (
12821322 parser = self ._add_parser (
12831323 subparsers ,
1284- resolved_name ,
1324+ field_name ,
12851325 help = field_info .description ,
12861326 formatter_class = self ._formatter_class ,
12871327 description = model .__doc__ ,
12881328 ),
12891329 model = model ,
12901330 added_args = [],
1291- arg_prefix = f'{ arg_prefix } { resolved_name } .' ,
1292- subcommand_prefix = f'{ subcommand_prefix } { resolved_name } .' ,
1331+ arg_prefix = f'{ arg_prefix } { field_name } .' ,
1332+ subcommand_prefix = f'{ subcommand_prefix } { field_name } .' ,
12931333 group = None ,
1334+ alias_prefixes = [],
12941335 )
12951336 else :
1337+ resolved_names , is_alias_path_only = self ._get_resolved_names (field_name , field_info , alias_path_args )
12961338 arg_flag : str = '--'
12971339 kwargs : dict [str , Any ] = {}
12981340 kwargs ['default' ] = SUPPRESS
12991341 kwargs ['help' ] = self ._help_format (field_info )
1300- kwargs ['dest' ] = f'{ arg_prefix } { resolved_name } '
1342+ kwargs ['dest' ] = f'{ arg_prefix } { resolved_names [ 0 ] } '
13011343 kwargs ['metavar' ] = self ._metavar_format (field_info .annotation )
13021344 kwargs ['required' ] = self .cli_enforce_required and field_info .is_required ()
13031345 if kwargs ['dest' ] in added_args :
@@ -1309,51 +1351,126 @@ def _add_parser_args(
13091351 if _annotation_contains_types (field_info .annotation , (dict , Mapping ), is_strip_annotated = True ):
13101352 self ._cli_dict_args [kwargs ['dest' ]] = field_info .annotation
13111353
1312- arg_name = (
1313- f'{ arg_prefix } { resolved_name } '
1314- if subcommand_prefix == self .env_prefix
1315- else f'{ arg_prefix .replace (subcommand_prefix , "" , 1 )} { resolved_name } '
1316- )
1354+ arg_names = self ._get_arg_names (arg_prefix , subcommand_prefix , alias_prefixes , resolved_names )
13171355 if _CliPositionalArg in field_info .metadata :
1318- kwargs ['metavar' ] = resolved_name .upper ()
1319- arg_name = kwargs ['dest' ]
1356+ kwargs ['metavar' ] = resolved_names [ 0 ] .upper ()
1357+ arg_names = [ kwargs ['dest' ] ]
13201358 del kwargs ['dest' ]
13211359 del kwargs ['required' ]
13221360 arg_flag = ''
13231361
13241362 if sub_models and kwargs .get ('action' ) != 'append' :
1325- model_group : Any = None
1326- model_group_kwargs : dict [str , Any ] = {}
1327- model_group_kwargs ['title' ] = f'{ arg_name } options'
1328- model_group_kwargs ['description' ] = (
1329- sub_models [0 ].__doc__
1330- if self .cli_use_class_docs_for_groups and len (sub_models ) == 1
1331- else field_info .description
1363+ self ._add_parser_submodels (
1364+ parser ,
1365+ sub_models ,
1366+ added_args ,
1367+ arg_prefix ,
1368+ subcommand_prefix ,
1369+ arg_flag ,
1370+ arg_names ,
1371+ kwargs ,
1372+ field_info ,
1373+ resolved_names ,
13321374 )
1333- if not self .cli_avoid_json :
1334- added_args .append (arg_name )
1335- kwargs ['help' ] = f'set { arg_name } from JSON string'
1336- model_group = self ._add_argument_group (parser , ** model_group_kwargs )
1337- self ._add_argument (model_group , f'{ arg_flag } { arg_name } ' , ** kwargs )
1338- for model in sub_models :
1339- self ._add_parser_args (
1340- parser = parser ,
1341- model = model ,
1342- added_args = added_args ,
1343- arg_prefix = f'{ arg_prefix } { resolved_name } .' ,
1344- subcommand_prefix = subcommand_prefix ,
1345- group = model_group if model_group else model_group_kwargs ,
1346- )
1375+ elif is_alias_path_only :
1376+ continue
13471377 elif group is not None :
13481378 if isinstance (group , dict ):
13491379 group = self ._add_argument_group (parser , ** group )
1350- added_args . append ( arg_name )
1351- self ._add_argument (group , f'{ arg_flag } { arg_name } ' , ** kwargs )
1380+ added_args += list ( arg_names )
1381+ self ._add_argument (group , * ( f'{ arg_flag } { name } ' for name in arg_names ) , ** kwargs )
13521382 else :
1353- added_args .append (arg_name )
1354- self ._add_argument (parser , f'{ arg_flag } { arg_name } ' , ** kwargs )
1383+ added_args += list (arg_names )
1384+ self ._add_argument (parser , * (f'{ arg_flag } { name } ' for name in arg_names ), ** kwargs )
1385+
1386+ self ._add_parser_alias_paths (parser , alias_path_args , added_args , arg_prefix , subcommand_prefix , group )
13551387 return parser
13561388
1389+ def _get_arg_names (
1390+ self , arg_prefix : str , subcommand_prefix : str , alias_prefixes : list [str ], resolved_names : tuple [str , ...]
1391+ ) -> list [str ]:
1392+ arg_names : list [str ] = []
1393+ for prefix in [arg_prefix ] + alias_prefixes :
1394+ for name in resolved_names :
1395+ arg_names .append (
1396+ f'{ prefix } { name } '
1397+ if subcommand_prefix == self .env_prefix
1398+ else f'{ prefix .replace (subcommand_prefix , "" , 1 )} { name } '
1399+ )
1400+ return arg_names
1401+
1402+ def _add_parser_submodels (
1403+ self ,
1404+ parser : Any ,
1405+ sub_models : list [type [BaseModel ]],
1406+ added_args : list [str ],
1407+ arg_prefix : str ,
1408+ subcommand_prefix : str ,
1409+ arg_flag : str ,
1410+ arg_names : list [str ],
1411+ kwargs : dict [str , Any ],
1412+ field_info : FieldInfo ,
1413+ resolved_names : tuple [str , ...],
1414+ ) -> None :
1415+ model_group : Any = None
1416+ model_group_kwargs : dict [str , Any ] = {}
1417+ model_group_kwargs ['title' ] = f'{ arg_names [0 ]} options'
1418+ model_group_kwargs ['description' ] = (
1419+ sub_models [0 ].__doc__
1420+ if self .cli_use_class_docs_for_groups and len (sub_models ) == 1
1421+ else field_info .description
1422+ )
1423+ if not self .cli_avoid_json :
1424+ added_args .append (arg_names [0 ])
1425+ kwargs ['help' ] = f'set { arg_names [0 ]} from JSON string'
1426+ model_group = self ._add_argument_group (parser , ** model_group_kwargs )
1427+ self ._add_argument (model_group , * (f'{ arg_flag } { name } ' for name in arg_names ), ** kwargs )
1428+ for model in sub_models :
1429+ self ._add_parser_args (
1430+ parser = parser ,
1431+ model = model ,
1432+ added_args = added_args ,
1433+ arg_prefix = f'{ arg_prefix } { resolved_names [0 ]} .' ,
1434+ subcommand_prefix = subcommand_prefix ,
1435+ group = model_group if model_group else model_group_kwargs ,
1436+ alias_prefixes = [f'{ arg_prefix } { name } .' for name in resolved_names [1 :]],
1437+ )
1438+
1439+ def _add_parser_alias_paths (
1440+ self ,
1441+ parser : Any ,
1442+ alias_path_args : dict [str , str ],
1443+ added_args : list [str ],
1444+ arg_prefix : str ,
1445+ subcommand_prefix : str ,
1446+ group : Any ,
1447+ ) -> None :
1448+ if alias_path_args :
1449+ context = parser
1450+ if group is not None :
1451+ context = self ._add_argument_group (parser , ** group ) if isinstance (group , dict ) else group
1452+ is_nested_alias_path = arg_prefix .endswith ('.' )
1453+ arg_prefix = arg_prefix [:- 1 ] if is_nested_alias_path else arg_prefix
1454+ for name , metavar in alias_path_args .items ():
1455+ name = '' if is_nested_alias_path else name
1456+ arg_name = (
1457+ f'{ arg_prefix } { name } '
1458+ if subcommand_prefix == self .env_prefix
1459+ else f'{ arg_prefix .replace (subcommand_prefix , "" , 1 )} { name } '
1460+ )
1461+ kwargs : dict [str , Any ] = {}
1462+ kwargs ['default' ] = SUPPRESS
1463+ kwargs ['help' ] = 'pydantic alias path'
1464+ kwargs ['dest' ] = f'{ arg_prefix } { name } '
1465+ if metavar == 'dict' or is_nested_alias_path :
1466+ kwargs ['metavar' ] = 'dict'
1467+ else :
1468+ kwargs ['action' ] = 'append'
1469+ kwargs ['metavar' ] = 'list'
1470+ if arg_name not in added_args :
1471+ added_args .append (arg_name )
1472+ self ._add_argument (context , f'--{ arg_name } ' , ** kwargs )
1473+
13571474 def _get_modified_args (self , obj : Any ) -> tuple [str , ...]:
13581475 if not self .cli_hide_none_type :
13591476 return get_args (obj )
0 commit comments