@@ -1333,7 +1333,11 @@ def _load_env_vars(
13331333 if subcommand_dest not in selected_subcommands :
13341334 parsed_args [subcommand_dest ] = self .cli_parse_none_str
13351335
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+ }
13371341 if selected_subcommands :
13381342 last_selected_subcommand = max (selected_subcommands , key = len )
13391343 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,
14941498 )
14951499
14961500 def _sort_arg_fields (self , model : type [BaseModel ]) -> list [tuple [str , FieldInfo ]]:
1501+ positional_variadic_arg = []
14971502 positional_args , subcommand_args , optional_args = [], [], []
14981503 for field_name , field_info in _get_model_fields (model ).items ():
14991504 if _CliSubCommand in field_info .metadata :
@@ -1511,17 +1516,31 @@ def _sort_arg_fields(self, model: type[BaseModel]) -> list[tuple[str, FieldInfo]
15111516 )
15121517 subcommand_args .append ((field_name , field_info ))
15131518 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 ))
15161527 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 ))
15211529 else :
15221530 self ._verify_cli_flag_annotations (model , field_name , field_info )
15231531 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
15251544
15261545 @property
15271546 def root_parser (self ) -> T :
@@ -1727,11 +1746,9 @@ def _add_parser_args(
17271746 self ._cli_dict_args [kwargs ['dest' ]] = field_info .annotation
17281747
17291748 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+ )
17351752
17361753 self ._convert_bool_flag (kwargs , field_info , model_default )
17371754
@@ -1787,6 +1804,27 @@ def _convert_bool_flag(self, kwargs: dict[str, Any], field_info: FieldInfo, mode
17871804 BooleanOptionalAction if sys .version_info >= (3 , 9 ) else f'store_{ str (not default ).lower ()} '
17881805 )
17891806
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+
17901828 def _get_arg_names (
17911829 self ,
17921830 arg_prefix : str ,
0 commit comments