@@ -72,6 +72,99 @@ class MyModel:
7272 assert m .__dict__ == {'field_a' : 'test' , 'field_b' : 12 }
7373
7474
75+ def test_model_class_extra_forbid ():
76+ class MyModel :
77+ class Meta :
78+ pass
79+
80+ # this is not required, but it avoids `__pydantic_fields_set__` being included in `__dict__`
81+ __slots__ = '__dict__' , '__pydantic_fields_set__' , '__pydantic_extra__' , '__pydantic_private__'
82+ field_a : str
83+ field_b : int
84+
85+ class Wrapper :
86+ def __init__ (self , inner ):
87+ self ._inner = inner
88+
89+ def __dir__ (self ):
90+ return dir (self ._inner )
91+
92+ def __getattr__ (self , key ):
93+ return getattr (self ._inner , key )
94+
95+ v = SchemaValidator (
96+ core_schema .model_schema (
97+ MyModel ,
98+ core_schema .model_fields_schema (
99+ {
100+ 'field_a' : core_schema .model_field (core_schema .str_schema ()),
101+ 'field_b' : core_schema .model_field (core_schema .int_schema ()),
102+ },
103+ extra_behavior = 'forbid' ,
104+ ),
105+ )
106+ )
107+ m = v .validate_python ({'field_a' : 'test' , 'field_b' : 12 })
108+ assert isinstance (m , MyModel )
109+ assert m .field_a == 'test'
110+ assert m .field_b == 12
111+
112+ # try revalidating from the model's attributes
113+ m = v .validate_python (Wrapper (m ), from_attributes = True )
114+
115+ with pytest .raises (ValidationError ) as exc_info :
116+ m = v .validate_python ({'field_a' : 'test' , 'field_b' : 12 , 'field_c' : 'extra' })
117+
118+ assert exc_info .value .errors (include_url = False ) == [
119+ {'type' : 'extra_forbidden' , 'loc' : ('field_c' ,), 'msg' : 'Extra inputs are not permitted' , 'input' : 'extra' }
120+ ]
121+
122+
123+ @pytest .mark .parametrize ('extra_behavior' , ['allow' , 'ignore' , 'forbid' ])
124+ def test_model_class_extra_forbid_from_attributes (extra_behavior : str ):
125+ # iterating attributes includes much more than just __dict__, so need
126+ # careful interaction with __extra__
127+
128+ class MyModel :
129+ # this is not required, but it avoids `__pydantic_fields_set__` being included in `__dict__`
130+ __slots__ = '__dict__' , '__pydantic_fields_set__' , '__pydantic_extra__' , '__pydantic_private__'
131+ field_a : str
132+ field_b : int
133+
134+ class Data :
135+ # https://github.com/pydantic/pydantic/issues/9242
136+ class Meta :
137+ pass
138+
139+ def __init__ (self , ** values ):
140+ self .__dict__ .update (values )
141+
142+ v = SchemaValidator (
143+ core_schema .model_schema (
144+ MyModel ,
145+ core_schema .model_fields_schema (
146+ {
147+ 'field_a' : core_schema .model_field (core_schema .str_schema ()),
148+ 'field_b' : core_schema .model_field (core_schema .int_schema ()),
149+ },
150+ extra_behavior = extra_behavior ,
151+ from_attributes = True ,
152+ ),
153+ )
154+ )
155+ m = v .validate_python (Data (field_a = 'test' , field_b = 12 ))
156+ assert isinstance (m , MyModel )
157+ assert m .field_a == 'test'
158+ assert m .field_b == 12
159+
160+ # with from_attributes, extra is basically ignored
161+ m = v .validate_python (Data (field_a = 'test' , field_b = 12 , field_c = 'extra' ))
162+ assert isinstance (m , MyModel )
163+ assert m .field_a == 'test'
164+ assert m .field_b == 12
165+ assert not hasattr (m , 'field_c' )
166+
167+
75168def test_model_class_setattr ():
76169 setattr_calls = []
77170
0 commit comments