Skip to content

Commit 9f76e24

Browse files
committed
Remove generics cache workaround
Backport of: #11755
1 parent 5e46044 commit 9f76e24

File tree

3 files changed

+49
-14
lines changed

3 files changed

+49
-14
lines changed

pydantic/main.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -908,12 +908,7 @@ def __class_getitem__(
908908

909909
submodel = _generics.create_generic_submodel(model_name, origin, args, params)
910910

911-
# Cache the generated model *only* if not in the process of parametrizing
912-
# another model. In some valid scenarios, we miss the opportunity to cache
913-
# it but in some cases this results in `PydanticRecursiveRef` instances left
914-
# on `FieldInfo` annotations:
915-
if len(_generics.recursively_defined_type_refs()) == 1:
916-
_generics.set_cached_generic_type(cls, typevar_values, submodel, origin, args)
911+
_generics.set_cached_generic_type(cls, typevar_values, submodel, origin, args)
917912

918913
return submodel
919914

tests/test_forward_ref.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -718,15 +718,9 @@ def test_recursive_models_union(create_module):
718718
# This test should pass because PydanticRecursiveRef.__or__ is implemented,
719719
# not because `eval_type_backport` magically makes `|` work,
720720
# since it's installed for tests but otherwise optional.
721-
# When generic models are involved in recursive models, parametrizing a model
722-
# can result in a `PydanticRecursiveRef` instance. This isn't ideal, as in the
723-
# example below, this results in the `FieldInfo.annotation` attribute being changed,
724-
# e.g. for `bar` to something like `PydanticRecursiveRef(...) | None`.
725-
# We currently have a workaround (avoid caching parametrized models where this bad
726-
# annotation mutation can happen).
727721
sys.modules['eval_type_backport'] = None # type: ignore
728722
try:
729-
create_module(
723+
module = create_module(
730724
# language=Python
731725
"""
732726
from __future__ import annotations
@@ -742,14 +736,20 @@ class Foo(BaseModel):
742736
743737
class Bar(BaseModel, Generic[T]):
744738
foo: Foo
739+
740+
Foo.model_rebuild()
745741
"""
746742
)
747743
finally:
748744
del sys.modules['eval_type_backport']
749745

746+
assert module.Foo.model_fields['bar'].annotation == typing.Optional[module.Bar[str]]
747+
assert module.Foo.model_fields['bar2'].annotation == typing.Union[int, module.Bar[float]]
748+
assert module.Bar.model_fields['foo'].annotation == module.Foo
749+
750750

751751
def test_recursive_models_union_backport(create_module):
752-
create_module(
752+
module = create_module(
753753
# language=Python
754754
"""
755755
from __future__ import annotations
@@ -768,9 +768,15 @@ class Foo(BaseModel):
768768
769769
class Bar(BaseModel, Generic[T]):
770770
foo: Foo
771+
772+
Foo.model_rebuild()
771773
"""
772774
)
773775

776+
assert module.Foo.model_fields['bar'].annotation == typing.Optional[module.Bar[str]]
777+
assert module.Foo.model_fields['bar2'].annotation == typing.Union[int, str, module.Bar[float]]
778+
assert module.Bar.model_fields['foo'].annotation == module.Foo
779+
774780

775781
def test_force_rebuild():
776782
class Foobar(BaseModel):

tests/test_generics.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
Never,
2828
NotRequired,
2929
ParamSpec,
30+
TypeAliasType,
3031
TypedDict,
3132
TypeVarTuple,
3233
Unpack,
@@ -519,6 +520,37 @@ class M(BaseModel):
519520
del generics
520521

521522

523+
def test_generics_reused() -> None:
524+
"""https://github.com/pydantic/pydantic/issues/11747
525+
526+
To fix an issue with recursive generics, we introduced a change in 2.11 that would
527+
skip caching the parameterized model under specific circumstances. The following setup
528+
is an example of where this would happen. As a result, we ended up with two different `A[int]`
529+
classes, although they were the same in practice.
530+
When serializing, we check that the value instances are matching the type, but we ended up
531+
with warnings as `isinstance(value, A[int])` fails.
532+
The fix was reverted as a refactor (https://github.com/pydantic/pydantic/pull/11388) fixed
533+
the underlying issue.
534+
"""
535+
536+
T = TypeVar('T')
537+
538+
class A(BaseModel, Generic[T]):
539+
pass
540+
541+
class B(BaseModel, Generic[T]):
542+
pass
543+
544+
AorB = TypeAliasType('AorB', Union[A[T], B[T]], type_params=(T,))
545+
546+
class Main(BaseModel, Generic[T]):
547+
ls: list[AorB[T]] = []
548+
549+
m = Main[int]()
550+
m.ls.append(A[int]())
551+
m.model_dump_json(warnings='error')
552+
553+
522554
def test_generic_config():
523555
data_type = TypeVar('data_type')
524556

@@ -1445,7 +1477,9 @@ class InnerModel(OuterModel[T], Generic[T]):
14451477

14461478
with pytest.raises(ValidationError):
14471479
InnerModel[int](a=['s', {'a': 'wrong'}])
1480+
14481481
assert InnerModel[int](a=['s', {'a': 1}]).a[1].a == 1
1482+
assert InnerModel[int].model_fields['a'].annotation == Optional[list[Union[ReferencedModel[int], str]]]
14491483

14501484

14511485
def test_deep_generic_with_multiple_typevars():

0 commit comments

Comments
 (0)