13
13
if sys .version_info >= (3 , 9 ):
14
14
from argparse import BooleanOptionalAction
15
15
from argparse import SUPPRESS , ArgumentParser , Namespace , RawDescriptionHelpFormatter , _SubParsersAction
16
- from collections import deque
16
+ from collections import defaultdict , deque
17
17
from dataclasses import asdict , is_dataclass
18
18
from enum import Enum
19
19
from pathlib import Path
@@ -1239,12 +1239,14 @@ def _load_env_vars(
1239
1239
if isinstance (val , list ):
1240
1240
parsed_args [field_name ] = self ._merge_parsed_list (val , field_name )
1241
1241
elif field_name .endswith (':subcommand' ) and val is not None :
1242
- selected_subcommands .append (field_name .split (':' )[0 ] + val )
1242
+ subcommand_name = field_name .split (':' )[0 ] + val
1243
+ subcommand_dest = self ._cli_subcommands [field_name ][subcommand_name ]
1244
+ selected_subcommands .append (subcommand_dest )
1243
1245
1244
1246
for subcommands in self ._cli_subcommands .values ():
1245
- for subcommand in subcommands :
1246
- if subcommand not in selected_subcommands :
1247
- parsed_args [subcommand ] = self .cli_parse_none_str
1247
+ for subcommand_dest in subcommands . values () :
1248
+ if subcommand_dest not in selected_subcommands :
1249
+ parsed_args [subcommand_dest ] = self .cli_parse_none_str
1248
1250
1249
1251
parsed_args = {key : val for key , val in parsed_args .items () if not key .endswith (':subcommand' )}
1250
1252
if selected_subcommands :
@@ -1389,26 +1391,26 @@ def _get_sub_models(self, model: type[BaseModel], field_name: str, field_info: F
1389
1391
sub_models .append (type_ ) # type: ignore
1390
1392
return sub_models
1391
1393
1392
- def _get_resolved_names (
1394
+ def _get_alias_names (
1393
1395
self , field_name : str , field_info : FieldInfo , alias_path_args : dict [str , str ]
1394
1396
) -> tuple [tuple [str , ...], bool ]:
1395
- resolved_names : list [str ] = []
1397
+ alias_names : list [str ] = []
1396
1398
is_alias_path_only : bool = True
1397
1399
if not any ((field_info .alias , field_info .validation_alias )):
1398
- resolved_names += [field_name ]
1400
+ alias_names += [field_name ]
1399
1401
is_alias_path_only = False
1400
1402
else :
1401
1403
new_alias_paths : list [AliasPath ] = []
1402
1404
for alias in (field_info .alias , field_info .validation_alias ):
1403
1405
if alias is None :
1404
1406
continue
1405
1407
elif isinstance (alias , str ):
1406
- resolved_names .append (alias )
1408
+ alias_names .append (alias )
1407
1409
is_alias_path_only = False
1408
1410
elif isinstance (alias , AliasChoices ):
1409
1411
for name in alias .choices :
1410
1412
if isinstance (name , str ):
1411
- resolved_names .append (name )
1413
+ alias_names .append (name )
1412
1414
is_alias_path_only = False
1413
1415
else :
1414
1416
new_alias_paths .append (name )
@@ -1418,11 +1420,11 @@ def _get_resolved_names(
1418
1420
name = cast (str , alias_path .path [0 ])
1419
1421
name = name .lower () if not self .case_sensitive else name
1420
1422
alias_path_args [name ] = 'dict' if len (alias_path .path ) > 2 else 'list'
1421
- if not resolved_names and is_alias_path_only :
1422
- resolved_names .append (name )
1423
+ if not alias_names and is_alias_path_only :
1424
+ alias_names .append (name )
1423
1425
if not self .case_sensitive :
1424
- resolved_names = [resolved_name .lower () for resolved_name in resolved_names ]
1425
- return tuple (dict .fromkeys (resolved_names )), is_alias_path_only
1426
+ alias_names = [alias_name .lower () for alias_name in alias_names ]
1427
+ return tuple (dict .fromkeys (alias_names )), is_alias_path_only
1426
1428
1427
1429
def _verify_cli_flag_annotations (self , model : type [BaseModel ], field_name : str , field_info : FieldInfo ) -> None :
1428
1430
if _CliImplicitFlag in field_info .metadata :
@@ -1447,22 +1449,24 @@ def _sort_arg_fields(self, model: type[BaseModel]) -> list[tuple[str, FieldInfo]
1447
1449
if _CliSubCommand in field_info .metadata :
1448
1450
if not field_info .is_required ():
1449
1451
raise SettingsError (f'subcommand argument { model .__name__ } .{ field_name } has a default value' )
1450
- elif any ((field_info .alias , field_info .validation_alias )):
1451
- raise SettingsError (f'subcommand argument { model .__name__ } .{ field_name } has an alias' )
1452
1452
else :
1453
+ alias_names , * _ = self ._get_alias_names (field_name , field_info , {})
1454
+ if len (alias_names ) > 1 :
1455
+ raise SettingsError (f'subcommand argument { model .__name__ } .{ field_name } has multiple aliases' )
1453
1456
field_types = [type_ for type_ in get_args (field_info .annotation ) if type_ is not type (None )]
1454
- if len (field_types ) != 1 :
1455
- raise SettingsError (f'subcommand argument { model .__name__ } .{ field_name } has multiple types' )
1456
- elif not (is_model_class (field_types [0 ]) or is_pydantic_dataclass (field_types [0 ])):
1457
- raise SettingsError (
1458
- f'subcommand argument { model .__name__ } .{ field_name } is not derived from BaseModel'
1459
- )
1457
+ for field_type in field_types :
1458
+ if not (is_model_class (field_type ) or is_pydantic_dataclass (field_type )):
1459
+ raise SettingsError (
1460
+ f'subcommand argument { model .__name__ } .{ field_name } has type not derived from BaseModel'
1461
+ )
1460
1462
subcommand_args .append ((field_name , field_info ))
1461
1463
elif _CliPositionalArg in field_info .metadata :
1462
1464
if not field_info .is_required ():
1463
1465
raise SettingsError (f'positional argument { model .__name__ } .{ field_name } has a default value' )
1464
- elif any ((field_info .alias , field_info .validation_alias )):
1465
- raise SettingsError (f'positional argument { model .__name__ } .{ field_name } has an alias' )
1466
+ else :
1467
+ alias_names , * _ = self ._get_alias_names (field_name , field_info , {})
1468
+ if len (alias_names ) > 1 :
1469
+ raise SettingsError (f'positional argument { model .__name__ } .{ field_name } has multiple aliases' )
1466
1470
positional_args .append ((field_name , field_info ))
1467
1471
else :
1468
1472
self ._verify_cli_flag_annotations (model , field_name , field_info )
@@ -1529,7 +1533,7 @@ def _connect_root_parser(
1529
1533
self ._add_subparsers = self ._connect_parser_method (add_subparsers_method , 'add_subparsers_method' )
1530
1534
self ._formatter_class = formatter_class
1531
1535
self ._cli_dict_args : dict [str , type [Any ] | None ] = {}
1532
- self ._cli_subcommands : dict [str , list [str ]] = {}
1536
+ self ._cli_subcommands : defaultdict [str , dict [str , str ]] = defaultdict ( dict )
1533
1537
self ._add_parser_args (
1534
1538
parser = self .root_parser ,
1535
1539
model = self .settings_cls ,
@@ -1556,64 +1560,93 @@ def _add_parser_args(
1556
1560
alias_path_args : dict [str , str ] = {}
1557
1561
for field_name , field_info in self ._sort_arg_fields (model ):
1558
1562
sub_models : list [type [BaseModel ]] = self ._get_sub_models (model , field_name , field_info )
1563
+ alias_names , is_alias_path_only = self ._get_alias_names (field_name , field_info , alias_path_args )
1564
+ preferred_alias = alias_names [0 ]
1559
1565
if _CliSubCommand in field_info .metadata :
1560
- if subparsers is None :
1561
- subparsers = self ._add_subparsers (parser , title = 'subcommands' , dest = f'{ arg_prefix } :subcommand' )
1562
- self ._cli_subcommands [f'{ arg_prefix } :subcommand' ] = [f'{ arg_prefix } { field_name } ' ]
1563
- else :
1564
- self ._cli_subcommands [f'{ arg_prefix } :subcommand' ].append (f'{ arg_prefix } { field_name } ' )
1565
- if hasattr (subparsers , 'metavar' ):
1566
- metavar = ',' .join (self ._cli_subcommands [f'{ arg_prefix } :subcommand' ])
1567
- subparsers .metavar = f'{{{ metavar } }}'
1568
-
1569
- model = sub_models [0 ]
1570
- self ._add_parser_args (
1571
- parser = self ._add_parser (
1572
- subparsers ,
1573
- field_name ,
1574
- help = field_info .description ,
1575
- formatter_class = self ._formatter_class ,
1576
- description = None if model .__doc__ is None else dedent (model .__doc__ ),
1577
- ),
1578
- model = model ,
1579
- added_args = [],
1580
- arg_prefix = f'{ arg_prefix } { field_name } .' ,
1581
- subcommand_prefix = f'{ subcommand_prefix } { field_name } .' ,
1582
- group = None ,
1583
- alias_prefixes = [],
1584
- model_default = PydanticUndefined ,
1585
- )
1566
+ for model in sub_models :
1567
+ subcommand_alias = model .__name__ if len (sub_models ) > 1 else preferred_alias
1568
+ subcommand_name = f'{ arg_prefix } { subcommand_alias } '
1569
+ subcommand_dest = f'{ arg_prefix } { preferred_alias } '
1570
+ self ._cli_subcommands [f'{ arg_prefix } :subcommand' ][subcommand_name ] = subcommand_dest
1571
+
1572
+ subcommand_help = None if len (sub_models ) > 1 else field_info .description
1573
+ if self .cli_use_class_docs_for_groups :
1574
+ subcommand_help = None if model .__doc__ is None else dedent (model .__doc__ )
1575
+
1576
+ subparsers = (
1577
+ self ._add_subparsers (
1578
+ parser ,
1579
+ title = 'subcommands' ,
1580
+ dest = f'{ arg_prefix } :subcommand' ,
1581
+ description = field_info .description if len (sub_models ) > 1 else None ,
1582
+ )
1583
+ if subparsers is None
1584
+ else subparsers
1585
+ )
1586
+
1587
+ if hasattr (subparsers , 'metavar' ):
1588
+ subparsers .metavar = (
1589
+ f'{ subparsers .metavar [:- 1 ]} ,{ subcommand_alias } }}'
1590
+ if subparsers .metavar
1591
+ else f'{{{ subcommand_alias } }}'
1592
+ )
1593
+
1594
+ self ._add_parser_args (
1595
+ parser = self ._add_parser (
1596
+ subparsers ,
1597
+ subcommand_alias ,
1598
+ help = subcommand_help ,
1599
+ formatter_class = self ._formatter_class ,
1600
+ description = None if model .__doc__ is None else dedent (model .__doc__ ),
1601
+ ),
1602
+ model = model ,
1603
+ added_args = [],
1604
+ arg_prefix = f'{ arg_prefix } { preferred_alias } .' ,
1605
+ subcommand_prefix = f'{ subcommand_prefix } { preferred_alias } .' ,
1606
+ group = None ,
1607
+ alias_prefixes = [],
1608
+ model_default = PydanticUndefined ,
1609
+ )
1586
1610
else :
1587
- resolved_names , is_alias_path_only = self ._get_resolved_names (field_name , field_info , alias_path_args )
1588
1611
arg_flag : str = '--'
1612
+ is_append_action = _annotation_contains_types (
1613
+ field_info .annotation , (list , set , dict , Sequence , Mapping ), is_strip_annotated = True
1614
+ )
1615
+ is_parser_submodel = sub_models and not is_append_action
1589
1616
kwargs : dict [str , Any ] = {}
1590
1617
kwargs ['default' ] = SUPPRESS
1591
1618
kwargs ['help' ] = self ._help_format (field_name , field_info , model_default )
1592
- kwargs ['dest' ] = f'{ arg_prefix } { resolved_names [0 ]} '
1593
1619
kwargs ['metavar' ] = self ._metavar_format (field_info .annotation )
1594
1620
kwargs ['required' ] = (
1595
1621
self .cli_enforce_required and field_info .is_required () and model_default is PydanticUndefined
1596
1622
)
1623
+ kwargs ['dest' ] = (
1624
+ # Strip prefix if validation alias is set and value is not complex.
1625
+ # Related https://github.com/pydantic/pydantic-settings/pull/25
1626
+ f'{ arg_prefix } { preferred_alias } ' [self .env_prefix_len :]
1627
+ if arg_prefix and field_info .validation_alias is not None and not is_parser_submodel
1628
+ else f'{ arg_prefix } { preferred_alias } '
1629
+ )
1630
+
1597
1631
if kwargs ['dest' ] in added_args :
1598
1632
continue
1599
- if _annotation_contains_types (
1600
- field_info .annotation , (list , set , dict , Sequence , Mapping ), is_strip_annotated = True
1601
- ):
1633
+
1634
+ if is_append_action :
1602
1635
kwargs ['action' ] = 'append'
1603
1636
if _annotation_contains_types (field_info .annotation , (dict , Mapping ), is_strip_annotated = True ):
1604
1637
self ._cli_dict_args [kwargs ['dest' ]] = field_info .annotation
1605
1638
1606
- arg_names = self ._get_arg_names (arg_prefix , subcommand_prefix , alias_prefixes , resolved_names )
1639
+ arg_names = self ._get_arg_names (arg_prefix , subcommand_prefix , alias_prefixes , alias_names )
1607
1640
if _CliPositionalArg in field_info .metadata :
1608
- kwargs ['metavar' ] = resolved_names [ 0 ] .upper ()
1641
+ kwargs ['metavar' ] = preferred_alias .upper ()
1609
1642
arg_names = [kwargs ['dest' ]]
1610
1643
del kwargs ['dest' ]
1611
1644
del kwargs ['required' ]
1612
1645
arg_flag = ''
1613
1646
1614
1647
self ._convert_bool_flag (kwargs , field_info , model_default )
1615
1648
1616
- if sub_models and kwargs . get ( 'action' ) != 'append' :
1649
+ if is_parser_submodel :
1617
1650
self ._add_parser_submodels (
1618
1651
parser ,
1619
1652
sub_models ,
@@ -1625,14 +1658,10 @@ def _add_parser_args(
1625
1658
kwargs ,
1626
1659
field_name ,
1627
1660
field_info ,
1628
- resolved_names ,
1661
+ alias_names ,
1629
1662
model_default = model_default ,
1630
1663
)
1631
1664
elif not is_alias_path_only :
1632
- if arg_prefix and field_info .validation_alias is not None :
1633
- # Strip prefix if validation alias is set and value is not complex.
1634
- # Related https://github.com/pydantic/pydantic-settings/pull/25
1635
- kwargs ['dest' ] = kwargs ['dest' ][self .env_prefix_len :]
1636
1665
if group is not None :
1637
1666
if isinstance (group , dict ):
1638
1667
group = self ._add_argument_group (parser , ** group )
@@ -1662,11 +1691,11 @@ def _convert_bool_flag(self, kwargs: dict[str, Any], field_info: FieldInfo, mode
1662
1691
)
1663
1692
1664
1693
def _get_arg_names (
1665
- self , arg_prefix : str , subcommand_prefix : str , alias_prefixes : list [str ], resolved_names : tuple [str , ...]
1694
+ self , arg_prefix : str , subcommand_prefix : str , alias_prefixes : list [str ], alias_names : tuple [str , ...]
1666
1695
) -> list [str ]:
1667
1696
arg_names : list [str ] = []
1668
1697
for prefix in [arg_prefix ] + alias_prefixes :
1669
- for name in resolved_names :
1698
+ for name in alias_names :
1670
1699
arg_names .append (
1671
1700
f'{ prefix } { name } '
1672
1701
if subcommand_prefix == self .env_prefix
@@ -1686,7 +1715,7 @@ def _add_parser_submodels(
1686
1715
kwargs : dict [str , Any ],
1687
1716
field_name : str ,
1688
1717
field_info : FieldInfo ,
1689
- resolved_names : tuple [str , ...],
1718
+ alias_names : tuple [str , ...],
1690
1719
model_default : Any ,
1691
1720
) -> None :
1692
1721
model_group : Any = None
@@ -1711,6 +1740,7 @@ def _add_parser_submodels(
1711
1740
else :
1712
1741
model_group_kwargs ['description' ] = desc_header
1713
1742
1743
+ preferred_alias = alias_names [0 ]
1714
1744
if not self .cli_avoid_json :
1715
1745
added_args .append (arg_names [0 ])
1716
1746
kwargs ['help' ] = f'set { arg_names [0 ]} from JSON string'
@@ -1721,10 +1751,10 @@ def _add_parser_submodels(
1721
1751
parser = parser ,
1722
1752
model = model ,
1723
1753
added_args = added_args ,
1724
- arg_prefix = f'{ arg_prefix } { resolved_names [ 0 ] } .' ,
1754
+ arg_prefix = f'{ arg_prefix } { preferred_alias } .' ,
1725
1755
subcommand_prefix = subcommand_prefix ,
1726
1756
group = model_group if model_group else model_group_kwargs ,
1727
- alias_prefixes = [f'{ arg_prefix } { name } .' for name in resolved_names [1 :]],
1757
+ alias_prefixes = [f'{ arg_prefix } { name } .' for name in alias_names [1 :]],
1728
1758
model_default = model_default ,
1729
1759
)
1730
1760
0 commit comments