Skip to content

Commit 7f6943a

Browse files
committed
Force the use of extendable>=1.3.0
This is required to avoid a bug in some pydantic methods when a instance is checked against its own type and the result was wrongly returned as False see lmignon/extendable#18
1 parent 2dc6743 commit 7f6943a

File tree

2 files changed

+183
-1
lines changed

2 files changed

+183
-1
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ classifiers = [
1414
]
1515
dynamic = ["version", "description"]
1616
dependencies = [
17-
"extendable>=1.2.0",
17+
"extendable>=1.3.0",
1818
"pydantic>=2.0.2",
1919
"wrapt",
2020
]

tests/test_union.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
"""Test Union."""
2+
3+
import sys
4+
from typing import Union, Any, Optional
5+
6+
if sys.version_info >= (3, 9):
7+
from typing import Annotated
8+
else:
9+
from typing_extensions import Annotated
10+
11+
from pydantic import Tag, Discriminator
12+
from pydantic.main import BaseModel
13+
14+
from extendable_pydantic import ExtendableModelMeta
15+
16+
from pytest import fixture
17+
18+
19+
@fixture
20+
def model_union_base_models():
21+
class Coordinate(
22+
BaseModel,
23+
metaclass=ExtendableModelMeta,
24+
revalidate_instances="always",
25+
validate_assignment=True,
26+
):
27+
lat: float = 0.1
28+
lng: float = 10.1
29+
30+
class CoordinateWithCountry(Coordinate):
31+
country: str = None
32+
33+
def _test_discriminate_coordinate_type(v: Any) -> Optional[str]:
34+
if isinstance(v, CoordinateWithCountry):
35+
return "coordinate_with_country"
36+
if isinstance(v, Coordinate):
37+
return "coordinate"
38+
if isinstance(v, dict):
39+
if "country" in v:
40+
return "coordinate_with_country"
41+
if "lat" in v:
42+
return "coordinate"
43+
return None
44+
45+
class Person(
46+
BaseModel,
47+
metaclass=ExtendableModelMeta,
48+
revalidate_instances="always",
49+
validate_assignment=True,
50+
):
51+
name: str
52+
coordinate: Optional[
53+
Annotated[
54+
Union[
55+
Annotated[Coordinate, Tag("coordinate")],
56+
Annotated[CoordinateWithCountry, Tag("coordinate_with_country")],
57+
],
58+
Discriminator(_test_discriminate_coordinate_type),
59+
]
60+
] = None
61+
62+
return Person, Coordinate, CoordinateWithCountry
63+
64+
65+
@fixture
66+
def model_union_models(model_union_base_models):
67+
Person, Coordinate, CoordinateWithCountry = model_union_base_models
68+
69+
class ExtendedCoordinate(Coordinate, extends=Coordinate):
70+
state: str = None
71+
72+
class ExtendedCoordinateWithCountry(
73+
CoordinateWithCountry, extends=CoordinateWithCountry
74+
):
75+
currency: str = None
76+
77+
return (
78+
Person,
79+
Coordinate,
80+
ExtendedCoordinate,
81+
CoordinateWithCountry,
82+
ExtendedCoordinateWithCountry,
83+
)
84+
85+
86+
def test_model_union(model_union_models, test_registry):
87+
88+
(
89+
Person,
90+
Coordinate,
91+
ExtendedCoordinate,
92+
CoordinateWithCountry,
93+
_ExtendedCoordinateWithCountry,
94+
) = model_union_models
95+
test_registry.init_registry()
96+
ClsPerson = test_registry[Person.__xreg_name__]
97+
# check that the behaviour is the same for all the definitions
98+
# of the same model...
99+
classes = Person, ClsPerson
100+
for cls in classes:
101+
person = cls(
102+
name="test",
103+
coordinate={
104+
"lng": 5.0,
105+
"lat": 4.2,
106+
"country": "belgium",
107+
"state": "brussels",
108+
"currency": "euro",
109+
},
110+
)
111+
coordinate = person.coordinate
112+
assert isinstance(coordinate, CoordinateWithCountry)
113+
person = cls(
114+
name="test",
115+
coordinate={"lng": 5.0, "lat": 4.2, "state": "brussels"},
116+
)
117+
coordinate = person.coordinate
118+
assert isinstance(coordinate, Coordinate)
119+
120+
# sub schema are stored into the definition property
121+
definitions = ClsPerson.model_json_schema().get("$defs", {})
122+
assert "Coordinate" in definitions
123+
coordinate_properties = definitions["Coordinate"].get("properties", {}).keys()
124+
assert {"lat", "lng", "state"} == set(coordinate_properties)
125+
assert "CoordinateWithCountry" in definitions
126+
coordinate_with_country_properties = (
127+
definitions["CoordinateWithCountry"].get("properties", {}).keys()
128+
)
129+
assert {"lat", "lng", "country", "state", "currency"} == set(
130+
coordinate_with_country_properties
131+
)
132+
133+
person = Person(
134+
name="test",
135+
coordinate=Coordinate(
136+
lat=5.0,
137+
lng=4.2,
138+
state="brussels",
139+
),
140+
)
141+
assert isinstance(person.coordinate, Coordinate)
142+
assert isinstance(person.coordinate, ExtendedCoordinate)
143+
assert person.model_dump(mode="python") == {
144+
"name": "test",
145+
"coordinate": {"lat": 5.0, "lng": 4.2, "state": "brussels"},
146+
}
147+
148+
person = Person(
149+
name="test",
150+
coordinate=ExtendedCoordinate(
151+
lat=5.0,
152+
lng=4.2,
153+
state="brussels",
154+
),
155+
)
156+
assert isinstance(person.coordinate, Coordinate)
157+
assert isinstance(person.coordinate, ExtendedCoordinate)
158+
159+
160+
def test_union_validate_on_assign(model_union_base_models, test_registry):
161+
# covers issue https://github.com/lmignon/extendable/issues/18
162+
Person, Coordinate, CoordinateWithCountry = model_union_base_models
163+
test_registry.init_registry()
164+
ClsPerson = test_registry[Person.__xreg_name__]
165+
# check that the behaviour is the same for all the definitions
166+
# of the same model...
167+
classes = Person, ClsPerson
168+
for cls in classes:
169+
person = cls(
170+
name="test",
171+
)
172+
person.coordinate = CoordinateWithCountry(
173+
lng=5.0, lat=4.2, country="belgium", state="brussels", currency="euro"
174+
)
175+
coordinate = person.coordinate
176+
assert isinstance(coordinate, CoordinateWithCountry)
177+
person = cls(
178+
name="test",
179+
coordinate={"lng": 5.0, "lat": 4.2, "state": "brussels"},
180+
)
181+
coordinate = person.coordinate
182+
assert isinstance(coordinate, Coordinate)

0 commit comments

Comments
 (0)