1- """ This test suite does not intend to re-test pydantic but rather
1+ """This test suite does not intend to re-test pydantic but rather
22check some "corner cases" or critical setups with pydantic model such that:
33
44- we can ensure a given behaviour is preserved through updates
55- document/clarify some concept
66
77"""
88
9- from typing import Any , Union , get_args , get_origin
9+ from typing import Any , Literal , Union , get_args , get_origin
1010
1111import pytest
1212from common_library .json_serialization import json_dumps
1313from models_library .projects_nodes import InputTypes , OutputTypes
1414from models_library .projects_nodes_io import SimCoreFileLink
15- from pydantic import BaseModel , Field , TypeAdapter , ValidationError
15+ from models_library .utils .change_case import snake_to_camel
16+ from pydantic import BaseModel , ConfigDict , Field , TypeAdapter , ValidationError
1617from pydantic .types import Json
1718from pydantic .version import version_short
1819
@@ -120,7 +121,7 @@ class Func(BaseModel):
120121 {"$ref" : "#/$defs/DatCoreFileLink" },
121122 {"$ref" : "#/$defs/DownloadLink" },
122123 {"type" : "array" , "items" : {}},
123- {"type" : "object" },
124+ {"type" : "object" , "additionalProperties" : True },
124125 ],
125126 }
126127
@@ -154,7 +155,7 @@ class Func(BaseModel):
154155 MINIMAL = 2 # <--- index of the example with the minimum required fields
155156 assert SimCoreFileLink in get_args (OutputTypes )
156157 example = SimCoreFileLink .model_validate (
157- SimCoreFileLink .model_config [ "json_schema_extra" ] ["examples" ][MINIMAL ]
158+ SimCoreFileLink .model_json_schema () ["examples" ][MINIMAL ]
158159 )
159160 model = Func .model_validate (
160161 {
@@ -183,7 +184,9 @@ def test_nullable_fields_from_pydantic_v1():
183184 # SEE https://github.com/ITISFoundation/osparc-simcore/pull/6751
184185 class MyModel (BaseModel ):
185186 # pydanticv1 would add a default to fields set as nullable
186- nullable_required : str | None # <--- This was default to =None in pydantic 1 !!!
187+ nullable_required : (
188+ str | None
189+ ) # <--- This was default to =None in pydantic 1 !!!
187190 nullable_required_with_hyphen : str | None = Field (default = ...)
188191 nullable_optional : str | None = None
189192
@@ -209,3 +212,112 @@ class MyModel(BaseModel):
209212 data ["nullable_required" ] = None
210213 model = MyModel .model_validate (data )
211214 assert model .model_dump (exclude_unset = True ) == data
215+
216+
217+ # BELOW some tests related to depreacated `populate_by_name` in pydantic v2.11+ !!
218+ #
219+ # https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict.populate_by_name
220+ #
221+ # `populate_by_name` usage is not recommended in v2.11+ and will be deprecated in v3. Instead, you should use the validate_by_name configuration setting.
222+ # When validate_by_name=True and validate_by_alias=True, this is strictly equivalent to the previous behavior of populate_by_name=True.
223+ # In v2.11, we also introduced a validate_by_alias setting that introduces more fine grained control for validation behavior.
224+ # Here's how you might go about using the new settings to achieve the same behavior:
225+ #
226+
227+
228+ @pytest .mark .parametrize ("extra" , ["ignore" , "allow" , "forbid" ])
229+ @pytest .mark .parametrize (
230+ "validate_by_alias, validate_by_name" ,
231+ [
232+ # NOTE: (False, False) is not allowed: at least one has to be True!
233+ # SEE https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict.validate_by_alias
234+ (False , True ),
235+ (True , False ),
236+ (True , True ),
237+ ],
238+ )
239+ def test_model_config_validate_by_alias_and_name (
240+ validate_by_alias : bool ,
241+ validate_by_name : bool ,
242+ extra : Literal ["ignore" , "allow" , "forbid" ],
243+ ):
244+ class TestModel (BaseModel ):
245+ snake_case : str | None = None
246+
247+ model_config = ConfigDict (
248+ validate_by_alias = validate_by_alias ,
249+ validate_by_name = validate_by_name ,
250+ extra = extra ,
251+ alias_generator = snake_to_camel ,
252+ )
253+
254+ assert TestModel .model_config .get ("populate_by_name" ) is None
255+ assert TestModel .model_config .get ("validate_by_alias" ) is validate_by_alias
256+ assert TestModel .model_config .get ("validate_by_name" ) is validate_by_name
257+ assert TestModel .model_config .get ("extra" ) == extra
258+
259+ if validate_by_alias is False :
260+
261+ if extra == "forbid" :
262+ with pytest .raises (ValidationError ):
263+ TestModel .model_validate ({"snakeCase" : "foo" })
264+
265+ elif extra == "ignore" :
266+ model = TestModel .model_validate ({"snakeCase" : "foo" })
267+ assert model .snake_case is None
268+ assert model .model_dump () == {"snake_case" : None }
269+
270+ elif extra == "allow" :
271+ model = TestModel .model_validate ({"snakeCase" : "foo" })
272+ assert model .snake_case is None
273+ assert model .model_dump () == {"snake_case" : None , "snakeCase" : "foo" }
274+
275+ else :
276+ assert TestModel .model_validate ({"snakeCase" : "foo" }).snake_case == "foo"
277+
278+ if validate_by_name is False :
279+ if extra == "forbid" :
280+ with pytest .raises (ValidationError ):
281+ TestModel .model_validate ({"snake_case" : "foo" })
282+
283+ elif extra == "ignore" :
284+ model = TestModel .model_validate ({"snake_case" : "foo" })
285+ assert model .snake_case is None
286+ assert model .model_dump () == {"snake_case" : None }
287+
288+ elif extra == "allow" :
289+ model = TestModel .model_validate ({"snake_case" : "foo" })
290+ assert model .snake_case is None
291+ assert model .model_dump () == {"snake_case" : "foo" }
292+ else :
293+ assert TestModel .model_validate ({"snake_case" : "foo" }).snake_case == "foo"
294+
295+
296+ @pytest .mark .parametrize ("populate_by_name" , [True , False ])
297+ def test_model_config_populate_by_name (populate_by_name : bool ):
298+ # SEE https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict.populate_by_name
299+ class TestModel (BaseModel ):
300+ snake_case : str | None = None
301+
302+ model_config = ConfigDict (
303+ populate_by_name = populate_by_name ,
304+ extra = "forbid" , # easier to check the effect of populate_by_name!
305+ alias_generator = snake_to_camel ,
306+ )
307+
308+ # checks how they are set
309+ assert TestModel .model_config .get ("populate_by_name" ) is populate_by_name
310+ assert TestModel .model_config .get ("extra" ) == "forbid"
311+
312+ # NOTE how defaults work with populate_by_name!!
313+ assert TestModel .model_config .get ("validate_by_name" ) == populate_by_name
314+ assert TestModel .model_config .get ("validate_by_alias" ) is True # Default
315+
316+ # validate_by_alias BEHAVIUOR defaults to True
317+ TestModel .model_validate ({"snakeCase" : "foo" })
318+
319+ if populate_by_name :
320+ assert TestModel .model_validate ({"snake_case" : "foo" }).snake_case == "foo"
321+ else :
322+ with pytest .raises (ValidationError ):
323+ TestModel .model_validate ({"snake_case" : "foo" })
0 commit comments