3535)
3636
3737import typing_extensions
38- from pydantic import BaseModel , Field
38+ from pydantic import AliasChoices , AliasPath , BaseModel , Field
3939from pydantic ._internal ._repr import Representation
4040from pydantic ._internal ._utils import is_model_class
4141from pydantic .dataclasses import is_pydantic_dataclass
@@ -74,6 +74,10 @@ def error(self, message: str) -> NoReturn:
7474 super ().error (message )
7575
7676
77+ class _CliInternalArgSerializer (_CliInternalArgParser ):
78+ pass
79+
80+
7781class CliMutuallyExclusiveGroup (BaseModel ):
7882 pass
7983
@@ -664,6 +668,8 @@ def _parse_known_args(*args: Any, **kwargs: Any) -> Namespace:
664668 self ._formatter_class = formatter_class
665669 self ._cli_dict_args : dict [str , type [Any ] | None ] = {}
666670 self ._cli_subcommands : defaultdict [str , dict [str , str ]] = defaultdict (dict )
671+ self ._is_serialize_args = isinstance (root_parser , _CliInternalArgSerializer )
672+ self ._serialize_positional_args : dict [str , Any ] = {}
667673 self ._add_parser_args (
668674 parser = self .root_parser ,
669675 model = self .settings_cls ,
@@ -689,6 +695,7 @@ def _add_parser_args(
689695 ) -> ArgumentParser :
690696 subparsers : Any = None
691697 alias_path_args : dict [str , str ] = {}
698+ alias_path_only_defaults : dict [str , Any ] = {}
692699 # Ignore model default if the default is a model and not a subclass of the current model.
693700 model_default = (
694701 None
@@ -756,9 +763,11 @@ def _add_parser_args(
756763 is_append_action = _annotation_contains_types (
757764 field_info .annotation , (list , set , dict , Sequence , Mapping ), is_strip_annotated = True
758765 )
759- is_parser_submodel = sub_models and not is_append_action
766+ is_parser_submodel = bool ( sub_models ) and not is_append_action
760767 kwargs : dict [str , Any ] = {}
761- kwargs ['default' ] = CLI_SUPPRESS
768+ kwargs ['default' ] = self ._get_cli_default_value (
769+ field_name , field_info , model_default , is_parser_submodel
770+ )
762771 kwargs ['help' ] = self ._help_format (field_name , field_info , model_default , is_model_suppressed )
763772 kwargs ['metavar' ] = self ._metavar_format (field_info .annotation )
764773 kwargs ['required' ] = (
@@ -817,8 +826,14 @@ def _add_parser_args(
817826 self ._add_argument (
818827 parser , * (f'{ flag_prefix [: len (name )]} { name } ' for name in arg_names ), ** kwargs
819828 )
829+ elif kwargs ['default' ] != CLI_SUPPRESS :
830+ self ._update_alias_path_only_defaults (
831+ kwargs ['dest' ], kwargs ['default' ], field_info , alias_path_only_defaults
832+ )
820833
821- self ._add_parser_alias_paths (parser , alias_path_args , added_args , arg_prefix , subcommand_prefix , group )
834+ self ._add_parser_alias_paths (
835+ parser , alias_path_args , added_args , arg_prefix , subcommand_prefix , group , alias_path_only_defaults
836+ )
822837 return parser
823838
824839 def _check_kebab_name (self , name : str ) -> str :
@@ -845,8 +860,6 @@ def _convert_positional_arg(
845860 ) -> tuple [list [str ], str ]:
846861 flag_prefix = ''
847862 arg_names = [kwargs ['dest' ]]
848- kwargs ['default' ] = PydanticUndefined
849- kwargs ['metavar' ] = self ._check_kebab_name (preferred_alias .upper ())
850863
851864 # Note: CLI positional args are always strictly required at the CLI. Therefore, use field_info.is_required in
852865 # conjunction with model_default instead of the derived kwargs['required'].
@@ -857,6 +870,13 @@ def _convert_positional_arg(
857870 elif not is_required :
858871 kwargs ['nargs' ] = '?'
859872
873+ if self ._is_serialize_args :
874+ self ._serialize_positional_args [kwargs ['dest' ]] = kwargs ['default' ]
875+ kwargs ['nargs' ] = '*'
876+
877+ kwargs ['default' ] = PydanticUndefined
878+ kwargs ['metavar' ] = self ._check_kebab_name (preferred_alias .upper ())
879+
860880 del kwargs ['dest' ]
861881 del kwargs ['required' ]
862882 return arg_names , flag_prefix
@@ -944,7 +964,7 @@ def _add_parser_submodels(
944964 is_model_suppressed = self ._is_field_suppressed (field_info ) or is_model_suppressed
945965 if is_model_suppressed :
946966 model_group_kwargs ['description' ] = CLI_SUPPRESS
947- if not self .cli_avoid_json :
967+ if not self .cli_avoid_json and not self . _is_serialize_args :
948968 added_args .append (arg_names [0 ])
949969 kwargs ['nargs' ] = '?'
950970 kwargs ['const' ] = '{}'
@@ -974,6 +994,7 @@ def _add_parser_alias_paths(
974994 arg_prefix : str ,
975995 subcommand_prefix : str ,
976996 group : Any ,
997+ alias_path_only_defaults : dict [str , Any ],
977998 ) -> None :
978999 if alias_path_args :
9791000 context = parser
@@ -989,9 +1010,9 @@ def _add_parser_alias_paths(
9891010 else f'{ arg_prefix .replace (subcommand_prefix , "" , 1 )} { name } '
9901011 )
9911012 kwargs : dict [str , Any ] = {}
992- kwargs ['default' ] = CLI_SUPPRESS
9931013 kwargs ['help' ] = 'pydantic alias path'
9941014 kwargs ['dest' ] = f'{ arg_prefix } { name } '
1015+ kwargs ['default' ] = alias_path_only_defaults .get (kwargs ['dest' ], CLI_SUPPRESS )
9951016 if metavar == 'dict' or is_nested_alias_path :
9961017 kwargs ['metavar' ] = 'dict'
9971018 else :
@@ -1084,3 +1105,60 @@ def _help_format(
10841105 def _is_field_suppressed (self , field_info : FieldInfo ) -> bool :
10851106 _help = field_info .description if field_info .description else ''
10861107 return _help == CLI_SUPPRESS or CLI_SUPPRESS in field_info .metadata
1108+
1109+ def _get_cli_default_value (
1110+ self , field_name : str , field_info : FieldInfo , model_default : Any , is_parser_submodel : bool
1111+ ) -> Any :
1112+ if is_parser_submodel or not isinstance (self .root_parser , _CliInternalArgSerializer ):
1113+ return CLI_SUPPRESS
1114+
1115+ return getattr (model_default , field_name , field_info .default )
1116+
1117+ def _update_alias_path_only_defaults (
1118+ self , dest : str , default : Any , field_info : FieldInfo , alias_path_only_defaults : dict [str , Any ]
1119+ ) -> None :
1120+ alias_path : AliasPath = [
1121+ alias if isinstance (alias , AliasPath ) else cast (AliasPath , alias .choices [0 ])
1122+ for alias in (field_info .alias , field_info .validation_alias )
1123+ if isinstance (alias , (AliasPath , AliasChoices ))
1124+ ][0 ]
1125+
1126+ alias_nested_paths : list [str ] = alias_path .path [1 :- 1 ] # type: ignore
1127+ if '.' in dest :
1128+ alias_nested_paths = dest .split ('.' ) + alias_nested_paths
1129+ dest = alias_nested_paths .pop (0 )
1130+
1131+ if not alias_nested_paths :
1132+ alias_path_only_defaults .setdefault (dest , [])
1133+ alias_default = alias_path_only_defaults [dest ]
1134+ else :
1135+ alias_path_only_defaults .setdefault (dest , {})
1136+ current_path = alias_path_only_defaults [dest ]
1137+
1138+ for nested_path in alias_nested_paths [:- 1 ]:
1139+ current_path .setdefault (nested_path , {})
1140+ current_path = current_path [nested_path ]
1141+ current_path .setdefault (alias_nested_paths [- 1 ], [])
1142+ alias_default = current_path [alias_nested_paths [- 1 ]]
1143+
1144+ alias_path_index = cast (int , alias_path .path [- 1 ])
1145+ alias_default .extend (['' ] * max (alias_path_index + 1 - len (alias_default ), 0 ))
1146+ alias_default [alias_path_index ] = default
1147+
1148+ def _serialized_args (self ) -> list [str ]:
1149+ if not self ._is_serialize_args :
1150+ raise SettingsError ('Root parser is not _CliInternalArgSerializer' )
1151+
1152+ cli_args = []
1153+ for arg , values in self ._serialize_positional_args .items ():
1154+ for value in values if isinstance (values , list ) else [values ]:
1155+ value = json .dumps (value ) if isinstance (value , (dict , list , set )) else str (value )
1156+ cli_args .append (value )
1157+
1158+ for arg , value in self .env_vars .items ():
1159+ if arg not in self ._serialize_positional_args :
1160+ value = json .dumps (value ) if isinstance (value , (dict , list , set )) else str (value )
1161+ cli_args .append (f'{ self .cli_flag_prefix_char * min (len (arg ), 2 )} { arg } ' )
1162+ cli_args .append (value )
1163+
1164+ return cli_args
0 commit comments