@@ -1333,7 +1333,11 @@ def _load_env_vars(
1333
1333
if subcommand_dest not in selected_subcommands :
1334
1334
parsed_args [subcommand_dest ] = self .cli_parse_none_str
1335
1335
1336
- parsed_args = {key : val for key , val in parsed_args .items () if not key .endswith (':subcommand' )}
1336
+ parsed_args = {
1337
+ key : val
1338
+ for key , val in parsed_args .items ()
1339
+ if not key .endswith (':subcommand' ) and val is not PydanticUndefined
1340
+ }
1337
1341
if selected_subcommands :
1338
1342
last_selected_subcommand = max (selected_subcommands , key = len )
1339
1343
if not any (field_name for field_name in parsed_args .keys () if f'{ last_selected_subcommand } .' in field_name ):
@@ -1494,6 +1498,7 @@ def _verify_cli_flag_annotations(self, model: type[BaseModel], field_name: str,
1494
1498
)
1495
1499
1496
1500
def _sort_arg_fields (self , model : type [BaseModel ]) -> list [tuple [str , FieldInfo ]]:
1501
+ positional_variadic_arg = []
1497
1502
positional_args , subcommand_args , optional_args = [], [], []
1498
1503
for field_name , field_info in _get_model_fields (model ).items ():
1499
1504
if _CliSubCommand in field_info .metadata :
@@ -1511,17 +1516,31 @@ def _sort_arg_fields(self, model: type[BaseModel]) -> list[tuple[str, FieldInfo]
1511
1516
)
1512
1517
subcommand_args .append ((field_name , field_info ))
1513
1518
elif _CliPositionalArg in field_info .metadata :
1514
- if not field_info .is_required ():
1515
- raise SettingsError (f'positional argument { model .__name__ } .{ field_name } has a default value' )
1519
+ alias_names , * _ = _get_alias_names (field_name , field_info )
1520
+ if len (alias_names ) > 1 :
1521
+ raise SettingsError (f'positional argument { model .__name__ } .{ field_name } has multiple aliases' )
1522
+ is_append_action = _annotation_contains_types (
1523
+ field_info .annotation , (list , set , dict , Sequence , Mapping ), is_strip_annotated = True
1524
+ )
1525
+ if not is_append_action :
1526
+ positional_args .append ((field_name , field_info ))
1516
1527
else :
1517
- alias_names , * _ = _get_alias_names (field_name , field_info )
1518
- if len (alias_names ) > 1 :
1519
- raise SettingsError (f'positional argument { model .__name__ } .{ field_name } has multiple aliases' )
1520
- positional_args .append ((field_name , field_info ))
1528
+ positional_variadic_arg .append ((field_name , field_info ))
1521
1529
else :
1522
1530
self ._verify_cli_flag_annotations (model , field_name , field_info )
1523
1531
optional_args .append ((field_name , field_info ))
1524
- return positional_args + subcommand_args + optional_args
1532
+
1533
+ if positional_variadic_arg :
1534
+ if len (positional_variadic_arg ) > 1 :
1535
+ field_names = ', ' .join ([name for name , info in positional_variadic_arg ])
1536
+ raise SettingsError (f'{ model .__name__ } has multiple variadic positonal arguments: { field_names } ' )
1537
+ elif subcommand_args :
1538
+ field_names = ', ' .join ([name for name , info in positional_variadic_arg + subcommand_args ])
1539
+ raise SettingsError (
1540
+ f'{ model .__name__ } has variadic positonal arguments and subcommand arguments: { field_names } '
1541
+ )
1542
+
1543
+ return positional_args + positional_variadic_arg + subcommand_args + optional_args
1525
1544
1526
1545
@property
1527
1546
def root_parser (self ) -> T :
@@ -1727,11 +1746,9 @@ def _add_parser_args(
1727
1746
self ._cli_dict_args [kwargs ['dest' ]] = field_info .annotation
1728
1747
1729
1748
if _CliPositionalArg in field_info .metadata :
1730
- kwargs ['metavar' ] = self ._check_kebab_name (preferred_alias .upper ())
1731
- arg_names = [kwargs ['dest' ]]
1732
- del kwargs ['dest' ]
1733
- del kwargs ['required' ]
1734
- flag_prefix = ''
1749
+ arg_names , flag_prefix = self ._convert_positional_arg (
1750
+ kwargs , field_info , preferred_alias , model_default
1751
+ )
1735
1752
1736
1753
self ._convert_bool_flag (kwargs , field_info , model_default )
1737
1754
@@ -1787,6 +1804,27 @@ def _convert_bool_flag(self, kwargs: dict[str, Any], field_info: FieldInfo, mode
1787
1804
BooleanOptionalAction if sys .version_info >= (3 , 9 ) else f'store_{ str (not default ).lower ()} '
1788
1805
)
1789
1806
1807
+ def _convert_positional_arg (
1808
+ self , kwargs : dict [str , Any ], field_info : FieldInfo , preferred_alias : str , model_default : Any
1809
+ ) -> tuple [list [str ], str ]:
1810
+ flag_prefix = ''
1811
+ arg_names = [kwargs ['dest' ]]
1812
+ kwargs ['default' ] = PydanticUndefined
1813
+ kwargs ['metavar' ] = self ._check_kebab_name (preferred_alias .upper ())
1814
+
1815
+ # Note: CLI positional args are always strictly required at the CLI. Therefore, use field_info.is_required in
1816
+ # conjunction with model_default instead of the derived kwargs['required'].
1817
+ is_required = field_info .is_required () and model_default is PydanticUndefined
1818
+ if kwargs .get ('action' ) == 'append' :
1819
+ del kwargs ['action' ]
1820
+ kwargs ['nargs' ] = '+' if is_required else '*'
1821
+ elif not is_required :
1822
+ kwargs ['nargs' ] = '?'
1823
+
1824
+ del kwargs ['dest' ]
1825
+ del kwargs ['required' ]
1826
+ return arg_names , flag_prefix
1827
+
1790
1828
def _get_arg_names (
1791
1829
self ,
1792
1830
arg_prefix : str ,
0 commit comments