Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork2.2k
Only evaluateFieldInfo
annotations if required during schema building#10769
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes from1 commit
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
FieldInfo
annotations if required during schema buildingBy adding a new `evaluated` attribute that can be set during modelfields collection. Also refactor a bit the utility functions to evalsuch annotations.
- Loading branch information
Uh oh!
There was an error while loading.Please reload this page.
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1221,11 +1221,15 @@ def _common_field_schema( # C901 | ||
) -> _CommonField: | ||
# Update FieldInfo annotation if appropriate: | ||
FieldInfo = import_cached_field_info() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. The interesting part is that we remove the call to | ||
if not field_info.evaluated: | ||
# TODO Can we use field_info.apply_typevars_map here? | ||
try: | ||
evaluated = _typing_extra.eval_type(field_info.annotation, *self._types_namespace) | ||
sydney-runkle marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
except NameError as e: | ||
raise PydanticUndefinedAnnotation.from_name_error(e) from e | ||
Viicos marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
evaluated = replace_types(evaluated, self._typevars_map) | ||
field_info.evaluated = True | ||
if not has_instance_in_type(evaluated, PydanticRecursiveRef): | ||
new_field_info = FieldInfo.from_annotation(evaluated) | ||
field_info.annotation = new_field_info.annotation | ||
@@ -1344,12 +1348,13 @@ def _type_alias_type_schema(self, obj: TypeAliasType) -> CoreSchema: | ||
return maybe_schema | ||
origin: TypeAliasType = get_origin(obj) or obj | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Moved below, as accessing the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Maybe I'm misunderstanding - was this moved below? Or do we not need this specific line anymore? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Basically, here is what can happen with type aliases: typeA=Intannotation=origin.__value__# NameError, 'Int' is not definedtypeB='Int'annotation=origin.__value__assertannotation=='Int'eval_type(B, ..., ...)# NameError, 'Int' is not defined So everything was moved in a single try:annotation=_typing_extra.eval_type(origin.__value__,*self._types_namespace)except ...: ...
| ||
typevars_map = get_standard_typevars_map(obj) | ||
with self._ns_resolver.push(origin): | ||
try: | ||
annotation = _typing_extra.eval_type(origin.__value__, *self._types_namespace) | ||
except NameError as e: | ||
raise PydanticUndefinedAnnotation.from_name_error(e) from e | ||
annotation = replace_types(annotation, typevars_map) | ||
schema = self.generate_schema(annotation) | ||
assert schema['type'] != 'definitions' | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -9,7 +9,7 @@ | ||
import typing | ||
import warnings | ||
from functools import lru_cache, partial | ||
from typing import Any, Callable, Literal, overload | ||
import typing_extensions | ||
from typing_extensions import TypeIs, deprecated, get_args, get_origin | ||
@@ -449,17 +449,54 @@ def parent_frame_namespace(*, parent_depth: int = 2, force: bool = False) -> dic | ||
return frame.f_locals | ||
def _type_convert(arg: Any) -> Any: | ||
"""Convert `None` to `NoneType` and strings to `ForwardRef` instances. | ||
This is a backport of the private `typing._type_convert` function. When | ||
evaluating a type, `ForwardRef._evaluate` ends up being called, and is | ||
responsible for making this conversion. However, we still have to apply | ||
it for the first argument passed to our type evaluation functions, similarly | ||
to the `typing.get_type_hints` function. | ||
""" | ||
Viicos marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
if arg is None: | ||
return NoneType | ||
if isinstance(arg, str): | ||
# Like `typing.get_type_hints`, assume the arg can be in any context, | ||
# hence the proper `is_argument` and `is_class` args: | ||
return _make_forward_ref(arg, is_argument=False, is_class=True) | ||
return arg | ||
@overload | ||
sydney-runkle marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
def get_cls_type_hints( | ||
obj: type[Any], | ||
*, | ||
ns_resolver: NsResolver | None = None, | ||
lenient: Literal[True], | ||
) -> dict[str, tuple[Any, bool]]: ... | ||
@overload | ||
def get_cls_type_hints( | ||
obj: type[Any], | ||
*, | ||
ns_resolver: NsResolver | None = None, | ||
lenient: Literal[False] = ..., | ||
) -> dict[str, Any]: ... | ||
def get_cls_type_hints( | ||
obj: type[Any], | ||
*, | ||
ns_resolver: NsResolver | None = None, | ||
lenient: bool = False, | ||
sydney-runkle marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
) -> dict[str, Any] | dict[str, tuple[Any, bool]]: | ||
"""Collect annotations from a class, including those from parent classes. | ||
Args: | ||
obj: The class to inspect. | ||
ns_resolver: A namespace resolver instance to use. Defaults to an empty instance. | ||
lenient: Whether to keep unresolvable annotations as is or re-raise the `NameError` exception. | ||
If lenient, an extra boolean flag is set for each annotation value to indicate whether the | ||
evaluation succeeded or not. Default: re-raise. | ||
""" | ||
hints: dict[str, Any] | dict[str, tuple[Any, bool]] = {} | ||
ns_resolver = ns_resolver or NsResolver() | ||
for base in reversed(obj.__mro__): | ||
@@ -469,50 +506,66 @@ def get_cls_type_hints( | ||
with ns_resolver.push(base): | ||
globalns, localns = ns_resolver.types_namespace | ||
for name, value in ann.items(): | ||
if lenient: | ||
hints[name] = try_eval_type(value, globalns, localns) | ||
else: | ||
hints[name] = eval_type(value, globalns, localns) | ||
return hints | ||
deftry_eval_type( | ||
value: Any, | ||
globalns: GlobalsNamespace | None = None, | ||
localns: MappingNamespace | None = None, | ||
) -> tuple[Any, bool]: | ||
"""Try evaluating the annotation using the provided namespaces. | ||
Args: | ||
value: The value to evaluate. If `None`, it will be replaced by `type[None]`. If an instance | ||
of `str`, it will be converted to a `ForwardRef`. | ||
localns: The global namespace to use during annotation evaluation. | ||
globalns: The local namespace to use during annotation evaluation. | ||
Returns: | ||
A two-tuple containing the possibly evaluated type and a boolean indicating | ||
whether the evaluation succeeded or not. | ||
""" | ||
value = _type_convert(value) | ||
try: | ||
return eval_type_backport(value, globalns, localns), True | ||
except NameError: | ||
return value, False | ||
def eval_type( | ||
value: Any, | ||
globalns: GlobalsNamespace | None = None, | ||
localns: MappingNamespace | None = None, | ||
) -> Any: | ||
"""Evaluate the annotation using the provided namespaces. | ||
Args: | ||
value: The value to evaluate. If `None`, it will be replaced by `type[None]`. If an instance | ||
of `str`, it will be converted to a `ForwardRef`. | ||
localns: The global namespace to use during annotation evaluation. | ||
globalns: The local namespace to use during annotation evaluation. | ||
""" | ||
value = _type_convert(value) | ||
return eval_type_backport(value, globalns, localns) | ||
@deprecated( | ||
'`eval_type_lenient` is deprecated, use `try_eval_type` instead.', | ||
Comment on lines -507 to +559 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Hmm, this feels like an anti-pattern - if we already deprecated this, there's probably a reason why we didn't want to have two functions. Even if we disagree with this deprecation now, seems like a lot of churn to basically reintroduce the function and redirect for the deprecated case... I believe my suggestion above re always returning a second value, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. A few more follow ups:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. I had to keep this function during the types namespaces refactor because fastapi makes use of it 🫤. I added the
Same issue ashttps://github.com/pydantic/pydantic/pull/10769/files#r1832947088, we can't have a single function because we want to catch the | ||
category=None, | ||
) | ||
def eval_type_lenient( | ||
value: Any, | ||
globalns: GlobalsNamespace | None = None, | ||
localns: MappingNamespace | None = None, | ||
) -> Any: | ||
ev, _ = try_eval_type(value, globalns, localns) | ||
return ev | ||
def eval_type_backport( | ||