@@ -825,6 +825,62 @@ class NestedModel(pydantic.BaseModel):
825
825
class PydanticNestedDict (pydantic .BaseModel ):
826
826
nested : Optional [Dict [str , NestedModel ]] = None
827
827
828
+ # Helper function to create test models dynamically based on current pydantic version
829
+ def _create_extra_models ():
830
+ """Create pydantic models with extra field handling based on the current pydantic version."""
831
+ # Check if the actual pydantic module supports v2 syntax
832
+ # In pydantic v1, ConfigDict is just dict, in v2 it's a special class
833
+ # In pydantic.v1 compatibility mode, ConfigDict exists but is in pydantic.v1.config module
834
+ _pydantic_v2_syntax = (
835
+ hasattr (pydantic , "ConfigDict" )
836
+ and hasattr (pydantic .ConfigDict , "__module__" )
837
+ and "pydantic.config" in pydantic .ConfigDict .__module__
838
+ and "v1" not in pydantic .ConfigDict .__module__
839
+ )
840
+
841
+ if _pydantic_v2_syntax :
842
+ from pydantic import ConfigDict
843
+
844
+ class PydanticExtraAllow (pydantic .BaseModel ):
845
+ model_config = ConfigDict (extra = "allow" )
846
+ name : str
847
+ age : int = 25
848
+
849
+ class PydanticExtraForbid (pydantic .BaseModel ):
850
+ model_config = ConfigDict (extra = "forbid" )
851
+ name : str
852
+ age : int = 25
853
+
854
+ class PydanticExtraIgnore (pydantic .BaseModel ):
855
+ model_config = ConfigDict (extra = "ignore" )
856
+ name : str
857
+ age : int = 25
858
+
859
+ else :
860
+ # Pydantic v1 style (including v1 compatibility mode)
861
+ class PydanticExtraAllow (pydantic .BaseModel ):
862
+ name : str
863
+ age : int = 25
864
+
865
+ class Config :
866
+ extra = "allow"
867
+
868
+ class PydanticExtraForbid (pydantic .BaseModel ):
869
+ name : str
870
+ age : int = 25
871
+
872
+ class Config :
873
+ extra = "forbid"
874
+
875
+ class PydanticExtraIgnore (pydantic .BaseModel ):
876
+ name : str
877
+ age : int = 25
878
+
879
+ class Config :
880
+ extra = "ignore"
881
+
882
+ return PydanticExtraAllow , PydanticExtraForbid , PydanticExtraIgnore
883
+
828
884
829
885
def none (x ):
830
886
return x
@@ -989,6 +1045,171 @@ def test_nested_dict(self, parser):
989
1045
assert isinstance (init .model , PydanticNestedDict )
990
1046
assert isinstance (init .model .nested ["key" ], NestedModel )
991
1047
1048
+ def test_extra_allow (self , parser ):
1049
+ """Test that extra='allow' accepts and includes extra fields."""
1050
+ PydanticExtraAllow , _ , _ = _create_extra_models ()
1051
+ parser .add_argument ("--model" , type = PydanticExtraAllow , default = PydanticExtraAllow (name = "default" ))
1052
+
1053
+ # Test with parse_object (where validation logic applies)
1054
+ cfg = parser .parse_object ({"model" : {"name" : "John" , "age" : 30 , "extra_field" : "extra_value" }})
1055
+
1056
+ # Check that extra field is in the namespace
1057
+ assert cfg .model .name == "John"
1058
+ assert cfg .model .age == 30
1059
+ assert cfg .model .extra_field == "extra_value"
1060
+
1061
+ # Check that instantiation includes the extra field
1062
+ init = parser .instantiate_classes (cfg )
1063
+ assert isinstance (init .model , PydanticExtraAllow )
1064
+ assert init .model .name == "John"
1065
+ assert init .model .age == 30
1066
+ assert hasattr (init .model , "extra_field" )
1067
+ assert init .model .extra_field == "extra_value"
1068
+
1069
+ def test_extra_forbid (self , parser ):
1070
+ """Test that extra='forbid' rejects extra fields with appropriate error."""
1071
+ _ , PydanticExtraForbid , _ = _create_extra_models ()
1072
+ parser .add_argument ("--model" , type = PydanticExtraForbid , default = PydanticExtraForbid (name = "default" ))
1073
+
1074
+ # Test with parse_object (where validation logic applies)
1075
+ with pytest .raises (ArgumentError ) as ctx :
1076
+ parser .parse_object ({"model" : {"name" : "John" , "age" : 30 , "extra_field" : "extra_value" }})
1077
+ assert "does not accept nested key 'extra_field'" in str (ctx .value )
1078
+
1079
+ def test_extra_ignore (self , parser ):
1080
+ """Test that extra='ignore' accepts but ignores extra fields."""
1081
+ _ , _ , PydanticExtraIgnore = _create_extra_models ()
1082
+ parser .add_argument ("--model" , type = PydanticExtraIgnore , default = PydanticExtraIgnore (name = "default" ))
1083
+
1084
+ # Test with parse_object (where validation logic applies)
1085
+ cfg = parser .parse_object ({"model" : {"name" : "John" , "age" : 30 , "extra_field" : "extra_value" }})
1086
+
1087
+ # Check that extra field is in the namespace (parsing succeeded)
1088
+ assert cfg .model .name == "John"
1089
+ assert cfg .model .age == 30
1090
+ assert cfg .model .extra_field == "extra_value"
1091
+
1092
+ # Check that instantiation ignores the extra field
1093
+ init = parser .instantiate_classes (cfg )
1094
+ assert isinstance (init .model , PydanticExtraIgnore )
1095
+ assert init .model .name == "John"
1096
+ assert init .model .age == 30
1097
+ assert not hasattr (init .model , "extra_field" )
1098
+
1099
+ def test_extra_default_behavior (self , parser ):
1100
+ """Test that models without explicit extra config behave according to their Pydantic version defaults."""
1101
+ parser .add_argument ("--model" , type = PydanticModel , default = PydanticModel (p1 = "default" ))
1102
+
1103
+ from jsonargparse ._optionals import is_pydantic_model
1104
+
1105
+ model_version = is_pydantic_model (PydanticModel )
1106
+
1107
+ if model_version == 1 :
1108
+ # Pydantic v1 models (including v1 compatibility mode) default to 'ignore'
1109
+ cfg = parser .parse_object ({"model" : {"p1" : "test" , "p2" : 5 , "extra_field" : "extra_value" }})
1110
+ assert cfg .model .p1 == "test"
1111
+ assert cfg .model .p2 == 5
1112
+ assert cfg .model .extra_field == "extra_value"
1113
+
1114
+ # Check that instantiation ignores the extra field (Pydantic v1 default behavior)
1115
+ init = parser .instantiate_classes (cfg )
1116
+ assert isinstance (init .model , PydanticModel )
1117
+ assert init .model .p1 == "test"
1118
+ assert init .model .p2 == 5
1119
+ assert not hasattr (init .model , "extra_field" )
1120
+ else :
1121
+ # Pydantic v2 models default to 'forbid'
1122
+ with pytest .raises (ArgumentError ) as ctx :
1123
+ parser .parse_object ({"model" : {"p1" : "test" , "p2" : 5 , "extra_field" : "extra_value" }})
1124
+ assert "does not accept nested key 'extra_field'" in str (ctx .value )
1125
+
1126
+ def test_extra_with_class_arguments (self , parser ):
1127
+ """Test extra field handling when using add_class_arguments."""
1128
+ PydanticExtraAllow , _ , _ = _create_extra_models ()
1129
+ parser .add_class_arguments (PydanticExtraAllow , "model" )
1130
+
1131
+ # Test with parse_object to include extra field
1132
+ cfg = parser .parse_object ({"model" : {"name" : "John" , "age" : 30 , "extra_field" : "extra_value" }})
1133
+
1134
+ assert cfg .model .name == "John"
1135
+ assert cfg .model .age == 30
1136
+ assert cfg .model .extra_field == "extra_value"
1137
+
1138
+ # Test instantiation
1139
+ init = parser .instantiate_classes (cfg )
1140
+ assert isinstance (init .model , PydanticExtraAllow )
1141
+ assert init .model .extra_field == "extra_value"
1142
+
1143
+ def test_extra_config_function_coverage (self , parser ):
1144
+ """Test edge cases in get_pydantic_extra_config function for coverage."""
1145
+ from jsonargparse ._optionals import get_pydantic_extra_config
1146
+
1147
+ # Test with non-pydantic class
1148
+ class NonPydanticClass :
1149
+ pass
1150
+
1151
+ assert get_pydantic_extra_config (NonPydanticClass ) is None
1152
+
1153
+ # Test with pydantic model that has no extra config
1154
+ class PydanticNoExtra (pydantic .BaseModel ):
1155
+ name : str
1156
+
1157
+ result = get_pydantic_extra_config (PydanticNoExtra )
1158
+ # In pydantic v1, models without explicit extra config default to 'ignore'
1159
+ # In pydantic v2, they default to 'forbid' (but our function returns None for default)
1160
+ from jsonargparse ._optionals import is_pydantic_model
1161
+
1162
+ model_version = is_pydantic_model (PydanticNoExtra )
1163
+ if model_version == 1 :
1164
+ # Pydantic v1 has a default extra='ignore' behavior
1165
+ assert result in [None , "ignore" ] # Allow both since it depends on implementation details
1166
+ else :
1167
+ # Pydantic v2 models without explicit extra config
1168
+ assert result is None
1169
+
1170
+ # Test with a model that has __config__ but no extra
1171
+ class PydanticConfigNoExtra (pydantic .BaseModel ):
1172
+ name : str
1173
+
1174
+ class Config :
1175
+ validate_assignment = True
1176
+
1177
+ result = get_pydantic_extra_config (PydanticConfigNoExtra )
1178
+ # This should return None since no extra is specified
1179
+ if model_version == 1 :
1180
+ assert result in [None , "ignore" ] # v1 might have default behavior
1181
+ else :
1182
+ assert result is None
1183
+
1184
+ # Test with pydantic v1 enum if available
1185
+ try :
1186
+ # Import pydantic v1 directly to avoid regex replacement issues
1187
+ from pydantic import v1 as pydantic_v1
1188
+
1189
+ class PydanticV1ExtraEnum (pydantic_v1 .BaseModel ):
1190
+ name : str
1191
+
1192
+ class Config :
1193
+ extra = pydantic_v1 .Extra .allow
1194
+
1195
+ result = get_pydantic_extra_config (PydanticV1ExtraEnum )
1196
+ assert result == "allow"
1197
+
1198
+ except (ImportError , AttributeError ):
1199
+ # pydantic v1 not available, skip this test
1200
+ pass
1201
+
1202
+ # Test with a class that might cause an exception (edge case)
1203
+ class ProblematicClass :
1204
+ """A class that might cause issues in the function."""
1205
+
1206
+ def __init__ (self ):
1207
+ pass
1208
+
1209
+ # This should not raise an exception and should return None
1210
+ result = get_pydantic_extra_config (ProblematicClass )
1211
+ assert result is None
1212
+
992
1213
993
1214
# attrs tests
994
1215
0 commit comments