about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/pydantic/_internal/_namespace_utils.py
diff options
context:
space:
mode:
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.py284
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)