_namespace_utils.py 13 KB

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