1from __future__ import annotations
2
3import sys
4from collections.abc import Callable, Generator, Iterator, Mapping
5from contextlib import contextmanager
6from functools import cached_property
7from types import MappingProxyType
8from typing import Any, NamedTuple, TypeAlias, TypeVar
9
10from typing_extensions import ParamSpec, TypeAliasType, TypeVarTuple
11
12GlobalsNamespace: TypeAlias = 'dict[str, Any]'
13"""A global namespace.
14
15In most cases, this is a reference to the `__dict__` attribute of a module.
16This namespace type is expected as the `globals` argument during annotations evaluation.
17"""
18
19MappingNamespace: TypeAlias = Mapping[str, Any]
20"""Any kind of namespace.
21
22In most cases, this is a local namespace (e.g. the `__dict__` attribute of a class,
23the [`f_locals`][frame.f_locals] attribute of a frame object, when dealing with types
24defined inside functions).
25This namespace type is expected as the `locals` argument during annotations evaluation.
26"""
27
28_TypeVarLike: TypeAlias = 'TypeVar | ParamSpec | TypeVarTuple'
29
30
31class NamespacesTuple(NamedTuple):
32 """A tuple of globals and locals to be used during annotations evaluation.
33
34 This datastructure is defined as a named tuple so that it can easily be unpacked:
35
36 ```python {lint="skip" test="skip"}
37 def eval_type(typ: type[Any], ns: NamespacesTuple) -> None:
38 return eval(typ, *ns)
39 ```
40 """
41
42 globals: GlobalsNamespace
43 """The namespace to be used as the `globals` argument during annotations evaluation."""
44
45 locals: MappingNamespace
46 """The namespace to be used as the `locals` argument during annotations evaluation."""
47
48
49def get_module_ns_of(obj: Any) -> dict[str, Any]:
50 """Get the namespace of the module where the object is defined.
51
52 Caution: this function does not return a copy of the module namespace, so the result
53 should not be mutated. The burden of enforcing this is on the caller.
54 """
55 module_name = getattr(obj, '__module__', None)
56 if module_name:
57 try:
58 return sys.modules[module_name].__dict__
59 except KeyError:
60 # happens occasionally, see https://github.com/pydantic/pydantic/issues/2363
61 return {}
62 return {}
63
64
65# Note that this class is almost identical to `collections.ChainMap`, but need to enforce
66# immutable mappings here:
67class LazyLocalNamespace(Mapping[str, Any]):
68 """A lazily evaluated mapping, to be used as the `locals` argument during annotations evaluation.
69
70 While the [`eval`][eval] function expects a mapping as the `locals` argument, it only
71 performs `__getitem__` calls. The [`Mapping`][collections.abc.Mapping] abstract base class
72 is fully implemented only for type checking purposes.
73
74 Args:
75 *namespaces: The namespaces to consider, in ascending order of priority.
76
77 Example:
78 ```python {lint="skip" test="skip"}
79 ns = LazyLocalNamespace({'a': 1, 'b': 2}, {'a': 3})
80 ns['a']
81 #> 3
82 ns['b']
83 #> 2
84 ```
85 """
86
87 def __init__(self, *namespaces: MappingNamespace) -> None:
88 self._namespaces = namespaces
89
90 @cached_property
91 def data(self) -> dict[str, Any]:
92 # In rare scenarios, a race condition can happen when iterating over class namespaces (i.e. created with `var(SomeClass)`),
93 # if another thread accesses the type annotations for the first time (since 3.14, reading annotations can set attributes
94 # such as `__annotations_cache__` or `__annotate_func__` to the type).
95 return {
96 k: v for ns in self._namespaces for k, v in (ns.copy() if isinstance(ns, MappingProxyType) else ns).items()
97 }
98
99 def __len__(self) -> int:
100 return len(self.data)
101
102 def __getitem__(self, key: str) -> Any:
103 return self.data[key]
104
105 def __contains__(self, key: object) -> bool:
106 return key in self.data
107
108 def __iter__(self) -> Iterator[str]:
109 return iter(self.data)
110
111
112def ns_for_function(obj: Callable[..., Any], parent_namespace: MappingNamespace | None = None) -> NamespacesTuple:
113 """Return the global and local namespaces to be used when evaluating annotations for the provided function.
114
115 The global namespace will be the `__dict__` attribute of the module the function was defined in.
116 The local namespace will contain the `__type_params__` introduced by PEP 695.
117
118 Args:
119 obj: The object to use when building namespaces.
120 parent_namespace: Optional namespace to be added with the lowest priority in the local namespace.
121 If the passed function is a method, the `parent_namespace` will be the namespace of the class
122 the method is defined in. Thus, we also fetch type `__type_params__` from there (i.e. the
123 class-scoped type variables).
124 """
125 locals_list: list[MappingNamespace] = []
126 if parent_namespace is not None:
127 locals_list.append(parent_namespace)
128
129 # Get the `__type_params__` attribute introduced by PEP 695.
130 # Note that the `typing._eval_type` function expects type params to be
131 # passed as a separate argument. However, internally, `_eval_type` calls
132 # `ForwardRef._evaluate` which will merge type params with the localns,
133 # essentially mimicking what we do here.
134 type_params: tuple[_TypeVarLike, ...] = getattr(obj, '__type_params__', ())
135 if parent_namespace is not None:
136 # We also fetch type params from the parent namespace. If present, it probably
137 # means the function was defined in a class. This is to support the following:
138 # https://github.com/python/cpython/issues/124089.
139 type_params += parent_namespace.get('__type_params__', ())
140
141 locals_list.append({t.__name__: t for t in type_params})
142
143 # What about short-circuiting to `obj.__globals__`?
144 globalns = get_module_ns_of(obj)
145
146 return NamespacesTuple(globalns, LazyLocalNamespace(*locals_list))
147
148
149class NsResolver:
150 """A class responsible for the namespaces resolving logic for annotations evaluation.
151
152 This class handles the namespace logic when evaluating annotations mainly for class objects.
153
154 It holds a stack of classes that are being inspected during the core schema building,
155 and the `types_namespace` property exposes the globals and locals to be used for
156 type annotation evaluation. Additionally -- if no class is present in the stack -- a
157 fallback globals and locals can be provided using the `namespaces_tuple` argument
158 (this is useful when generating a schema for a simple annotation, e.g. when using
159 `TypeAdapter`).
160
161 The namespace creation logic is unfortunately flawed in some cases, for backwards
162 compatibility reasons and to better support valid edge cases. See the description
163 for the `parent_namespace` argument and the example for more details.
164
165 Args:
166 namespaces_tuple: The default globals and locals to use if no class is present
167 on the stack. This can be useful when using the `GenerateSchema` class
168 with `TypeAdapter`, where the "type" being analyzed is a simple annotation.
169 parent_namespace: An optional parent namespace that will be added to the locals
170 with the lowest priority. For a given class defined in a function, the locals
171 of this function are usually used as the parent namespace:
172
173 ```python {lint="skip" test="skip"}
174 from pydantic import BaseModel
175
176 def func() -> None:
177 SomeType = int
178
179 class Model(BaseModel):
180 f: 'SomeType'
181
182 # when collecting fields, an namespace resolver instance will be created
183 # this way:
184 # ns_resolver = NsResolver(parent_namespace={'SomeType': SomeType})
185 ```
186
187 For backwards compatibility reasons and to support valid edge cases, this parent
188 namespace will be used for *every* type being pushed to the stack. In the future,
189 we might want to be smarter by only doing so when the type being pushed is defined
190 in the same module as the parent namespace.
191
192 Example:
193 ```python {lint="skip" test="skip"}
194 ns_resolver = NsResolver(
195 parent_namespace={'fallback': 1},
196 )
197
198 class Sub:
199 m: 'Model'
200
201 class Model:
202 some_local = 1
203 sub: Sub
204
205 ns_resolver = NsResolver()
206
207 # This is roughly what happens when we build a core schema for `Model`:
208 with ns_resolver.push(Model):
209 ns_resolver.types_namespace
210 #> NamespacesTuple({'Sub': Sub}, {'Model': Model, 'some_local': 1})
211 # First thing to notice here, the model being pushed is added to the locals.
212 # Because `NsResolver` is being used during the model definition, it is not
213 # yet added to the globals. This is useful when resolving self-referencing annotations.
214
215 with ns_resolver.push(Sub):
216 ns_resolver.types_namespace
217 #> NamespacesTuple({'Sub': Sub}, {'Sub': Sub, 'Model': Model})
218 # Second thing to notice: `Sub` is present in both the globals and locals.
219 # This is not an issue, just that as described above, the model being pushed
220 # is added to the locals, but it happens to be present in the globals as well
221 # because it is already defined.
222 # Third thing to notice: `Model` is also added in locals. This is a backwards
223 # compatibility workaround that allows for `Sub` to be able to resolve `'Model'`
224 # correctly (as otherwise models would have to be rebuilt even though this
225 # doesn't look necessary).
226 ```
227 """
228
229 def __init__(
230 self,
231 namespaces_tuple: NamespacesTuple | None = None,
232 parent_namespace: MappingNamespace | None = None,
233 ) -> None:
234 self._base_ns_tuple = namespaces_tuple or NamespacesTuple({}, {})
235 self._parent_ns = parent_namespace
236 self._types_stack: list[type[Any] | TypeAliasType] = []
237
238 @cached_property
239 def types_namespace(self) -> NamespacesTuple:
240 """The current global and local namespaces to be used for annotations evaluation."""
241 if not self._types_stack:
242 # TODO: should we merge the parent namespace here?
243 # This is relevant for TypeAdapter, where there are no types on the stack, and we might
244 # need access to the parent_ns. Right now, we sidestep this in `type_adapter.py` by passing
245 # locals to both parent_ns and the base_ns_tuple, but this is a bit hacky.
246 # we might consider something like:
247 # if self._parent_ns is not None:
248 # # Hacky workarounds, see class docstring:
249 # # An optional parent namespace that will be added to the locals with the lowest priority
250 # locals_list: list[MappingNamespace] = [self._parent_ns, self._base_ns_tuple.locals]
251 # return NamespacesTuple(self._base_ns_tuple.globals, LazyLocalNamespace(*locals_list))
252 return self._base_ns_tuple
253
254 typ = self._types_stack[-1]
255
256 globalns = get_module_ns_of(typ)
257
258 locals_list: list[MappingNamespace] = []
259 # Hacky workarounds, see class docstring:
260 # An optional parent namespace that will be added to the locals with the lowest priority
261 if self._parent_ns is not None:
262 locals_list.append(self._parent_ns)
263 if len(self._types_stack) > 1:
264 first_type = self._types_stack[0]
265 locals_list.append({first_type.__name__: first_type})
266
267 # Adding `__type_params__` *before* `vars(typ)`, as the latter takes priority
268 # (see https://github.com/python/cpython/pull/120272).
269 # TODO `typ.__type_params__` when we drop support for Python 3.11:
270 type_params: tuple[_TypeVarLike, ...] = getattr(typ, '__type_params__', ())
271 if type_params:
272 # Adding `__type_params__` is mostly useful for generic classes defined using
273 # PEP 695 syntax *and* using forward annotations (see the example in
274 # https://github.com/python/cpython/issues/114053). For TypeAliasType instances,
275 # it is way less common, but still required if using a string annotation in the alias
276 # value, e.g. `type A[T] = 'T'` (which is not necessary in most cases).
277 locals_list.append({t.__name__: t for t in type_params})
278
279 # TypeAliasType instances don't have a `__dict__` attribute, so the check
280 # is necessary:
281 if hasattr(typ, '__dict__'):
282 locals_list.append(vars(typ))
283
284 # The `len(self._types_stack) > 1` check above prevents this from being added twice:
285 locals_list.append({typ.__name__: typ})
286
287 return NamespacesTuple(globalns, LazyLocalNamespace(*locals_list))
288
289 @contextmanager
290 def push(self, typ: type[Any] | TypeAliasType, /) -> Generator[None]:
291 """Push a type to the stack."""
292 self._types_stack.append(typ)
293 # Reset the cached property:
294 self.__dict__.pop('types_namespace', None)
295 try:
296 yield
297 finally:
298 self._types_stack.pop()
299 self.__dict__.pop('types_namespace', None)