-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Make sure the type reference is removed from the seen references #11143
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Deploying pydantic-docs with
|
Latest commit: |
75241c2
|
Status: | ✅ Deploy successful! |
Preview URL: | https://95df4efe.pydantic-docs.pages.dev |
Branch Preview URL: | https://11024.pydantic-docs.pages.dev |
CodSpeed Performance ReportMerging #11143 will improve performances by 6.26%Comparing Summary
Benchmarks breakdown
|
I'll note, the current implementation is confusing and I think this is what led to the oversight. Here is a simpler implementation: _generic_recursion_cache: ContextVar[set[str]] = ContextVar('_generic_recursion_cache', default=set())
@contextmanager
def generic_recursion_self_type(
origin: type[BaseModel], args: tuple[Any, ...]
) -> Iterator[PydanticRecursiveRef | None]:
"""This contextmanager should be placed around the recursive calls used to build a generic type,
and accept as arguments the generic origin type and the type arguments being passed to it.
If the same origin and arguments are observed twice, it implies that a self-reference placeholder
can be used while building the core schema, and will produce a schema_ref that will be valid in the
final parent schema.
"""
previously_seen_type_refs = _generic_recursion_cache.get()
type_ref = get_type_ref(origin, args_override=args)
if type_ref in previously_seen_type_refs:
self_type = PydanticRecursiveRef(type_ref=type_ref)
yield self_type
else:
try:
previously_seen_type_refs.add(type_ref)
yield
finally:
previously_seen_type_refs.remove(type_ref) Diffdiff --git a/pydantic/_internal/_generics.py b/pydantic/_internal/_generics.py
index 7b57fc71c..dc474861a 100644
--- a/pydantic/_internal/_generics.py
+++ b/pydantic/_internal/_generics.py
@@ -401,7 +401,7 @@ def check_parameters_count(cls: type[BaseModel], parameters: tuple[Any, ...]) ->
raise TypeError(f'Too {description} parameters for {cls}; actual {actual}, expected {expected}')
-_generic_recursion_cache: ContextVar[set[str] | None] = ContextVar('_generic_recursion_cache', default=None)
+_generic_recursion_cache: ContextVar[set[str]] = ContextVar('_generic_recursion_cache', default=set())
@contextmanager
@@ -416,24 +416,17 @@ def generic_recursion_self_type(
final parent schema.
"""
previously_seen_type_refs = _generic_recursion_cache.get()
- if previously_seen_type_refs is None:
- previously_seen_type_refs = set()
- token = _generic_recursion_cache.set(previously_seen_type_refs)
- else:
- token = None
+ type_ref = get_type_ref(origin, args_override=args)
- try:
- type_ref = get_type_ref(origin, args_override=args)
- if type_ref in previously_seen_type_refs:
- self_type = PydanticRecursiveRef(type_ref=type_ref)
- yield self_type
- else:
+ if type_ref in previously_seen_type_refs:
+ self_type = PydanticRecursiveRef(type_ref=type_ref)
+ yield self_type
+ else:
+ try:
previously_seen_type_refs.add(type_ref)
yield
+ finally:
previously_seen_type_refs.remove(type_ref)
- finally:
- if token:
- _generic_recursion_cache.reset(token)
def recursively_defined_type_refs() -> set[str]: Edit: turns out this is not going to work well as context variables shouldn't have mutable defaults. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work, easy enough fix. Thanks for the thorough test.
When parametrizing generic models, the `generic_recursion_self_type` context manager is entered, and is used to avoid infinite recursions if the same generic model happens to be parametrized again with the same args during the first parametrization. However, upon exiting the context manager (and thus when the parametrized model is fully created), we forgot to remove the type ref from the set. This happened only if we were already in the process of parametrizing another model, as otherwise the `_generic_recursion_cache` would be reset (see the `if token` condition). In theory, this couldn't cause issues because parametrized models are cached, and the cache is checked *before* entering the context manager. However, because we have custom `mro()` implementation on the `BaseModel` metaclass, this ends up causing issues is some really specific scenarios.
Well credits goes to @kc0506 for this one |
) When parametrizing generic models, the `generic_recursion_self_type` context manager is entered, and is used to avoid infinite recursions if the same generic model happens to be parametrized again with the same args during the first parametrization. However, upon exiting the context manager (and thus when the parametrized model is fully created), we forgot to remove the type ref from the set. This happened only if we were already in the process of parametrizing another model, as otherwise the `_generic_recursion_cache` would be reset (see the `if token` condition). In theory, this couldn't cause issues because parametrized models are cached, and the cache is checked *before* entering the context manager. However, because we have custom `mro()` implementation on the `BaseModel` metaclass, this ends up causing issues is some really specific scenarios. Co-authored-by: kc0506 <kchong0506@gmail.com>
As per #11037. Fixes #11024.
When parametrizing generic models, the
generic_recursion_self_type
context manager is entered, and is used to avoid infinite recursions if the same generic model happens to be parametrized again with the same args during the first parametrization.However, upon exiting the context manager (and thus when the parametrized model is fully created), we forgot to remove the type ref from the set. This happened only if we were already in the process of parametrizing another model, as otherwise the
_generic_recursion_cache
would be reset (see theif token
condition).In theory, this couldn't cause issues because parametrized models are cached, and the cache is checked before entering the context manager. However, because we have custom
mro()
implementation on theBaseModel
metaclass, this ends up causing issues is some really specific scenarios.Change Summary
Related issue number
Checklist