@@ -825,6 +825,62 @@ class NestedModel(pydantic.BaseModel):
825825 class PydanticNestedDict (pydantic .BaseModel ):
826826 nested : Optional [Dict [str , NestedModel ]] = None
827827
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+
828884
829885def none (x ):
830886 return x
@@ -989,6 +1045,171 @@ def test_nested_dict(self, parser):
9891045 assert isinstance (init .model , PydanticNestedDict )
9901046 assert isinstance (init .model .nested ["key" ], NestedModel )
9911047
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+
9921213
9931214# attrs tests
9941215
0 commit comments