35
35
)
36
36
37
37
import typing_extensions
38
- from pydantic import BaseModel , Field
38
+ from pydantic import AliasChoices , AliasPath , BaseModel , Field
39
39
from pydantic ._internal ._repr import Representation
40
40
from pydantic ._internal ._utils import is_model_class
41
41
from pydantic .dataclasses import is_pydantic_dataclass
@@ -74,6 +74,10 @@ def error(self, message: str) -> NoReturn:
74
74
super ().error (message )
75
75
76
76
77
+ class _CliInternalArgSerializer (_CliInternalArgParser ):
78
+ pass
79
+
80
+
77
81
class CliMutuallyExclusiveGroup (BaseModel ):
78
82
pass
79
83
@@ -664,6 +668,8 @@ def _parse_known_args(*args: Any, **kwargs: Any) -> Namespace:
664
668
self ._formatter_class = formatter_class
665
669
self ._cli_dict_args : dict [str , type [Any ] | None ] = {}
666
670
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 ] = {}
667
673
self ._add_parser_args (
668
674
parser = self .root_parser ,
669
675
model = self .settings_cls ,
@@ -689,6 +695,7 @@ def _add_parser_args(
689
695
) -> ArgumentParser :
690
696
subparsers : Any = None
691
697
alias_path_args : dict [str , str ] = {}
698
+ alias_path_only_defaults : dict [str , Any ] = {}
692
699
# Ignore model default if the default is a model and not a subclass of the current model.
693
700
model_default = (
694
701
None
@@ -756,9 +763,11 @@ def _add_parser_args(
756
763
is_append_action = _annotation_contains_types (
757
764
field_info .annotation , (list , set , dict , Sequence , Mapping ), is_strip_annotated = True
758
765
)
759
- is_parser_submodel = sub_models and not is_append_action
766
+ is_parser_submodel = bool ( sub_models ) and not is_append_action
760
767
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
+ )
762
771
kwargs ['help' ] = self ._help_format (field_name , field_info , model_default , is_model_suppressed )
763
772
kwargs ['metavar' ] = self ._metavar_format (field_info .annotation )
764
773
kwargs ['required' ] = (
@@ -817,8 +826,14 @@ def _add_parser_args(
817
826
self ._add_argument (
818
827
parser , * (f'{ flag_prefix [: len (name )]} { name } ' for name in arg_names ), ** kwargs
819
828
)
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
+ )
820
833
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
+ )
822
837
return parser
823
838
824
839
def _check_kebab_name (self , name : str ) -> str :
@@ -845,8 +860,6 @@ def _convert_positional_arg(
845
860
) -> tuple [list [str ], str ]:
846
861
flag_prefix = ''
847
862
arg_names = [kwargs ['dest' ]]
848
- kwargs ['default' ] = PydanticUndefined
849
- kwargs ['metavar' ] = self ._check_kebab_name (preferred_alias .upper ())
850
863
851
864
# Note: CLI positional args are always strictly required at the CLI. Therefore, use field_info.is_required in
852
865
# conjunction with model_default instead of the derived kwargs['required'].
@@ -857,6 +870,13 @@ def _convert_positional_arg(
857
870
elif not is_required :
858
871
kwargs ['nargs' ] = '?'
859
872
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
+
860
880
del kwargs ['dest' ]
861
881
del kwargs ['required' ]
862
882
return arg_names , flag_prefix
@@ -944,7 +964,7 @@ def _add_parser_submodels(
944
964
is_model_suppressed = self ._is_field_suppressed (field_info ) or is_model_suppressed
945
965
if is_model_suppressed :
946
966
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 :
948
968
added_args .append (arg_names [0 ])
949
969
kwargs ['nargs' ] = '?'
950
970
kwargs ['const' ] = '{}'
@@ -974,6 +994,7 @@ def _add_parser_alias_paths(
974
994
arg_prefix : str ,
975
995
subcommand_prefix : str ,
976
996
group : Any ,
997
+ alias_path_only_defaults : dict [str , Any ],
977
998
) -> None :
978
999
if alias_path_args :
979
1000
context = parser
@@ -989,9 +1010,9 @@ def _add_parser_alias_paths(
989
1010
else f'{ arg_prefix .replace (subcommand_prefix , "" , 1 )} { name } '
990
1011
)
991
1012
kwargs : dict [str , Any ] = {}
992
- kwargs ['default' ] = CLI_SUPPRESS
993
1013
kwargs ['help' ] = 'pydantic alias path'
994
1014
kwargs ['dest' ] = f'{ arg_prefix } { name } '
1015
+ kwargs ['default' ] = alias_path_only_defaults .get (kwargs ['dest' ], CLI_SUPPRESS )
995
1016
if metavar == 'dict' or is_nested_alias_path :
996
1017
kwargs ['metavar' ] = 'dict'
997
1018
else :
@@ -1084,3 +1105,60 @@ def _help_format(
1084
1105
def _is_field_suppressed (self , field_info : FieldInfo ) -> bool :
1085
1106
_help = field_info .description if field_info .description else ''
1086
1107
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