Skip to content

Commit 7c0ed72

Browse files
Viicossydney-runkle
authored andcommitted
Do not evaluate annotations for private fields (#10962)
1 parent d6fc7fc commit 7c0ed72

File tree

4 files changed

+54
-30
lines changed

4 files changed

+54
-30
lines changed

pydantic/_internal/_fields.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def collect_model_fields( # noqa: C901
109109
if model_fields := getattr(base, '__pydantic_fields__', None):
110110
parent_fields_lookup.update(model_fields)
111111

112-
type_hints = _typing_extra.get_cls_type_hints(cls, ns_resolver=ns_resolver, lenient=True)
112+
type_hints = _typing_extra.get_model_type_hints(cls, ns_resolver=ns_resolver)
113113

114114
# https://docs.python.org/3/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older
115115
# annotations is only used for finding fields in parent classes
@@ -216,7 +216,6 @@ def collect_model_fields( # noqa: C901
216216
# Nothing stops us from just creating a new FieldInfo for this type hint, so we do this.
217217
field_info = FieldInfo_.from_annotation(ann_type)
218218
field_info.evaluated = evaluated
219-
220219
else:
221220
_warn_on_nested_alias_in_annotation(ann_type, ann_name)
222221
if isinstance(default, FieldInfo_) and ismethoddescriptor(default.default):

pydantic/_internal/_generate_schema.py

+2-6
Original file line numberDiff line numberDiff line change
@@ -1432,9 +1432,7 @@ def _typed_dict_schema(self, typed_dict_cls: Any, origin: Any) -> core_schema.Co
14321432
field_docstrings = None
14331433

14341434
try:
1435-
annotations = _typing_extra.get_cls_type_hints(
1436-
typed_dict_cls, ns_resolver=self._ns_resolver, lenient=False
1437-
)
1435+
annotations = _typing_extra.get_cls_type_hints(typed_dict_cls, ns_resolver=self._ns_resolver)
14381436
except NameError as e:
14391437
raise PydanticUndefinedAnnotation.from_name_error(e) from e
14401438

@@ -1496,9 +1494,7 @@ def _namedtuple_schema(self, namedtuple_cls: Any, origin: Any) -> core_schema.Co
14961494
namedtuple_cls = origin
14971495

14981496
try:
1499-
annotations = _typing_extra.get_cls_type_hints(
1500-
namedtuple_cls, ns_resolver=self._ns_resolver, lenient=False
1501-
)
1497+
annotations = _typing_extra.get_cls_type_hints(namedtuple_cls, ns_resolver=self._ns_resolver)
15021498
except NameError as e:
15031499
raise PydanticUndefinedAnnotation.from_name_error(e) from e
15041500
if not annotations:

pydantic/_internal/_typing_extra.py

+44-22
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import typing
1010
import warnings
1111
from functools import lru_cache, partial
12-
from typing import Any, Callable, Literal, overload
12+
from typing import TYPE_CHECKING, Any, Callable
1313

1414
import typing_extensions
1515
from typing_extensions import TypeIs, deprecated, get_args, get_origin
@@ -23,6 +23,8 @@
2323
from types import EllipsisType as EllipsisType
2424
from types import NoneType as NoneType
2525

26+
if TYPE_CHECKING:
27+
from pydantic import BaseModel
2628

2729
# See https://typing-extensions.readthedocs.io/en/latest/#runtime-use-of-types:
2830

@@ -467,34 +469,57 @@ def _type_convert(arg: Any) -> Any:
467469
return arg
468470

469471

470-
@overload
471-
def get_cls_type_hints(
472-
obj: type[Any],
473-
*,
474-
ns_resolver: NsResolver | None = None,
475-
lenient: Literal[True],
476-
) -> dict[str, tuple[Any, bool]]: ...
477-
@overload
478-
def get_cls_type_hints(
479-
obj: type[Any],
472+
def get_model_type_hints(
473+
obj: type[BaseModel],
480474
*,
481475
ns_resolver: NsResolver | None = None,
482-
lenient: Literal[False] = ...,
483-
) -> dict[str, Any]: ...
476+
) -> dict[str, tuple[Any, bool]]:
477+
"""Collect annotations from a Pydantic model class, including those from parent classes.
478+
479+
Args:
480+
obj: The Pydantic model to inspect.
481+
ns_resolver: A namespace resolver instance to use. Defaults to an empty instance.
482+
483+
Returns:
484+
A dictionary mapping annotation names to a two-tuple: the first element is the evaluated
485+
type or the original annotation if a `NameError` occurred, the second element is a boolean
486+
indicating if whether the evaluation succeeded.
487+
"""
488+
hints: dict[str, Any] | dict[str, tuple[Any, bool]] = {}
489+
ns_resolver = ns_resolver or NsResolver()
490+
491+
for base in reversed(obj.__mro__):
492+
ann: dict[str, Any] | None = base.__dict__.get('__annotations__')
493+
if not ann or isinstance(ann, types.GetSetDescriptorType):
494+
continue
495+
with ns_resolver.push(base):
496+
globalns, localns = ns_resolver.types_namespace
497+
for name, value in ann.items():
498+
if name.startswith('_'):
499+
# For private attributes, we only need the annotation to detect the `ClassVar` special form.
500+
# For this reason, we still try to evaluate it, but we also catch any possible exception (on
501+
# top of the `NameError`s caught in `try_eval_type`) that could happen so that users are free
502+
# to use any kind of forward annotation for private fields (e.g. circular imports, new typing
503+
# syntax, etc).
504+
try:
505+
hints[name] = try_eval_type(value, globalns, localns)
506+
except Exception:
507+
hints[name] = (value, False)
508+
else:
509+
hints[name] = try_eval_type(value, globalns, localns)
510+
return hints
511+
512+
484513
def get_cls_type_hints(
485514
obj: type[Any],
486515
*,
487516
ns_resolver: NsResolver | None = None,
488-
lenient: bool = False,
489-
) -> dict[str, Any] | dict[str, tuple[Any, bool]]:
517+
) -> dict[str, Any]:
490518
"""Collect annotations from a class, including those from parent classes.
491519
492520
Args:
493521
obj: The class to inspect.
494522
ns_resolver: A namespace resolver instance to use. Defaults to an empty instance.
495-
lenient: Whether to keep unresolvable annotations as is or re-raise the `NameError` exception.
496-
If lenient, an extra boolean flag is set for each annotation value to indicate whether the
497-
evaluation succeeded or not. Default: re-raise.
498523
"""
499524
hints: dict[str, Any] | dict[str, tuple[Any, bool]] = {}
500525
ns_resolver = ns_resolver or NsResolver()
@@ -506,10 +531,7 @@ def get_cls_type_hints(
506531
with ns_resolver.push(base):
507532
globalns, localns = ns_resolver.types_namespace
508533
for name, value in ann.items():
509-
if lenient:
510-
hints[name] = try_eval_type(value, globalns, localns)
511-
else:
512-
hints[name] = eval_type(value, globalns, localns)
534+
hints[name] = eval_type(value, globalns, localns)
513535
return hints
514536

515537

tests/test_forward_ref.py

+7
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,13 @@ class Model(BaseModel):
598598
assert module.Model.__private_attributes__ == {}
599599

600600

601+
def test_private_attr_annotation_not_evaluated() -> None:
602+
class Model(BaseModel):
603+
_a: 'UnknownAnnotation'
604+
605+
assert '_a' in Model.__private_attributes__
606+
607+
601608
def test_json_encoder_str(create_module):
602609
module = create_module(
603610
# language=Python

0 commit comments

Comments
 (0)