@@ -1498,6 +1498,7 @@ def _verify_cli_flag_annotations(self, model: type[BaseModel], field_name: str,
14981498 )
14991499
15001500 def _sort_arg_fields (self , model : type [BaseModel ]) -> list [tuple [str , FieldInfo ]]:
1501+ positional_variadic_arg = []
15011502 positional_args , subcommand_args , optional_args = [], [], []
15021503 for field_name , field_info in _get_model_fields (model ).items ():
15031504 if _CliSubCommand in field_info .metadata :
@@ -1518,11 +1519,28 @@ def _sort_arg_fields(self, model: type[BaseModel]) -> list[tuple[str, FieldInfo]
15181519 alias_names , * _ = _get_alias_names (field_name , field_info )
15191520 if len (alias_names ) > 1 :
15201521 raise SettingsError (f'positional argument { model .__name__ } .{ field_name } has multiple aliases' )
1521- positional_args .append ((field_name , field_info ))
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 ))
1527+ else :
1528+ positional_variadic_arg .append ((field_name , field_info ))
15221529 else :
15231530 self ._verify_cli_flag_annotations (model , field_name , field_info )
15241531 optional_args .append ((field_name , field_info ))
1525- 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
15261544
15271545 @property
15281546 def root_parser (self ) -> T :
@@ -1728,7 +1746,9 @@ def _add_parser_args(
17281746 self ._cli_dict_args [kwargs ['dest' ]] = field_info .annotation
17291747
17301748 if _CliPositionalArg in field_info .metadata :
1731- arg_names , flag_prefix = self ._convert_positional_arg (kwargs , field_info , preferred_alias )
1749+ arg_names , flag_prefix = self ._convert_positional_arg (
1750+ kwargs , field_info , preferred_alias , model_default
1751+ )
17321752
17331753 self ._convert_bool_flag (kwargs , field_info , model_default )
17341754
@@ -1785,16 +1805,20 @@ def _convert_bool_flag(self, kwargs: dict[str, Any], field_info: FieldInfo, mode
17851805 )
17861806
17871807 def _convert_positional_arg (
1788- self , kwargs : dict [str , Any ], field_info : FieldInfo , preferred_alias : str
1808+ self , kwargs : dict [str , Any ], field_info : FieldInfo , preferred_alias : str , model_default : Any
17891809 ) -> tuple [list [str ], str ]:
17901810 flag_prefix = ''
17911811 arg_names = [kwargs ['dest' ]]
17921812 kwargs ['default' ] = PydanticUndefined
17931813 kwargs ['metavar' ] = self ._check_kebab_name (preferred_alias .upper ())
17941814
1795- # Note: For positional args, we must strictly look at field_info.is_required instead of our derived
1796- # kwargs['required'].
1797- if not field_info .is_required ():
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 :
17981822 kwargs ['nargs' ] = '?'
17991823
18001824 del kwargs ['dest' ]
0 commit comments