@@ -1157,30 +1157,67 @@ def _get_sub_models(self, model: type[BaseModel], field_name: str, field_info: F
1157
1157
sub_models .append (type_ ) # type: ignore
1158
1158
return sub_models
1159
1159
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 ]]:
1161
1196
positional_args , subcommand_args , optional_args = [], [], []
1162
1197
fields = model .__pydantic_fields__ if is_pydantic_dataclass (model ) else model .model_fields
1163
1198
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
1166
1199
if _CliSubCommand in field_info .metadata :
1167
1200
if not field_info .is_required ():
1168
1201
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' )
1169
1204
else :
1170
1205
field_types = [type_ for type_ in get_args (field_info .annotation ) if type_ is not type (None )]
1171
1206
if len (field_types ) != 1 :
1172
1207
raise SettingsError (f'subcommand argument { model .__name__ } .{ field_name } has multiple types' )
1173
1208
elif not is_model_class (field_types [0 ]):
1174
1209
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'
1176
1211
)
1177
- subcommand_args .append ((field_name , resolved_name , field_info ))
1212
+ subcommand_args .append ((field_name , field_info ))
1178
1213
elif _CliPositionalArg in field_info .metadata :
1179
1214
if not field_info .is_required ():
1180
1215
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 ))
1182
1219
else :
1183
- optional_args .append ((field_name , resolved_name , field_info ))
1220
+ optional_args .append ((field_name , field_info ))
1184
1221
return positional_args + subcommand_args + optional_args
1185
1222
1186
1223
@property
@@ -1251,6 +1288,7 @@ def _connect_root_parser(
1251
1288
arg_prefix = self .env_prefix ,
1252
1289
subcommand_prefix = self .env_prefix ,
1253
1290
group = None ,
1291
+ alias_prefixes = [],
1254
1292
)
1255
1293
1256
1294
def _add_parser_args (
@@ -1261,18 +1299,20 @@ def _add_parser_args(
1261
1299
arg_prefix : str ,
1262
1300
subcommand_prefix : str ,
1263
1301
group : Any ,
1302
+ alias_prefixes : list [str ],
1264
1303
) -> ArgumentParser :
1265
1304
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 ):
1267
1307
sub_models : list [type [BaseModel ]] = self ._get_sub_models (model , field_name , field_info )
1268
1308
if _CliSubCommand in field_info .metadata :
1269
1309
if subparsers is None :
1270
1310
subparsers = self ._add_subparsers (
1271
1311
parser , title = 'subcommands' , dest = f'{ arg_prefix } :subcommand' , required = self .cli_enforce_required
1272
1312
)
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 } ' ]
1274
1314
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 } ' )
1276
1316
if hasattr (subparsers , 'metavar' ):
1277
1317
metavar = ',' .join (self ._cli_subcommands [f'{ arg_prefix } :subcommand' ])
1278
1318
subparsers .metavar = f'{{{ metavar } }}'
@@ -1281,23 +1321,25 @@ def _add_parser_args(
1281
1321
self ._add_parser_args (
1282
1322
parser = self ._add_parser (
1283
1323
subparsers ,
1284
- resolved_name ,
1324
+ field_name ,
1285
1325
help = field_info .description ,
1286
1326
formatter_class = self ._formatter_class ,
1287
1327
description = model .__doc__ ,
1288
1328
),
1289
1329
model = model ,
1290
1330
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 } .' ,
1293
1333
group = None ,
1334
+ alias_prefixes = [],
1294
1335
)
1295
1336
else :
1337
+ resolved_names , is_alias_path_only = self ._get_resolved_names (field_name , field_info , alias_path_args )
1296
1338
arg_flag : str = '--'
1297
1339
kwargs : dict [str , Any ] = {}
1298
1340
kwargs ['default' ] = SUPPRESS
1299
1341
kwargs ['help' ] = self ._help_format (field_info )
1300
- kwargs ['dest' ] = f'{ arg_prefix } { resolved_name } '
1342
+ kwargs ['dest' ] = f'{ arg_prefix } { resolved_names [ 0 ] } '
1301
1343
kwargs ['metavar' ] = self ._metavar_format (field_info .annotation )
1302
1344
kwargs ['required' ] = self .cli_enforce_required and field_info .is_required ()
1303
1345
if kwargs ['dest' ] in added_args :
@@ -1309,51 +1351,126 @@ def _add_parser_args(
1309
1351
if _annotation_contains_types (field_info .annotation , (dict , Mapping ), is_strip_annotated = True ):
1310
1352
self ._cli_dict_args [kwargs ['dest' ]] = field_info .annotation
1311
1353
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 )
1317
1355
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' ] ]
1320
1358
del kwargs ['dest' ]
1321
1359
del kwargs ['required' ]
1322
1360
arg_flag = ''
1323
1361
1324
1362
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 ,
1332
1374
)
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
1347
1377
elif group is not None :
1348
1378
if isinstance (group , dict ):
1349
1379
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 )
1352
1382
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 )
1355
1387
return parser
1356
1388
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
+
1357
1474
def _get_modified_args (self , obj : Any ) -> tuple [str , ...]:
1358
1475
if not self .cli_hide_none_type :
1359
1476
return get_args (obj )
0 commit comments