3535)
3636
3737import  typing_extensions 
38- from  pydantic  import  AliasChoices , AliasPath , BaseModel , Field 
38+ from  pydantic  import  AliasChoices , AliasPath , BaseModel , Field ,  create_model 
3939from  pydantic ._internal ._repr  import  Representation 
4040from  pydantic ._internal ._utils  import  is_model_class 
4141from  pydantic .dataclasses  import  is_pydantic_dataclass 
4747
4848from  ...exceptions  import  SettingsError 
4949from  ...utils  import  _lenient_issubclass , _WithArgsTypes 
50- from  ..types  import  NoDecode , _CliExplicitFlag , _CliImplicitFlag , _CliPositionalArg , _CliSubCommand , _CliUnknownArgs 
50+ from  ..types  import  (
51+     NoDecode ,
52+     PydanticModel ,
53+     _CliExplicitFlag ,
54+     _CliImplicitFlag ,
55+     _CliPositionalArg ,
56+     _CliSubCommand ,
57+     _CliUnknownArgs ,
58+ )
5159from  ..utils  import  (
5260    _annotation_contains_types ,
5361    _annotation_enum_val_to_name ,
@@ -74,10 +82,6 @@ def error(self, message: str) -> NoReturn:
7482        super ().error (message )
7583
7684
77- class  _CliInternalArgSerializer (_CliInternalArgParser ):
78-     pass 
79- 
80- 
8185class  CliMutuallyExclusiveGroup (BaseModel ):
8286    pass 
8387
@@ -666,8 +670,6 @@ def _parse_known_args(*args: Any, **kwargs: Any) -> Namespace:
666670        self ._formatter_class  =  formatter_class 
667671        self ._cli_dict_args : dict [str , type [Any ] |  None ] =  {}
668672        self ._cli_subcommands : defaultdict [str , dict [str , str ]] =  defaultdict (dict )
669-         self ._is_serialize_args  =  isinstance (root_parser , _CliInternalArgSerializer )
670-         self ._serialize_positional_args : dict [str , Any ] =  {}
671673        self ._add_parser_args (
672674            parser = self .root_parser ,
673675            model = self .settings_cls ,
@@ -693,7 +695,6 @@ def _add_parser_args(
693695    ) ->  ArgumentParser :
694696        subparsers : Any  =  None 
695697        alias_path_args : dict [str , str ] =  {}
696-         alias_path_only_defaults : dict [str , Any ] =  {}
697698        # Ignore model default if the default is a model and not a subclass of the current model. 
698699        model_default  =  (
699700            None 
@@ -762,11 +763,9 @@ def _add_parser_args(
762763                is_append_action  =  _annotation_contains_types (
763764                    field_info .annotation , (list , set , dict , Sequence , Mapping ), is_strip_annotated = True 
764765                )
765-                 is_parser_submodel  =  bool ( sub_models )  and  not  is_append_action 
766+                 is_parser_submodel  =  sub_models  and  not  is_append_action 
766767                kwargs : dict [str , Any ] =  {}
767-                 kwargs ['default' ] =  self ._get_cli_default_value (
768-                     field_name , field_info , model_default , is_parser_submodel 
769-                 )
768+                 kwargs ['default' ] =  CLI_SUPPRESS 
770769                kwargs ['help' ] =  self ._help_format (field_name , field_info , model_default , is_model_suppressed )
771770                kwargs ['metavar' ] =  self ._metavar_format (field_info .annotation )
772771                kwargs ['required' ] =  (
@@ -825,14 +824,8 @@ def _add_parser_args(
825824                        self ._add_argument (
826825                            parser , * (f'{ flag_prefix [: len (name )]} { name }  '  for  name  in  arg_names ), ** kwargs 
827826                        )
828-                 elif  kwargs ['default' ] !=  CLI_SUPPRESS :
829-                     self ._update_alias_path_only_defaults (
830-                         kwargs ['dest' ], kwargs ['default' ], field_info , alias_path_only_defaults 
831-                     )
832827
833-         self ._add_parser_alias_paths (
834-             parser , alias_path_args , added_args , arg_prefix , subcommand_prefix , group , alias_path_only_defaults 
835-         )
828+         self ._add_parser_alias_paths (parser , alias_path_args , added_args , arg_prefix , subcommand_prefix , group )
836829        return  parser 
837830
838831    def  _check_kebab_name (self , name : str ) ->  str :
@@ -859,6 +852,8 @@ def _convert_positional_arg(
859852    ) ->  tuple [list [str ], str ]:
860853        flag_prefix  =  '' 
861854        arg_names  =  [kwargs ['dest' ]]
855+         kwargs ['default' ] =  PydanticUndefined 
856+         kwargs ['metavar' ] =  self ._check_kebab_name (preferred_alias .upper ())
862857
863858        # Note: CLI positional args are always strictly required at the CLI. Therefore, use field_info.is_required in 
864859        # conjunction with model_default instead of the derived kwargs['required']. 
@@ -869,13 +864,6 @@ def _convert_positional_arg(
869864        elif  not  is_required :
870865            kwargs ['nargs' ] =  '?' 
871866
872-         if  self ._is_serialize_args :
873-             self ._serialize_positional_args [kwargs ['dest' ]] =  kwargs ['default' ]
874-             kwargs ['nargs' ] =  '*' 
875- 
876-         kwargs ['default' ] =  PydanticUndefined 
877-         kwargs ['metavar' ] =  self ._check_kebab_name (preferred_alias .upper ())
878- 
879867        del  kwargs ['dest' ]
880868        del  kwargs ['required' ]
881869        return  arg_names , flag_prefix 
@@ -963,7 +951,7 @@ def _add_parser_submodels(
963951        is_model_suppressed  =  self ._is_field_suppressed (field_info ) or  is_model_suppressed 
964952        if  is_model_suppressed :
965953            model_group_kwargs ['description' ] =  CLI_SUPPRESS 
966-         if  not  self .cli_avoid_json   and   not   self . _is_serialize_args :
954+         if  not  self .cli_avoid_json :
967955            added_args .append (arg_names [0 ])
968956            kwargs ['nargs' ] =  '?' 
969957            kwargs ['const' ] =  '{}' 
@@ -993,7 +981,6 @@ def _add_parser_alias_paths(
993981        arg_prefix : str ,
994982        subcommand_prefix : str ,
995983        group : Any ,
996-         alias_path_only_defaults : dict [str , Any ],
997984    ) ->  None :
998985        if  alias_path_args :
999986            context  =  parser 
@@ -1009,9 +996,9 @@ def _add_parser_alias_paths(
1009996                    else  f'{ arg_prefix .replace (subcommand_prefix , "" , 1 )} { name }  ' 
1010997                )
1011998                kwargs : dict [str , Any ] =  {}
999+                 kwargs ['default' ] =  CLI_SUPPRESS 
10121000                kwargs ['help' ] =  'pydantic alias path' 
10131001                kwargs ['dest' ] =  f'{ arg_prefix } { name }  ' 
1014-                 kwargs ['default' ] =  alias_path_only_defaults .get (kwargs ['dest' ], CLI_SUPPRESS )
10151002                if  metavar  ==  'dict'  or  is_nested_alias_path :
10161003                    kwargs ['metavar' ] =  'dict' 
10171004                else :
@@ -1105,34 +1092,27 @@ def _is_field_suppressed(self, field_info: FieldInfo) -> bool:
11051092        _help  =  field_info .description  if  field_info .description  else  '' 
11061093        return  _help  ==  CLI_SUPPRESS  or  CLI_SUPPRESS  in  field_info .metadata 
11071094
1108-     def  _get_cli_default_value (
1109-         self , field_name : str , field_info : FieldInfo , model_default : Any , is_parser_submodel : bool 
1110-     ) ->  Any :
1111-         if  is_parser_submodel  or  not  isinstance (self .root_parser , _CliInternalArgSerializer ):
1112-             return  CLI_SUPPRESS 
1113- 
1114-         return  getattr (model_default , field_name , field_info .default )
1115- 
1116-     def  _update_alias_path_only_defaults (
1117-         self , dest : str , default : Any , field_info : FieldInfo , alias_path_only_defaults : dict [str , Any ]
1118-     ) ->  None :
1095+     @classmethod  
1096+     def  _update_alias_path_only_default (
1097+         cls , arg_name : str , value : Any , field_info : FieldInfo , alias_path_only_defaults : dict [str , Any ]
1098+     ) ->  tuple [str , list [Any ] |  dict [str , Any ]]:
11191099        alias_path : AliasPath  =  [
11201100            alias  if  isinstance (alias , AliasPath ) else  cast (AliasPath , alias .choices [0 ])
11211101            for  alias  in  (field_info .alias , field_info .validation_alias )
11221102            if  isinstance (alias , (AliasPath , AliasChoices ))
11231103        ][0 ]
11241104
11251105        alias_nested_paths : list [str ] =  alias_path .path [1 :- 1 ]  # type: ignore 
1126-         if  '.'  in  dest :
1127-             alias_nested_paths  =  dest .split ('.' ) +  alias_nested_paths 
1128-             dest  =  alias_nested_paths .pop (0 )
1106+         if  '.'  in  arg_name :
1107+             alias_nested_paths  =  arg_name .split ('.' ) +  alias_nested_paths 
1108+             arg_name  =  alias_nested_paths .pop (0 )
11291109
11301110        if  not  alias_nested_paths :
1131-             alias_path_only_defaults .setdefault (dest , [])
1132-             alias_default  =  alias_path_only_defaults [dest ]
1111+             alias_path_only_defaults .setdefault (arg_name , [])
1112+             alias_default  =  alias_path_only_defaults [arg_name ]
11331113        else :
1134-             alias_path_only_defaults .setdefault (dest , {})
1135-             current_path  =  alias_path_only_defaults [dest ]
1114+             alias_path_only_defaults .setdefault (arg_name , {})
1115+             current_path  =  alias_path_only_defaults [arg_name ]
11361116
11371117            for  nested_path  in  alias_nested_paths [:- 1 ]:
11381118                current_path .setdefault (nested_path , {})
@@ -1142,22 +1122,84 @@ def _update_alias_path_only_defaults(
11421122
11431123        alias_path_index  =  cast (int , alias_path .path [- 1 ])
11441124        alias_default .extend (['' ] *  max (alias_path_index  +  1  -  len (alias_default ), 0 ))
1145-         alias_default [alias_path_index ] =  default 
1125+         alias_default [alias_path_index ] =  value 
1126+         return  arg_name , alias_path_only_defaults [arg_name ]
1127+ 
1128+     @classmethod  
1129+     def  _serialized_args (cls , model : PydanticModel , model_config : Any , prefix : str  =  '' ) ->  list [str ]:
1130+         model_field_definitions : dict [str , Any ] =  {}
1131+         for  field_name , field_info  in  _get_model_fields (type (model )).items ():
1132+             model_default  =  getattr (model , field_name )
1133+             if  field_info .default  ==  model_default :
1134+                 continue 
1135+             if  _CliSubCommand  in  field_info .metadata  and  model_default  is  None :
1136+                 continue 
1137+             model_field_definitions [field_name ] =  (field_info .annotation , field_info )
1138+         cli_serialize_cls  =  create_model ('CliSerialize' , __config__ = model_config , ** model_field_definitions )
1139+ 
1140+         added_args : set [str ] =  set ()
1141+         alias_path_args : dict [str , str ] =  {}
1142+         alias_path_only_defaults : dict [str , Any ] =  {}
1143+         optional_args : list [str  |  list [Any ] |  dict [str , Any ]] =  []
1144+         positional_args : list [str  |  list [Any ] |  dict [str , Any ]] =  []
1145+         subcommand_args : list [str ] =  []
1146+         cli_settings  =  CliSettingsSource [Any ](cli_serialize_cls )
1147+         for  field_name , field_info  in  _get_model_fields (cli_serialize_cls ).items ():
1148+             model_default  =  getattr (model , field_name )
1149+             alias_names , is_alias_path_only  =  _get_alias_names (
1150+                 field_name , field_info , alias_path_args = alias_path_args , case_sensitive = cli_settings .case_sensitive 
1151+             )
1152+             preferred_alias  =  alias_names [0 ]
1153+             if  _CliSubCommand  in  field_info .metadata :
1154+                 subcommand_args .append (cls ._check_kebab_name (cli_settings , preferred_alias ))
1155+                 subcommand_args  +=  cls ._serialized_args (model_default , model_config )
1156+                 continue 
1157+             if  is_model_class (type (model_default )) or  is_pydantic_dataclass (type (model_default )):
1158+                 positional_args  +=  cls ._serialized_args (
1159+                     model_default , model_config , prefix = f'{ prefix } { preferred_alias }  .' 
1160+                 )
1161+                 continue 
1162+ 
1163+             arg_name  =  f'{ prefix } { cls ._check_kebab_name (cli_settings , preferred_alias )}  ' 
1164+             value : str  |  list [Any ] |  dict [str , Any ] =  (
1165+                 json .dumps (model_default ) if  isinstance (model_default , (dict , list , set )) else  str (model_default )
1166+             )
1167+ 
1168+             if  is_alias_path_only :
1169+                 # For alias path only, we wont know the complete value until we've finished parsing the entire class. In 
1170+                 # this case, insert value as a non-string reference pointing to the relevant alias_path_only_defaults 
1171+                 # entry and convert into completed string value later. 
1172+                 arg_name , value  =  cls ._update_alias_path_only_default (
1173+                     arg_name , value , field_info , alias_path_only_defaults 
1174+                 )
1175+ 
1176+             if  arg_name  in  added_args :
1177+                 continue 
1178+             added_args .add (arg_name )
1179+ 
1180+             if  _CliPositionalArg  in  field_info .metadata :
1181+                 if  is_alias_path_only :
1182+                     positional_args .append (value )
1183+                     continue 
1184+                 for  value  in  model_default  if  isinstance (model_default , list ) else  [model_default ]:
1185+                     value  =  json .dumps (value ) if  isinstance (value , (dict , list , set )) else  str (value )
1186+                     positional_args .append (value )
1187+                 continue 
11461188
1147-     def  _serialized_args (self ) ->  list [str ]:
1148-         if  not  self ._is_serialize_args :
1149-             raise  SettingsError ('Root parser is not _CliInternalArgSerializer' )
1189+             flag_chars  =  f'{ cli_settings .cli_flag_prefix_char  *  min (len (arg_name ), 2 )}  ' 
1190+             kwargs  =  {'metavar' : cls ._metavar_format (cli_settings , field_info .annotation )}
1191+             cls ._convert_bool_flag (cli_settings , kwargs , field_info , model_default )
1192+             # Note: cls._convert_bool_flag will add action to kwargs if value is implicit bool flag 
1193+             if  'action'  in  kwargs  and  model_default  is  False :
1194+                 flag_chars  +=  'no-' 
11501195
1151-         cli_args  =  []
1152-         for  arg , values  in  self ._serialize_positional_args .items ():
1153-             for  value  in  values  if  isinstance (values , list ) else  [values ]:
1154-                 value  =  json .dumps (value ) if  isinstance (value , (dict , list , set )) else  str (value )
1155-                 cli_args .append (value )
1196+             optional_args .append (f'{ flag_chars } { arg_name }  ' )
11561197
1157-         for  arg , value  in  self .env_vars .items ():
1158-             if  arg  not  in   self ._serialize_positional_args :
1159-                 value  =  json .dumps (value ) if  isinstance (value , (dict , list , set )) else  str (value )
1160-                 cli_args .append (f'{ self .cli_flag_prefix_char  *  min (len (arg ), 2 )} { arg }  ' )
1161-                 cli_args .append (value )
1198+             # If implicit bool flag, do not add a value 
1199+             if  'action'  not  in   kwargs :
1200+                 optional_args .append (value )
11621201
1163-         return  cli_args 
1202+         serialized_args : list [str ] =  []
1203+         serialized_args  +=  [json .dumps (value ) if  not  isinstance (value , str ) else  value  for  value  in  optional_args ]
1204+         serialized_args  +=  [json .dumps (value ) if  not  isinstance (value , str ) else  value  for  value  in  positional_args ]
1205+         return  serialized_args  +  subcommand_args 
0 commit comments