@@ -149,6 +149,10 @@ def error(self, message: str) -> NoReturn:
149
149
super ().error (message )
150
150
151
151
152
+ class CliMutuallyExclusiveGroup (BaseModel ):
153
+ pass
154
+
155
+
152
156
T = TypeVar ('T' )
153
157
CliSubCommand = Annotated [Union [T , None ], _CliSubCommand ]
154
158
CliPositionalArg = Annotated [T , _CliPositionalArg ]
@@ -1483,7 +1487,7 @@ def _connect_parser_method(
1483
1487
if (
1484
1488
parser_method is not None
1485
1489
and self .case_sensitive is False
1486
- and method_name == 'parsed_args_method '
1490
+ and method_name == 'parse_args_method '
1487
1491
and isinstance (self ._root_parser , _CliInternalArgParser )
1488
1492
):
1489
1493
@@ -1515,6 +1519,26 @@ def none_parser_method(*args: Any, **kwargs: Any) -> Any:
1515
1519
else :
1516
1520
return parser_method
1517
1521
1522
+ def _connect_group_method (self , add_argument_group_method : Callable [..., Any ] | None ) -> Callable [..., Any ]:
1523
+ add_argument_group = self ._connect_parser_method (add_argument_group_method , 'add_argument_group_method' )
1524
+
1525
+ def add_group_method (parser : Any , ** kwargs : Any ) -> Any :
1526
+ if not kwargs .pop ('_is_cli_mutually_exclusive_group' ):
1527
+ kwargs .pop ('required' )
1528
+ return add_argument_group (parser , ** kwargs )
1529
+ else :
1530
+ main_group_kwargs = {arg : kwargs .pop (arg ) for arg in ['title' , 'description' ] if arg in kwargs }
1531
+ main_group_kwargs ['title' ] += ' (mutually exclusive)'
1532
+ group = add_argument_group (parser , ** main_group_kwargs )
1533
+ if not hasattr (group , 'add_mutually_exclusive_group' ):
1534
+ raise SettingsError (
1535
+ 'cannot connect CLI settings source root parser: '
1536
+ 'group object is missing add_mutually_exclusive_group but is needed for connecting'
1537
+ )
1538
+ return group .add_mutually_exclusive_group (** kwargs )
1539
+
1540
+ return add_group_method
1541
+
1518
1542
def _connect_root_parser (
1519
1543
self ,
1520
1544
root_parser : T ,
@@ -1531,9 +1555,9 @@ def _parse_known_args(*args: Any, **kwargs: Any) -> Namespace:
1531
1555
self ._root_parser = root_parser
1532
1556
if parse_args_method is None :
1533
1557
parse_args_method = _parse_known_args if self .cli_ignore_unknown_args else ArgumentParser .parse_args
1534
- self ._parse_args = self ._connect_parser_method (parse_args_method , 'parsed_args_method ' )
1558
+ self ._parse_args = self ._connect_parser_method (parse_args_method , 'parse_args_method ' )
1535
1559
self ._add_argument = self ._connect_parser_method (add_argument_method , 'add_argument_method' )
1536
- self ._add_argument_group = self ._connect_parser_method (add_argument_group_method , 'add_argument_group_method' )
1560
+ self ._add_group = self ._connect_group_method (add_argument_group_method )
1537
1561
self ._add_parser = self ._connect_parser_method (add_parser_method , 'add_parser_method' )
1538
1562
self ._add_subparsers = self ._connect_parser_method (add_subparsers_method , 'add_subparsers_method' )
1539
1563
self ._formatter_class = formatter_class
@@ -1665,6 +1689,7 @@ def _add_parser_args(
1665
1689
if is_parser_submodel :
1666
1690
self ._add_parser_submodels (
1667
1691
parser ,
1692
+ model ,
1668
1693
sub_models ,
1669
1694
added_args ,
1670
1695
arg_prefix ,
@@ -1680,7 +1705,7 @@ def _add_parser_args(
1680
1705
elif not is_alias_path_only :
1681
1706
if group is not None :
1682
1707
if isinstance (group , dict ):
1683
- group = self ._add_argument_group (parser , ** group )
1708
+ group = self ._add_group (parser , ** group )
1684
1709
added_args += list (arg_names )
1685
1710
self ._add_argument (group , * (f'{ flag_prefix [:len (name )]} { name } ' for name in arg_names ), ** kwargs )
1686
1711
else :
@@ -1724,6 +1749,7 @@ def _get_arg_names(
1724
1749
def _add_parser_submodels (
1725
1750
self ,
1726
1751
parser : Any ,
1752
+ model : type [BaseModel ],
1727
1753
sub_models : list [type [BaseModel ]],
1728
1754
added_args : list [str ],
1729
1755
arg_prefix : str ,
@@ -1736,10 +1762,23 @@ def _add_parser_submodels(
1736
1762
alias_names : tuple [str , ...],
1737
1763
model_default : Any ,
1738
1764
) -> None :
1765
+ if issubclass (model , CliMutuallyExclusiveGroup ):
1766
+ # Argparse has deprecated "calling add_argument_group() or add_mutually_exclusive_group() on a
1767
+ # mutually exclusive group" (https://docs.python.org/3/library/argparse.html#mutual-exclusion).
1768
+ # Since nested models result in a group add, raise an exception for nested models in a mutually
1769
+ # exclusive group.
1770
+ raise SettingsError ('cannot have nested models in a CliMutuallyExclusiveGroup' )
1771
+
1739
1772
model_group : Any = None
1740
1773
model_group_kwargs : dict [str , Any ] = {}
1741
1774
model_group_kwargs ['title' ] = f'{ arg_names [0 ]} options'
1742
1775
model_group_kwargs ['description' ] = field_info .description
1776
+ model_group_kwargs ['required' ] = kwargs ['required' ]
1777
+ model_group_kwargs ['_is_cli_mutually_exclusive_group' ] = any (
1778
+ issubclass (model , CliMutuallyExclusiveGroup ) for model in sub_models
1779
+ )
1780
+ if model_group_kwargs ['_is_cli_mutually_exclusive_group' ] and len (sub_models ) > 1 :
1781
+ raise SettingsError ('cannot use union with CliMutuallyExclusiveGroup' )
1743
1782
if self .cli_use_class_docs_for_groups and len (sub_models ) == 1 :
1744
1783
model_group_kwargs ['description' ] = None if sub_models [0 ].__doc__ is None else dedent (sub_models [0 ].__doc__ )
1745
1784
@@ -1762,7 +1801,7 @@ def _add_parser_submodels(
1762
1801
if not self .cli_avoid_json :
1763
1802
added_args .append (arg_names [0 ])
1764
1803
kwargs ['help' ] = f'set { arg_names [0 ]} from JSON string'
1765
- model_group = self ._add_argument_group (parser , ** model_group_kwargs )
1804
+ model_group = self ._add_group (parser , ** model_group_kwargs )
1766
1805
self ._add_argument (model_group , * (f'{ flag_prefix } { name } ' for name in arg_names ), ** kwargs )
1767
1806
for model in sub_models :
1768
1807
self ._add_parser_args (
@@ -1788,7 +1827,7 @@ def _add_parser_alias_paths(
1788
1827
if alias_path_args :
1789
1828
context = parser
1790
1829
if group is not None :
1791
- context = self ._add_argument_group (parser , ** group ) if isinstance (group , dict ) else group
1830
+ context = self ._add_group (parser , ** group ) if isinstance (group , dict ) else group
1792
1831
is_nested_alias_path = arg_prefix .endswith ('.' )
1793
1832
arg_prefix = arg_prefix [:- 1 ] if is_nested_alias_path else arg_prefix
1794
1833
for name , metavar in alias_path_args .items ():
0 commit comments