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 else:
280 if base_model_fields is not None and name in base_model_fields:
281 # Avoid unnecessarily evaluating annotations from parent models, as we'll end up
282 # copying the `FieldInfo` instance from it anyway if we need to.
283 # We use the `annotation` attribute here, but in reality could put anything here,
284 # As we are guaranteed to not make use of it:
285 hints[name] = (base_model_fields[name].annotation, True)
286 else:
287 globalns, localns = ns_resolver.types_namespace
288
289 hints[name] = try_eval_type(value, globalns, localns)
290
291 return hints
292
293
294def get_cls_type_hints(
295 obj: type[Any],
296 *,
297 ns_resolver: NsResolver | None = None,
298) -> dict[str, Any]:
299 """Collect annotations from a class, including those from parent classes.
300
301 Args:
302 obj: The class to inspect.
303 ns_resolver: A namespace resolver instance to use. Defaults to an empty instance.
304 """
305 hints: dict[str, Any] = {}
306 ns_resolver = ns_resolver or NsResolver()
307
308 for base in reversed(obj.__mro__):
309 # For Python 3.14, we could also use `Format.VALUE` and pass the globals/locals
310 # from the ns_resolver, but we want to be able to know which specific field failed
311 # to evaluate:
312 ann = safe_get_annotations(base)
313
314 if not ann:
315 continue
316
317 with ns_resolver.push(base):
318 globalns, localns = ns_resolver.types_namespace
319 for name, value in ann.items():
320 hints[name] = eval_type(value, globalns, localns)
321 return hints
322
323
324def try_eval_type(
325 value: Any,
326 globalns: GlobalsNamespace | None = None,
327 localns: MappingNamespace | None = None,
328) -> tuple[Any, bool]:
329 """Try evaluating the annotation using the provided namespaces.
330
331 Args:
332 value: The value to evaluate. If `None`, it will be replaced by `type[None]`. If an instance
333 of `str`, it will be converted to a `ForwardRef`.
334 localns: The global namespace to use during annotation evaluation.
335 globalns: The local namespace to use during annotation evaluation.
336
337 Returns:
338 A two-tuple containing the possibly evaluated type and a boolean indicating
339 whether the evaluation succeeded or not.
340 """
341 try:
342 return eval_type(value, globalns, localns), True
343 except NameError:
344 return _type_convert(value), False
345
346
347def eval_type(
348 value: Any,
349 globalns: GlobalsNamespace | None = None,
350 localns: MappingNamespace | None = None,
351 type_params: tuple[Any, ...] | None = None,
352) -> Any:
353 """Evaluate the annotation using the provided namespaces.
354
355 This function relies on our vendored `typing._eval_type()` function, and provide better error messages
356 if possible.
357
358 Args:
359 value: The value to evaluate (will be converted to `NoneType` if `None`, and to a `ForwardRef` instance if a string).
360 localns: The global namespace to use during annotation evaluation.
361 globalns: The local namespace to use during annotation evaluation.
362 type_params: The type params belonging to the object being evaluated.
363 """
364 value = _type_convert(value)
365 try:
366 return _eval_type(value, globalns, localns, type_params)
367 except ImportError as e:
368 # Starting in Python 3.15, resolving a lazy import during evaluation can fail, typically because of a
369 # circular import between modules (an `ImportError` can also happen without lazy imports but this is
370 # way less likely). Report it as a `NameError` so that callers treat the annotation as not resolvable
371 # yet, exactly an undefined forward reference.
372 if sys.version_info >= (3, 12):
373 name = e.name_from if e.name_from is not None else e.name
374 else:
375 name = e.name
376 raise NameError(f'Unable to resolve the import of {name!r}: {e}', name=name) from e
377 except TypeError as e:
378 if 'Unable to evaluate type annotation' in str(e):
379 raise
380
381 # In most cases, value is a `ForwardRef`, otherwise the `TypeError` would have been raised when the annotation
382 # was defined (at least on Python < 3.14). However, in rare cases, the `TypeError` can also appear when evaluating nested
383 # parts of the annotation (e.g. `list["1 + 'a'"]`).
384 if isinstance(value, ForwardRef):
385 message = f'Unable to evaluate type annotation {value.__forward_arg__!r}.'
386 else:
387 message = f'Unable to evaluate type annotation {value!r}.'
388 if sys.version_info >= (3, 11):
389 e.add_note(message)
390 raise
391 else:
392 raise TypeError(message) from e
393 except RecursionError as e:
394 message = (
395 "If you made use of an implicit recursive type alias (e.g. `MyType = list['MyType']), "
396 'consider using PEP 695 type aliases instead. For more details, refer to the documentation: '
397 f'https://pydantic.dev/docs/validation/{version_short()}/concepts/types/#named-recursive-types'
398 )
399 if sys.version_info >= (3, 11):
400 e.add_note(message)
401 raise
402 else:
403 raise RecursionError(f'{e.args[0]}\n{message}')
404
405
406@deprecated(
407 '`eval_type_lenient()` is deprecated, use `try_eval_type()` instead.',
408 category=None,
409)
410def eval_type_lenient(
411 value: Any,
412 globalns: GlobalsNamespace | None = None,
413 localns: MappingNamespace | None = None,
414) -> Any: # pragma: no cover
415 ev, _ = try_eval_type(value, globalns, localns)
416 return ev
417
418
419def _eval_type(
420 value: Any,
421 globalns: GlobalsNamespace | None = None,
422 localns: MappingNamespace | None = None,
423 type_params: tuple[Any, ...] | None = None,
424) -> Any:
425 """Evaluate all forward references for a given type.
426
427 This is a vendored version of the private `typing._eval_type()` function, adapted to work for all supported Python versions.
428 """
429 if sys.version_info >= (3, 14):
430 # Starting in 3.14, `_eval_type()` does *not* apply `_type_convert()`
431 # anymore. This means the `None` -> `NoneType` conversion does not apply:
432 evaluated = typing._eval_type( # type: ignore
433 value,
434 globalns,
435 localns,
436 type_params=type_params,
437 # This is relevant when evaluating types from `TypedDict` classes, where string annotations
438 # are automatically converted to `ForwardRef` instances with a module set. In this case,
439 # Our `globalns` is irrelevant and we need to indicate `typing._eval_type()` that it should
440 # infer it from the `ForwardRef.__forward_module__` attribute instead (`typing.get_type_hints()`
441 # does the same). Note that this would probably be unnecessary if we properly iterated over the
442 # `__orig_bases__` for TypedDicts in `get_cls_type_hints()`:
443 prefer_fwd_module=True,
444 )
445 if evaluated is None:
446 evaluated = NoneType
447 return evaluated
448 elif sys.version_info >= (3, 13):
449 return typing._eval_type( # type: ignore
450 value, globalns, localns, type_params=type_params
451 )
452 else:
453 return typing._eval_type( # type: ignore
454 value, globalns, localns
455 )
456
457
458def signature_no_eval(f: Callable[..., Any]) -> Signature:
459 """Get the signature of a callable without evaluating any annotations."""
460 if sys.version_info >= (3, 14):
461 from annotationlib import Format
462
463 return signature(f, annotation_format=Format.FORWARDREF)
464 else:
465 return signature(f)
466
467
468def get_function_type_hints(
469 function: Callable[..., Any],
470 *,
471 include_keys: set[str] | None = None,
472 globalns: GlobalsNamespace | None = None,
473 localns: MappingNamespace | None = None,
474) -> dict[str, Any]:
475 """Return type hints for a function.
476
477 This is similar to the `typing.get_type_hints` function, with a few differences:
478 - Support `functools.partial` by using the underlying `func` attribute.
479 - Do not wrap type annotation of a parameter with `Optional` if it has a default value of `None`
480 (related bug: https://github.com/python/cpython/issues/90353, only fixed in 3.11+).
481 """
482 if isinstance(function, partial):
483 annotations = safe_get_annotations(function.func)
484 else:
485 annotations = safe_get_annotations(function)
486
487 if globalns is None:
488 globalns = get_module_ns_of(function)
489 type_params: tuple[Any, ...] | None = None
490 if localns is None:
491 # If localns was specified, it is assumed to already contain type params. This is because
492 # Pydantic has more advanced logic to do so (see `_namespace_utils.ns_for_function`).
493 type_params = getattr(function, '__type_params__', ())
494
495 type_hints = {}
496 for name, value in annotations.items():
497 if include_keys is not None and name not in include_keys:
498 continue
499
500 type_hints[name] = eval_type(value, globalns, localns, type_params)
501
502 return type_hints