diff options
Diffstat (limited to '.venv/lib/python3.12/site-packages/pydantic/_internal/_namespace_utils.py')
-rw-r--r-- | .venv/lib/python3.12/site-packages/pydantic/_internal/_namespace_utils.py | 284 |
1 files changed, 284 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/pydantic/_internal/_namespace_utils.py b/.venv/lib/python3.12/site-packages/pydantic/_internal/_namespace_utils.py new file mode 100644 index 00000000..799c4c4e --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pydantic/_internal/_namespace_utils.py @@ -0,0 +1,284 @@ +from __future__ import annotations + +import sys +from collections.abc import Generator +from contextlib import contextmanager +from functools import cached_property +from typing import Any, Callable, Iterator, Mapping, NamedTuple, TypeVar + +from typing_extensions import ParamSpec, TypeAlias, TypeAliasType, TypeVarTuple + +GlobalsNamespace: TypeAlias = 'dict[str, Any]' +"""A global namespace. + +In most cases, this is a reference to the `__dict__` attribute of a module. +This namespace type is expected as the `globals` argument during annotations evaluation. +""" + +MappingNamespace: TypeAlias = Mapping[str, Any] +"""Any kind of namespace. + +In most cases, this is a local namespace (e.g. the `__dict__` attribute of a class, +the [`f_locals`][frame.f_locals] attribute of a frame object, when dealing with types +defined inside functions). +This namespace type is expected as the `locals` argument during annotations evaluation. +""" + +_TypeVarLike: TypeAlias = 'TypeVar | ParamSpec | TypeVarTuple' + + +class NamespacesTuple(NamedTuple): + """A tuple of globals and locals to be used during annotations evaluation. + + This datastructure is defined as a named tuple so that it can easily be unpacked: + + ```python {lint="skip" test="skip"} + def eval_type(typ: type[Any], ns: NamespacesTuple) -> None: + return eval(typ, *ns) + ``` + """ + + globals: GlobalsNamespace + """The namespace to be used as the `globals` argument during annotations evaluation.""" + + locals: MappingNamespace + """The namespace to be used as the `locals` argument during annotations evaluation.""" + + +def get_module_ns_of(obj: Any) -> dict[str, Any]: + """Get the namespace of the module where the object is defined. + + Caution: this function does not return a copy of the module namespace, so the result + should not be mutated. The burden of enforcing this is on the caller. + """ + module_name = getattr(obj, '__module__', None) + if module_name: + try: + return sys.modules[module_name].__dict__ + except KeyError: + # happens occasionally, see https://github.com/pydantic/pydantic/issues/2363 + return {} + return {} + + +# Note that this class is almost identical to `collections.ChainMap`, but need to enforce +# immutable mappings here: +class LazyLocalNamespace(Mapping[str, Any]): + """A lazily evaluated mapping, to be used as the `locals` argument during annotations evaluation. + + While the [`eval`][eval] function expects a mapping as the `locals` argument, it only + performs `__getitem__` calls. The [`Mapping`][collections.abc.Mapping] abstract base class + is fully implemented only for type checking purposes. + + Args: + *namespaces: The namespaces to consider, in ascending order of priority. + + Example: + ```python {lint="skip" test="skip"} + ns = LazyLocalNamespace({'a': 1, 'b': 2}, {'a': 3}) + ns['a'] + #> 3 + ns['b'] + #> 2 + ``` + """ + + def __init__(self, *namespaces: MappingNamespace) -> None: + self._namespaces = namespaces + + @cached_property + def data(self) -> dict[str, Any]: + return {k: v for ns in self._namespaces for k, v in ns.items()} + + def __len__(self) -> int: + return len(self.data) + + def __getitem__(self, key: str) -> Any: + return self.data[key] + + def __contains__(self, key: object) -> bool: + return key in self.data + + def __iter__(self) -> Iterator[str]: + return iter(self.data) + + +def ns_for_function(obj: Callable[..., Any], parent_namespace: MappingNamespace | None = None) -> NamespacesTuple: + """Return the global and local namespaces to be used when evaluating annotations for the provided function. + + The global namespace will be the `__dict__` attribute of the module the function was defined in. + The local namespace will contain the `__type_params__` introduced by PEP 695. + + Args: + obj: The object to use when building namespaces. + parent_namespace: Optional namespace to be added with the lowest priority in the local namespace. + If the passed function is a method, the `parent_namespace` will be the namespace of the class + the method is defined in. Thus, we also fetch type `__type_params__` from there (i.e. the + class-scoped type variables). + """ + locals_list: list[MappingNamespace] = [] + if parent_namespace is not None: + locals_list.append(parent_namespace) + + # Get the `__type_params__` attribute introduced by PEP 695. + # Note that the `typing._eval_type` function expects type params to be + # passed as a separate argument. However, internally, `_eval_type` calls + # `ForwardRef._evaluate` which will merge type params with the localns, + # essentially mimicking what we do here. + type_params: tuple[_TypeVarLike, ...] + if hasattr(obj, '__type_params__'): + type_params = obj.__type_params__ + else: + type_params = () + if parent_namespace is not None: + # We also fetch type params from the parent namespace. If present, it probably + # means the function was defined in a class. This is to support the following: + # https://github.com/python/cpython/issues/124089. + type_params += parent_namespace.get('__type_params__', ()) + + locals_list.append({t.__name__: t for t in type_params}) + + # What about short-cirtuiting to `obj.__globals__`? + globalns = get_module_ns_of(obj) + + return NamespacesTuple(globalns, LazyLocalNamespace(*locals_list)) + + +class NsResolver: + """A class responsible for the namespaces resolving logic for annotations evaluation. + + This class handles the namespace logic when evaluating annotations mainly for class objects. + + It holds a stack of classes that are being inspected during the core schema building, + and the `types_namespace` property exposes the globals and locals to be used for + type annotation evaluation. Additionally -- if no class is present in the stack -- a + fallback globals and locals can be provided using the `namespaces_tuple` argument + (this is useful when generating a schema for a simple annotation, e.g. when using + `TypeAdapter`). + + The namespace creation logic is unfortunately flawed in some cases, for backwards + compatibility reasons and to better support valid edge cases. See the description + for the `parent_namespace` argument and the example for more details. + + Args: + namespaces_tuple: The default globals and locals to use if no class is present + on the stack. This can be useful when using the `GenerateSchema` class + with `TypeAdapter`, where the "type" being analyzed is a simple annotation. + parent_namespace: An optional parent namespace that will be added to the locals + with the lowest priority. For a given class defined in a function, the locals + of this function are usually used as the parent namespace: + + ```python {lint="skip" test="skip"} + from pydantic import BaseModel + + def func() -> None: + SomeType = int + + class Model(BaseModel): + f: 'SomeType' + + # when collecting fields, an namespace resolver instance will be created + # this way: + # ns_resolver = NsResolver(parent_namespace={'SomeType': SomeType}) + ``` + + For backwards compatibility reasons and to support valid edge cases, this parent + namespace will be used for *every* type being pushed to the stack. In the future, + we might want to be smarter by only doing so when the type being pushed is defined + in the same module as the parent namespace. + + Example: + ```python {lint="skip" test="skip"} + ns_resolver = NsResolver( + parent_namespace={'fallback': 1}, + ) + + class Sub: + m: 'Model' + + class Model: + some_local = 1 + sub: Sub + + ns_resolver = NsResolver() + + # This is roughly what happens when we build a core schema for `Model`: + with ns_resolver.push(Model): + ns_resolver.types_namespace + #> NamespacesTuple({'Sub': Sub}, {'Model': Model, 'some_local': 1}) + # First thing to notice here, the model being pushed is added to the locals. + # Because `NsResolver` is being used during the model definition, it is not + # yet added to the globals. This is useful when resolving self-referencing annotations. + + with ns_resolver.push(Sub): + ns_resolver.types_namespace + #> NamespacesTuple({'Sub': Sub}, {'Sub': Sub, 'Model': Model}) + # Second thing to notice: `Sub` is present in both the globals and locals. + # This is not an issue, just that as described above, the model being pushed + # is added to the locals, but it happens to be present in the globals as well + # because it is already defined. + # Third thing to notice: `Model` is also added in locals. This is a backwards + # compatibility workaround that allows for `Sub` to be able to resolve `'Model'` + # correctly (as otherwise models would have to be rebuilt even though this + # doesn't look necessary). + ``` + """ + + def __init__( + self, + namespaces_tuple: NamespacesTuple | None = None, + parent_namespace: MappingNamespace | None = None, + ) -> None: + self._base_ns_tuple = namespaces_tuple or NamespacesTuple({}, {}) + self._parent_ns = parent_namespace + self._types_stack: list[type[Any] | TypeAliasType] = [] + + @cached_property + def types_namespace(self) -> NamespacesTuple: + """The current global and local namespaces to be used for annotations evaluation.""" + if not self._types_stack: + # TODO: should we merge the parent namespace here? + # This is relevant for TypeAdapter, where there are no types on the stack, and we might + # need access to the parent_ns. Right now, we sidestep this in `type_adapter.py` by passing + # locals to both parent_ns and the base_ns_tuple, but this is a bit hacky. + # we might consider something like: + # if self._parent_ns is not None: + # # Hacky workarounds, see class docstring: + # # An optional parent namespace that will be added to the locals with the lowest priority + # locals_list: list[MappingNamespace] = [self._parent_ns, self._base_ns_tuple.locals] + # return NamespacesTuple(self._base_ns_tuple.globals, LazyLocalNamespace(*locals_list)) + return self._base_ns_tuple + + typ = self._types_stack[-1] + + globalns = get_module_ns_of(typ) + + locals_list: list[MappingNamespace] = [] + # Hacky workarounds, see class docstring: + # An optional parent namespace that will be added to the locals with the lowest priority + if self._parent_ns is not None: + locals_list.append(self._parent_ns) + if len(self._types_stack) > 1: + first_type = self._types_stack[0] + locals_list.append({first_type.__name__: first_type}) + + if hasattr(typ, '__dict__'): + # TypeAliasType is the exception. + locals_list.append(vars(typ)) + + # The len check above presents this from being added twice: + locals_list.append({typ.__name__: typ}) + + return NamespacesTuple(globalns, LazyLocalNamespace(*locals_list)) + + @contextmanager + def push(self, typ: type[Any] | TypeAliasType, /) -> Generator[None]: + """Push a type to the stack.""" + self._types_stack.append(typ) + # Reset the cached property: + self.__dict__.pop('types_namespace', None) + try: + yield + finally: + self._types_stack.pop() + self.__dict__.pop('types_namespace', None) |