Skip to content

Commit

Permalink
Merge branch 'litestar-org:main' into dc/function-view
Browse files Browse the repository at this point in the history
  • Loading branch information
DanCardin authored Aug 16, 2024
2 parents b33ea80 + 6700005 commit 8534cb1
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 47 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ jobs:
strategy:
fail-fast: true
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
timeout-minutes: 30
defaults:
run:
Expand All @@ -115,6 +115,7 @@ jobs:
- uses: pdm-project/setup-pdm@v3
name: Set up PDM
with:
allow-python-prereleases: true
python-version: ${{ matrix.python-version }}
cache: true

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Software Development",
Expand Down
57 changes: 56 additions & 1 deletion tests/test_type_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
from typing import (
TYPE_CHECKING,
Any,
Dict,
ForwardRef,
List,
Literal,
Optional,
Sequence,
Tuple,
TypedDict,
TypeVar,
Expand Down Expand Up @@ -257,7 +259,14 @@ def test_parsed_type_is_optional_predicate() -> None:


def test_parsed_type_is_subtype_of() -> None:
"""Test ParsedType.is_type_of."""
"""Test TypeView.is_subtype_of."""

class Foo:
pass

class Bar(Foo):
pass

assert TypeView(bool).is_subtype_of(int) is True
assert TypeView(bool).is_subtype_of(str) is False
assert TypeView(Union[int, str]).is_subtype_of(int) is False
Expand All @@ -266,6 +275,31 @@ def test_parsed_type_is_subtype_of() -> None:
assert TypeView(Optional[int]).is_subtype_of(int) is False
assert TypeView(Union[bool, int]).is_subtype_of(int) is True

assert TypeView(Foo).is_subtype_of(Foo) is True
assert TypeView(Bar).is_subtype_of(Foo) is True


def test_is_subclass_of() -> None:
class Foo:
pass

class Bar(Foo):
pass

assert TypeView(bool).is_subclass_of(int) is True
assert TypeView(bool).is_subclass_of(str) is False
assert TypeView(Union[int, str]).is_subclass_of(int) is False
assert TypeView(List[int]).is_subclass_of(int) is False
assert TypeView(Optional[int]).is_subclass_of(int) is False
assert TypeView(None).is_subclass_of(int) is False
assert TypeView(Literal[1]).is_subclass_of(int) is False
assert TypeView(Union[bool, int]).is_subclass_of(int) is False

assert TypeView(bool).is_subclass_of(bool) is True
assert TypeView(List[int]).is_subclass_of(list) is True
assert TypeView(Foo).is_subclass_of(Foo) is True
assert TypeView(Bar).is_subclass_of(Foo) is True


def test_parsed_type_has_inner_subtype_of() -> None:
"""Test ParsedType.has_type_of."""
Expand Down Expand Up @@ -362,3 +396,24 @@ def test_repr_type() -> None:

if sys.version_info >= (3, 9):
assert TypeView(set[bool]).repr_type == "set[bool]"


def test_instantiatable_origin() -> None:
assert TypeView(int).instantiable_origin == int
assert TypeView(list).instantiable_origin == list
assert TypeView(List[int]).instantiable_origin == list
assert TypeView(Dict[int, int]).instantiable_origin == dict
assert TypeView(Sequence[int]).instantiable_origin == list
assert TypeView(TypeView).instantiable_origin == TypeView


def test_fallback_origin() -> None:
assert TypeView(int).fallback_origin == int
assert TypeView(list).fallback_origin == list
assert TypeView(List).fallback_origin == list
assert TypeView(List[str]).fallback_origin == list
assert TypeView(Literal[1]).fallback_origin == Literal
assert TypeView(Literal).fallback_origin == Literal

if sys.version_info >= (3, 9):
assert TypeView(set[bool]).fallback_origin == set
21 changes: 17 additions & 4 deletions type_lens/type_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing_extensions import Annotated, NotRequired, Required, get_args, get_origin

from type_lens.types.builtins import UNION_TYPES, NoneType
from type_lens.utils import get_instantiable_origin, get_safe_generic_origin, unwrap_annotation
from type_lens.utils import INSTANTIABLE_TYPE_MAPPING, SAFE_GENERIC_ORIGIN_MAP, unwrap_annotation

__all__ = ("TypeView",)

Expand All @@ -24,6 +24,7 @@ class TypeView(Generic[T]):
"inner_types": "The type's generic args parsed as ParsedType, if applicable.",
"metadata": "Any metadata associated with the annotation via Annotated.",
"origin": "The result of calling get_origin(annotation) after unwrapping Annotated, e.g. list.",
"fallback_origin": "The unsubscripted version of a type, distinct from 'origin' in that for non-generics, this is the original type.",
"raw": "The annotation exactly as received.",
"_wrappers": "A set of wrapper types that were removed from the annotation.",
}
Expand All @@ -47,6 +48,7 @@ def __init__(self, annotation: T) -> None:
self.raw: Final[T] = annotation
self.annotation: Final = unwrapped
self.origin: Final = origin
self.fallback_origin: Final = origin or unwrapped
self.args: Final = args
self.metadata: Final = metadata
self._wrappers: Final = wrappers
Expand Down Expand Up @@ -101,7 +103,7 @@ def instantiable_origin(self) -> Any:
Returns:
An instantiable type that is consistent with the origin type of the annotation.
"""
return get_instantiable_origin(self)
return INSTANTIABLE_TYPE_MAPPING.get(self.fallback_origin, self.fallback_origin)

@property
def is_annotated(self) -> bool:
Expand Down Expand Up @@ -182,14 +184,14 @@ def is_variadic_tuple(self) -> bool:

@property
def safe_generic_origin(self) -> Any:
"""An object safe to be used as a generic type across all supported Python versions.
"""A type, safe to be used as a generic type across all supported Python versions.
Examples:
>>> from type_lens import TypeView
>>> TypeView(dict[str, int]).safe_generic_origin
typing.Dict
"""
return get_safe_generic_origin(self)
return SAFE_GENERIC_ORIGIN_MAP.get(self.fallback_origin)

def has_inner_subtype_of(self, typ: type[Any] | tuple[type[Any], ...]) -> bool:
"""Whether any generic args are a subclass of the given type.
Expand Down Expand Up @@ -224,6 +226,17 @@ def is_subtype_of(self, typ: Any | tuple[Any, ...], /) -> bool:
return issubclass(str, typ) or issubclass(bytes, typ)
return self.annotation is not Any and not self.is_type_var and issubclass(self.annotation, typ)

def is_subclass_of(self, typ: Any | tuple[Any, ...], /) -> bool:
"""Whether the annotation is a subclass of the given type.
Args:
typ: The type to check, or tuple of types. Passed as 2nd argument to ``issubclass()``.
Returns:
Whether the annotation is a subclass of the given type(s).
"""
return isinstance(self.fallback_origin, type) and issubclass(self.fallback_origin, typ)

def strip_optional(self) -> TypeView:
if not self.is_optional:
return self
Expand Down
44 changes: 3 additions & 41 deletions type_lens/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,9 @@

from type_lens.types.builtins import UNION_TYPES

if t.TYPE_CHECKING:
from type_lens import TypeView
__all__ = ("unwrap_annotation", "SAFE_GENERIC_ORIGIN_MAP", "INSTANTIABLE_TYPE_MAPPING")

__all__ = (
"get_instantiable_origin",
"get_safe_generic_origin",
"unwrap_annotation",
)

_SAFE_GENERIC_ORIGIN_MAP: te.Final[dict[object, object]] = {
SAFE_GENERIC_ORIGIN_MAP: te.Final[dict[object, object]] = {
set: t.AbstractSet,
defaultdict: t.DefaultDict,
deque: t.Deque,
Expand Down Expand Up @@ -56,7 +49,7 @@
_WRAPPER_TYPES: te.Final = {te.Annotated, te.Required, te.NotRequired}
"""Types that always contain a wrapped type annotation as their first arg."""

_INSTANTIABLE_TYPE_MAPPING: te.Final = {
INSTANTIABLE_TYPE_MAPPING: te.Final = {
t.AbstractSet: set,
t.DefaultDict: defaultdict,
t.Deque: deque,
Expand Down Expand Up @@ -87,37 +80,6 @@
"""A mapping of types to equivalent types that are safe to instantiate."""


def get_instantiable_origin(type_view: TypeView) -> t.Any:
"""Get a type that is safe to instantiate for the given origin type.
If a builtin collection type is annotated without generic args, e.g, ``a: dict``, then the origin type will be
``None``. In this case, we can use the annotation to determine the correct instantiable type, if one exists.
Args:
type_view: A :class:`TypeView` instance.
Returns:
A builtin type that is safe to instantiate for the given origin type.
"""
if type_view.origin is None:
return _INSTANTIABLE_TYPE_MAPPING.get(type_view.annotation)
return _INSTANTIABLE_TYPE_MAPPING.get(type_view.origin, type_view.origin)


def get_safe_generic_origin(type_view: TypeView) -> t.Any | None:
"""Get a type that is safe to use as a generic type across all supported Python versions.
Args:
type_view: A :class:`TypeView` instance.
Returns:
A type that is safe to use as a generic type across all supported Python versions.
"""
if type_view.origin is None:
return _SAFE_GENERIC_ORIGIN_MAP.get(type_view.annotation)
return _SAFE_GENERIC_ORIGIN_MAP.get(type_view.origin)


def unwrap_annotation(annotation: t.Any) -> tuple[t.Any, tuple[t.Any, ...], set[t.Any]]:
"""Remove "wrapper" annotation types, such as ``Annotated``, ``Required``, and ``NotRequired``.
Expand Down

0 comments on commit 8534cb1

Please sign in to comment.