Source code for dmr.types

import inspect
import sys
from collections.abc import Callable, Iterator, Mapping
from typing import (
    TYPE_CHECKING,
    Any,
    ClassVar,
    Generic,
    TypeAlias,
    TypeVar,
    get_origin,
)

from typing_extensions import (
    Format,
    TypeForm,
    TypeIs,
    get_original_bases,
    get_type_hints,
)

from dmr.exceptions import UnsolvableAnnotationsError
from dmr.internal.type_inference import (
    resolve_type_args,
    resolve_type_var_default,
)
from dmr.internal.types import EMPTY as EMPTY
from dmr.internal.types import unwrap_type_alias

if TYPE_CHECKING:
    # During type checking it is a recursive alias, so we can be sure
    # that json is always correct
    Json: TypeAlias = (
        str | int | float | bool | list['Json'] | dict[str, 'Json'] | None  # noqa: WPS221
    )
else:
    # However, in runtime this is not a recursive type for speed.
    # There might also be problems in validating a recursive type alias
    # with some serializers.
    # It will still be rendered as `{}` in OpenAPI.
    # Which means that it can be any valid json.
    Json: TypeAlias = Any
    """
    Recursive type alias for JSON data.

    What is JSON? Integers, floats, booleans, strings,
    list of them and dicts of them, which keys are always strings.

    In runtime it is always :data:`typing.Any`
    because of the parsing complexity,
    while in type checking it correctly defined.

    We don't recommend using it for anything serious,
    it is better to define real models instead.
    """


[docs] def safe_typevar( typevar_name: str, *, stacklevel: int = 1, globalns: dict[str, Any] | None = None, ) -> Any: """ Typing utility to allow passing :class:`typing.TypeVar` safely. By default ``mypy`` and other type-checkers would raise a typing error on a code like this: .. code-block:: python >>> from typing import TypeVar >>> from http import HTTPStatus >>> from dmr import validate, ResponseSpec >>> _ModelT = TypeVar('_ModelT') >>> validate( ... ResponseSpec( ... # In traditional Python typing spec, it is not allowed ... # to use type var in this context: ... _ModelT, # type: ignore[misc] ... status_code=HTTPStatus.OK, ... ), ... ) <function ...> But, this function can help with this problem with no type errors: .. code-block:: python >>> validate( ... ResponseSpec( ... safe_typevar('_ModelT'), ... status_code=HTTPStatus.OK, ... ), ... ) <function ...> Parameters: typevar_name: TypeVar name to find in the globals. stacklevel: Levels of function frames to get globals from. globalns: Explicit ``globals`` namespace. Has a higher priority than *stacklevel*. Raises: KeyError: If *typevar_name* is not found in *globalns*. .. versionadded:: 0.14.0 """ ns = globalns or sys._getframe(stacklevel).f_globals # pyright: ignore[reportPrivateUsage] # noqa: SLF001 return ns[typevar_name]
def infer_type_args( orig_cls: type[Any], given_type: type[Any], ) -> tuple[TypeForm[Any] | TypeVar, ...]: """ Return type args for the closest given type. .. code:: python >>> import pydantic >>> from dmr import Controller >>> from dmr.plugins.pydantic import PydanticSerializer >>> class MyController(Controller[PydanticSerializer]): ... >>> assert infer_type_args(MyController, Controller) == ( ... PydanticSerializer, ... ) Will return ``(MyModel, )`` for ``Query`` as *given_type*. .. versionchanged:: 0.16.0 Bases without explicit type args now contribute :pep:`696` type var defaults, if they have any. """ return tuple( arg for base_class in infer_bases(orig_cls, given_type) for arg in resolve_type_args(base_class) ) def infer_bases( orig_cls: type[Any], given_type: type[Any], *, use_origin: bool = True, ) -> list[TypeForm[Any]]: """ Infers ``__origin_bases__`` from the given type. .. versionchanged:: 0.16.0 Bases without explicit type args are now returned as well, because they can still provide :pep:`696` type var defaults. """ return [ base for base in get_original_bases(orig_cls) if ( (origin := (get_origin(base) or base) if use_origin else base) # noqa: WPS509 and is_safe_subclass(origin, given_type) ) ] def infer_annotation( annotation: TypeForm[Any] | TypeVar, context: type[Any], ) -> TypeForm[Any]: """Infers annotation in the class definition context.""" if not isinstance(annotation, TypeVar): return annotation # It is already inferred return TypeVarInference(annotation, context)()[annotation] _TypeT = TypeVar('_TypeT') def is_safe_subclass( annotation: Any, base_class: type[_TypeT], ) -> TypeIs[type[_TypeT]]: """Possibly unwraps subscribed class before checking for subclassing.""" if annotation is None: annotation = type(None) try: return issubclass( get_origin(annotation) or annotation, base_class, ) except TypeError: return False
[docs] class AnnotationsContext: """ Annotation evaluation context. Use this type to change how controllers resolve type hints of their endpoints. For example, one can change this function to use :func:`inspect.get_annotations` function. Or to have some pre-defined global names. """ __slots__ = ('_format', '_globalns', '_include_extras', '_localns') def __init__( self, *, globalns: Mapping[str, Any] | None = None, localns: Mapping[str, Any] | None = None, include_extras: bool = True, format: Format | None = None, # noqa: A002 ) -> None: """Create the context for the future annotations.""" self._globalns = globalns self._localns = localns self._include_extras = include_extras self._format = format
[docs] def __call__(self, endpoint_func: Callable[..., Any]) -> dict[str, Any]: """ Get the annotations. Args: endpoint_func: function with return type annotation. Returns: Function's parsed and solved return type. Type aliases are unwrapped, so all the callers can work with real types and their metadata. Raises: UnsolvableAnnotationsError: when annotation can't be solved or when the annotation does not exist. """ type_hints_params: dict[str, Any] = { 'globalns': self._global_namespace(endpoint_func), 'localns': self._localns, 'include_extras': self._include_extras, } # `format` is only available in 3.14+ if self._format is not None: # pragma: >=3.14 cover type_hints_params['format'] = self._format try: type_hints = get_type_hints(endpoint_func, **type_hints_params) except Exception as exc: raise UnsolvableAnnotationsError( f'Annotations of {endpoint_func!r} cannot be solved', ) from exc return { context_name: unwrap_type_alias(annotation) for context_name, annotation in type_hints.items() }
def _global_namespace( self, endpoint_func: Callable[..., Any], ) -> Mapping[str, Any]: if self._globalns is not None: return self._globalns return inspect.unwrap(endpoint_func).__globals__ # type: ignore[no-any-return]
[docs] class TypeVarInference: """ Inferences type variables to the applied real type values. .. versionchanged:: 0.16.0 :pep:`696` type var defaults are now used when the inheritance chain does not provide a real type value. """ __slots__ = ('_context', '_to_infer') _max_depth: ClassVar[int] = 15 def __init__( self, to_infer: TypeVar, context: type[Any], ) -> None: """ Prepare the inference. Args: to_infer: type var which needs to be inferred. context: its usage in inheritance with real type values provided. """ self._to_infer = to_infer self._context = context
[docs] def __call__(self) -> dict[TypeVar, TypeForm[Any]]: """ Run the inference. Returns: Mapping of type vars to its inferenced values. It can still be a type variable, if no real values are provided. """ # We match type params by name, because they can be a bit different, # like `__type_params__` in >=3.12 and `TypeVar` in <=3.11. # This also ignore variance and stuff. type_map = {self._to_infer.__name__: self._to_infer} type_parameters = (self._to_infer,) for base in reversed(list(self._resolve_orig_bases(self._context))): # We apply type params in the "reversed mro order". self._apply_base_type_params(base, type_map) return self._infer(type_map, type_parameters)
def _resolve_orig_bases(self, typ: type[Any]) -> Iterator[type[Any]]: """Resolves ~full mro but with ``__orig_bases__`` instead of bases.""" orig_bases = getattr(typ, '__orig_bases__', None) if orig_bases is None: orig_bases = getattr(get_origin(typ), '__orig_bases__', []) for base in orig_bases: if get_origin(base) is Generic: continue yield base yield from self._resolve_orig_bases(base) def _apply_base_type_params( self, base: type[Any], type_map: dict[str, Any], ) -> None: type_args = resolve_type_args(base) if not type_args: # Either a regular non-generic base # or a bare generic one without any type var defaults. return origin = get_origin(base) or base for type_param, type_arg in zip( getattr(origin, '__parameters__', []), type_args, strict=True, ): # TODO: this might be something else, like `TypeVarTuple` # or `ParamSpec`. But, they are not supported. Yet? assert isinstance(type_param, TypeVar), type_param # noqa: S101 # We record all type params, not just the ones we need right now: # a type arg can be a type var that this very base also provides # a value for. Which is what `PEP 696` defaults do. type_map.update({type_param.__name__: type_arg}) def _infer( self, type_map: dict[str, Any], type_parameters: tuple[TypeVar, ...], ) -> dict[TypeVar, TypeForm[Any]]: inferenced: dict[TypeVar, TypeForm[Any]] = {} type_param: Any for type_param in type_parameters: orig_type_param = type_param iterations = 0 while isinstance(type_param, TypeVar): iterations += 1 type_param = self._resolve_type_var(type_param, type_map) if iterations >= self._max_depth: raise UnsolvableAnnotationsError( f'Cannot solve type annotations for {type_param!r}. ' f'Is definition for {self._to_infer!r} generic? ' 'It must be concrete', ) inferenced.update({orig_type_param: type_param}) return inferenced def _resolve_type_var( self, type_var: TypeVar, type_map: dict[str, Any], ) -> Any: resolved = type_map.get(type_var.__name__, type_var) if resolved is type_var: # Nothing in the inheritance chain provides a real value for it, # its `PEP 696` default is the last resort. return resolve_type_var_default(type_var) return resolved