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