1"""Logic for interacting with type annotations, mostly extensions, shims and hacks to wrap Python's typing module."""
2
3from __future__ import annotations
4
5import re
6import sys
7import types
8import typing
9from collections.abc import Callable, MutableMapping
10from functools import partial
11from inspect import Signature, signature
12from types import NoneType
13from typing import TYPE_CHECKING, Any, ForwardRef, cast
14
15import typing_extensions
16from typing_extensions import deprecated, get_args, get_origin # noqa: UP035 (for `get_args` and `get_origin`)
17from typing_inspection import typing_objects
18from typing_inspection.introspection import is_union_origin
19
20from pydantic.version import version_short
21
22from ._namespace_utils import GlobalsNamespace, MappingNamespace, NsResolver, get_module_ns_of
23
24if sys.version_info >= (3, 14):
25 import annotationlib
26
27if TYPE_CHECKING:
28 from pydantic import BaseModel
29 from pydantic.fields import FieldInfo
30
31# As per https://typing-extensions.readthedocs.io/en/latest/#runtime-use-of-types,
32# always check for both `typing` and `typing_extensions` variants of a typing construct.
33# (this is implemented differently than the suggested approach in the `typing_extensions`
34# docs for performance).
35
36
37_t_annotated = typing.Annotated
38_te_annotated = typing_extensions.Annotated
39
40
41def is_annotated(tp: Any, /) -> bool:
42 """Return whether the provided argument is a `Annotated` special form.
43
44 ```python {test="skip" lint="skip"}
45 is_annotated(Annotated[int, ...])
46 #> True
47 ```
48 """
49 origin = get_origin(tp)
50 return origin is _t_annotated or origin is _te_annotated
51
52
53def annotated_type(tp: Any, /) -> Any | None:
54 """Return the type of the `Annotated` special form, or `None`."""
55 return tp.__origin__ if typing_objects.is_annotated(get_origin(tp)) else None
56
57
58def unpack_type(tp: Any, /) -> Any | None:
59 """Return the type wrapped by the `Unpack` special form, or `None`."""
60 return get_args(tp)[0] if typing_objects.is_unpack(get_origin(tp)) else None
61
62
63_classvar_re = re.compile(r'((\w+\.)?Annotated\[)?(\w+\.)?ClassVar\[')
64
65
66def is_classvar_annotation(tp: Any, /) -> bool:
67 """Return whether the provided argument represents a class variable annotation.
68
69 Although not explicitly stated by the typing specification, `ClassVar` can be used
70 inside `Annotated` and as such, this function checks for this specific scenario.
71
72 Because this function is used to detect class variables before evaluating forward references
73 (or because evaluation failed), we also implement a naive regex match implementation. This is
74 required because class variables are inspected before fields are collected, so we try to be
75 as accurate as possible.
76 """
77 if typing_objects.is_classvar(tp):
78 return True
79
80 origin = get_origin(tp)
81
82 if typing_objects.is_classvar(origin):
83 return True
84
85 if typing_objects.is_annotated(origin):
86 annotated_type = tp.__origin__
87 if typing_objects.is_classvar(annotated_type) or typing_objects.is_classvar(get_origin(annotated_type)):
88 return True
89
90 str_ann: str | None = None
91 if isinstance(tp, typing.ForwardRef):
92 str_ann = tp.__forward_arg__
93 if isinstance(tp, str):
94 str_ann = tp
95
96 if str_ann is not None and _classvar_re.match(str_ann):
97 # stdlib dataclasses do something similar, although a bit more advanced
98 # (see `dataclass._is_type`).
99 return True
100
101 return False
102
103
104def is_namedtuple(tp: Any, /) -> bool:
105 """Return whether the provided argument is a named tuple class.
106
107 The class can be created using `typing.NamedTuple` or `collections.namedtuple`.
108 Parametrized generic classes are *not* assumed to be named tuples.
109 """
110 from ._utils import lenient_issubclass # circ. import
111
112 return lenient_issubclass(tp, tuple) and hasattr(tp, '_fields')
113
114
115# TODO In 2.12, delete this export. It is currently defined only to not break
116# pydantic-settings which relies on it:
117origin_is_union = is_union_origin
118
119
120def is_generic_alias(tp: Any, /) -> bool:
121 return isinstance(tp, (types.GenericAlias, typing._GenericAlias)) # pyright: ignore[reportAttributeAccessIssue]
122
123
124# TODO: Ideally, we should avoid relying on the private `typing` constructs:
125WithArgsTypes: tuple[Any, ...] = (typing._GenericAlias, types.GenericAlias, types.UnionType) # pyright: ignore[reportAttributeAccessIssue]
126
127# Similarly, we shouldn't rely on this `_Final` class, which is even more private than `_GenericAlias`:
128typing_base: Any = typing._Final # pyright: ignore[reportAttributeAccessIssue]
129
130
131### Annotation evaluations functions:
132
133
134def parent_frame_namespace(*, parent_depth: int = 2, force: bool = False) -> MutableMapping[str, Any] | None:
135 """Fetch the local namespace of the parent frame where this function is called.
136
137 Using this function is mostly useful to resolve forward annotations pointing to members defined in a local namespace,
138 such as assignments inside a function. Using the standard library tools, it is currently not possible to resolve
139 such annotations:
140
141 ```python {lint="skip" test="skip"}
142 from typing import get_type_hints
143
144 def func() -> None:
145 Alias = int
146
147 class C:
148 a: 'Alias'
149
150 # Raises a `NameError: 'Alias' is not defined`
151 get_type_hints(C)
152 ```
153
154 Pydantic uses this function when a Pydantic model is being defined to fetch the parent frame locals. However,
155 this only allows us to fetch the parent frame namespace and not other parents (e.g. a model defined in a function,
156 itself defined in another function). Inspecting the next outer frames (using `f_back`) is not reliable enough
157 (see https://discuss.python.org/t/20659).
158
159 Because this function is mostly used to better resolve forward annotations, nothing is returned if the parent frame's
160 code object is defined at the module level. In this case, the locals of the frame will be the same as the module
161 globals where the class is defined (see `_namespace_utils.get_module_ns_of`). However, if you still want to fetch
162 the module globals (e.g. when rebuilding a model, where the frame where the rebuild call is performed might contain
163 members that you want to use for forward annotations evaluation), you can use the `force` parameter.
164
165 Args:
166 parent_depth: The depth at which to get the frame. Defaults to 2, meaning the parent frame where this function
167 is called will be used.
168 force: Whether to always return the frame locals, even if the frame's code object is defined at the module level.
169
170 Returns:
171 The locals of the namespace, or `None` if it was skipped as per the described logic.
172 """
173 frame = sys._getframe(parent_depth)
174
175 if frame.f_code.co_name.startswith('<generic parameters of'):
176 # As `parent_frame_namespace` is mostly called in `ModelMetaclass.__new__`,
177 # the parent frame can be the annotation scope if the PEP 695 generic syntax is used.
178 # (see https://docs.python.org/3/reference/executionmodel.html#annotation-scopes,
179 # https://docs.python.org/3/reference/compound_stmts.html#generic-classes).
180 # In this case, the code name is set to `<generic parameters of MyClass>`,
181 # and we need to skip this frame as it is irrelevant.
182 frame = cast(types.FrameType, frame.f_back) # guaranteed to not be `None`
183
184 # note, we don't copy frame.f_locals here (or during the last return call), because we don't expect the namespace to be
185 # modified down the line if this becomes a problem, we could implement some sort of frozen mapping structure to enforce this.
186 if force:
187 return frame.f_locals
188
189 # If either of the following conditions are true, the class is defined at the top module level.
190 # To better understand why we need both of these checks, see
191 # https://github.com/pydantic/pydantic/pull/10113#discussion_r1714981531.
192 if frame.f_back is None or frame.f_code.co_name == '<module>':
193 return None
194
195 return frame.f_locals
196
197
198def _type_convert(arg: Any) -> Any:
199 """Convert `None` to `NoneType` and strings to `ForwardRef` instances.
200
201 This is a vendored version of the private `typing._type_convert()` function. When
202 evaluating a type, `ForwardRef._evaluate()` ends up being called, and is
203 responsible for making this conversion. However, we still have to apply
204 it for the first argument passed to our type evaluation functions, similarly
205 to the `typing.get_type_hints()` function.
206 """
207 if arg is None:
208 return NoneType
209 if isinstance(arg, str):
210 if sys.version_info[:3] == (3, 10, 0):
211 return ForwardRef(arg, is_argument=False)
212 else:
213 # Like `typing.get_type_hints()`, assume the arg can be in any context,
214 # hence the proper `is_argument` and `is_class` args:
215 return ForwardRef(arg, is_argument=False, is_class=True)
216 return arg
217
218
219def safe_get_annotations(obj: Any) -> dict[str, Any]:
220 """Get the annotations for the provided object, accounting for potential deferred forward references.
221
222 Starting with Python 3.14, accessing the `__annotations__` attribute might raise a `NameError` if
223 a referenced symbol isn't defined yet. In this case, we return the annotation in the *forward ref*
224 format.
225 """
226 if sys.version_info >= (3, 14):
227 return annotationlib.get_annotations(obj, format=annotationlib.Format.FORWARDREF)
228 else:
229 if isinstance(obj, type):
230 return obj.__dict__.get('__annotations__', {})
231 else:
232 return getattr(obj, '__annotations__', {})
233
234
235def get_model_type_hints(
236 model_class: type[BaseModel],
237 *,
238 ns_resolver: NsResolver | None = None,
239) -> dict[str, tuple[Any, bool]]:
240 """Collect annotations from a Pydantic model class, including those from parent classes.
241
242 Args:
243 model_class: The Pydantic model class to inspect.
244 ns_resolver: A namespace resolver instance to use. Defaults to an empty instance.
245
246 Returns:
247 A dictionary mapping annotation names to a two-tuple: the first element is the evaluated
248 type or the original annotation if a `NameError` occurred, the second element is a boolean
249 indicating if whether the evaluation succeeded.
250 """
251 hints: dict[str, Any] | dict[str, tuple[Any, bool]] = {}
252 ns_resolver = ns_resolver or NsResolver()
253
254 for base in reversed(model_class.__mro__[:-1]):
255 # For Python 3.14, we could also use `Format.VALUE` and pass the globals/locals
256 # from the ns_resolver, but we want to be able to know which specific field failed
257 # to evaluate:
258 ann = safe_get_annotations(base)
259
260 if not ann:
261 continue
262
263 with ns_resolver.push(base):
264 base_model_fields: dict[str, FieldInfo] | None = base.__dict__.get('__pydantic_fields__')
265
266 for name, value in ann.items():
267 if name.startswith('_'):
268 globalns, localns = ns_resolver.types_namespace
269
270 # For private attributes, we only need the annotation to detect the `ClassVar` special form.
271 # For this reason, we still try to evaluate it, but we also catch any possible exception (on
272 # top of the `NameError`s caught in `try_eval_type`) that could happen so that users are free
273 # to use any kind of forward annotation for private fields (e.g. circular imports, new typing
274 # syntax, etc).
275 try:
276 hints[name] = try_eval_type(value, globalns, localns)
277 except Exception:
278 hints[name] = (value, False)
279 elif name == 'model_config':
280 # `model_config` is never a field, and `collect_model_fields()` ignores its annotation,
281 # so there is no need to evaluate it:
282 hints[name] = (value, False)
283 else:
284 if base_model_fields is not None and name in base_model_fields:
285 # Avoid unnecessarily evaluating annotations from parent models, as we'll end up
286 # copying the `FieldInfo` instance from it anyway if we need to.
287 # We use the `annotation` attribute here, but in reality could put anything here,
288 # As we are guaranteed to not make use of it:
289 hints[name] = (base_model_fields[name].annotation, True)
290 else:
291 globalns, localns = ns_resolver.types_namespace
292
293 hints[name] = try_eval_type(value, globalns, localns)
294
295 return hints
296
297
298def get_cls_type_hints(
299 obj: type[Any],
300 *,
301 ns_resolver: NsResolver | None = None,
302) -> dict[str, Any]:
303 """Collect annotations from a class, including those from parent classes.
304
305 Args:
306 obj: The class to inspect.
307 ns_resolver: A namespace resolver instance to use. Defaults to an empty instance.
308 """
309 hints: dict[str, Any] = {}
310 ns_resolver = ns_resolver or NsResolver()
311
312 for base in reversed(obj.__mro__):
313 # For Python 3.14, we could also use `Format.VALUE` and pass the globals/locals
314 # from the ns_resolver, but we want to be able to know which specific field failed
315 # to evaluate:
316 ann = safe_get_annotations(base)
317
318 if not ann:
319 continue
320
321 with ns_resolver.push(base):
322 globalns, localns = ns_resolver.types_namespace
323 for name, value in ann.items():
324 hints[name] = eval_type(value, globalns, localns)
325 return hints
326
327
328def try_eval_type(
329 value: Any,
330 globalns: GlobalsNamespace | None = None,
331 localns: MappingNamespace | None = None,
332) -> tuple[Any, bool]:
333 """Try evaluating the annotation using the provided namespaces.
334
335 Args:
336 value: The value to evaluate. If `None`, it will be replaced by `type[None]`. If an instance
337 of `str`, it will be converted to a `ForwardRef`.
338 localns: The global namespace to use during annotation evaluation.
339 globalns: The local namespace to use during annotation evaluation.
340
341 Returns:
342 A two-tuple containing the possibly evaluated type and a boolean indicating
343 whether the evaluation succeeded or not.
344 """
345 try:
346 return eval_type(value, globalns, localns), True
347 except NameError:
348 return _type_convert(value), False
349
350
351def eval_type(
352 value: Any,
353 globalns: GlobalsNamespace | None = None,
354 localns: MappingNamespace | None = None,
355 type_params: tuple[Any, ...] | None = None,
356) -> Any:
357 """Evaluate the annotation using the provided namespaces.
358
359 This function relies on our vendored `typing._eval_type()` function, and provide better error messages
360 if possible.
361
362 Args:
363 value: The value to evaluate (will be converted to `NoneType` if `None`, and to a `ForwardRef` instance if a string).
364 localns: The global namespace to use during annotation evaluation.
365 globalns: The local namespace to use during annotation evaluation.
366 type_params: The type params belonging to the object being evaluated.
367 """
368 value = _type_convert(value)
369 try:
370 return _eval_type(value, globalns, localns, type_params)
371 except ImportError as e:
372 # Starting in Python 3.15, resolving a lazy import during evaluation can fail, typically because of a
373 # circular import between modules (an `ImportError` can also happen without lazy imports but this is
374 # way less likely). Report it as a `NameError` so that callers treat the annotation as not resolvable
375 # yet, exactly an undefined forward reference.
376 if sys.version_info >= (3, 12):
377 name = e.name_from if e.name_from is not None else e.name
378 else:
379 name = e.name
380 raise NameError(f'Unable to resolve the import of {name!r}: {e}', name=name) from e
381 except TypeError as e:
382 if 'Unable to evaluate type annotation' in str(e):
383 raise
384
385 # In most cases, value is a `ForwardRef`, otherwise the `TypeError` would have been raised when the annotation
386 # was defined (at least on Python < 3.14). However, in rare cases, the `TypeError` can also appear when evaluating nested
387 # parts of the annotation (e.g. `list["1 + 'a'"]`).
388 if isinstance(value, ForwardRef):
389 message = f'Unable to evaluate type annotation {value.__forward_arg__!r}.'
390 else:
391 message = f'Unable to evaluate type annotation {value!r}.'
392 if sys.version_info >= (3, 11):
393 e.add_note(message)
394 raise
395 else:
396 raise TypeError(message) from e
397 except RecursionError as e:
398 message = (
399 "If you made use of an implicit recursive type alias (e.g. `MyType = list['MyType']), "
400 'consider using PEP 695 type aliases instead. For more details, refer to the documentation: '
401 f'https://pydantic.dev/docs/validation/{version_short()}/concepts/types/#named-recursive-types'
402 )
403 if sys.version_info >= (3, 11):
404 e.add_note(message)
405 raise
406 else:
407 raise RecursionError(f'{e.args[0]}\n{message}')
408
409
410@deprecated(
411 '`eval_type_lenient()` is deprecated, use `try_eval_type()` instead.',
412 category=None,
413)
414def eval_type_lenient(
415 value: Any,
416 globalns: GlobalsNamespace | None = None,
417 localns: MappingNamespace | None = None,
418) -> Any: # pragma: no cover
419 ev, _ = try_eval_type(value, globalns, localns)
420 return ev
421
422
423def _eval_type(
424 value: Any,
425 globalns: GlobalsNamespace | None = None,
426 localns: MappingNamespace | None = None,
427 type_params: tuple[Any, ...] | None = None,
428) -> Any:
429 """Evaluate all forward references for a given type.
430
431 This is a vendored version of the private `typing._eval_type()` function, adapted to work for all supported Python versions.
432 """
433 if sys.version_info >= (3, 14):
434 # Starting in 3.14, `_eval_type()` does *not* apply `_type_convert()`
435 # anymore. This means the `None` -> `NoneType` conversion does not apply:
436 evaluated = typing._eval_type( # type: ignore
437 value,
438 globalns,
439 localns,
440 type_params=type_params,
441 # This is relevant when evaluating types from `TypedDict` classes, where string annotations
442 # are automatically converted to `ForwardRef` instances with a module set. In this case,
443 # Our `globalns` is irrelevant and we need to indicate `typing._eval_type()` that it should
444 # infer it from the `ForwardRef.__forward_module__` attribute instead (`typing.get_type_hints()`
445 # does the same). Note that this would probably be unnecessary if we properly iterated over the
446 # `__orig_bases__` for TypedDicts in `get_cls_type_hints()`:
447 prefer_fwd_module=True,
448 )
449 if evaluated is None:
450 evaluated = NoneType
451 return evaluated
452 elif sys.version_info >= (3, 13):
453 return typing._eval_type( # type: ignore
454 value, globalns, localns, type_params=type_params
455 )
456 else:
457 return typing._eval_type( # type: ignore
458 value, globalns, localns
459 )
460
461
462def signature_no_eval(f: Callable[..., Any]) -> Signature:
463 """Get the signature of a callable without evaluating any annotations."""
464 if sys.version_info >= (3, 14):
465 from annotationlib import Format
466
467 return signature(f, annotation_format=Format.FORWARDREF)
468 else:
469 return signature(f)
470
471
472def get_function_type_hints(
473 function: Callable[..., Any],
474 *,
475 include_keys: set[str] | None = None,
476 globalns: GlobalsNamespace | None = None,
477 localns: MappingNamespace | None = None,
478) -> dict[str, Any]:
479 """Return type hints for a function.
480
481 This is similar to the `typing.get_type_hints` function, with a few differences:
482 - Support `functools.partial` by using the underlying `func` attribute.
483 - Do not wrap type annotation of a parameter with `Optional` if it has a default value of `None`
484 (related bug: https://github.com/python/cpython/issues/90353, only fixed in 3.11+).
485 """
486 if isinstance(function, partial):
487 annotations = safe_get_annotations(function.func)
488 else:
489 annotations = safe_get_annotations(function)
490
491 if globalns is None:
492 globalns = get_module_ns_of(function)
493 type_params: tuple[Any, ...] | None = None
494 if localns is None:
495 # If localns was specified, it is assumed to already contain type params. This is because
496 # Pydantic has more advanced logic to do so (see `_namespace_utils.ns_for_function`).
497 type_params = getattr(function, '__type_params__', ())
498
499 type_hints = {}
500 for name, value in annotations.items():
501 if include_keys is not None and name not in include_keys:
502 continue
503
504 type_hints[name] = eval_type(value, globalns, localns, type_params)
505
506 return type_hints