diff --git a/pyproject.toml b/pyproject.toml index a182100..42743e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,9 +104,6 @@ omit = ["*/tests/*"] plugins = ["covdefaults"] source = ["type_lens"] -[tool.coverage.report] -fail-under = 100 - [tool.pytest.ini_options] addopts = "--strict-markers --strict-config" testpaths = ["tests"] diff --git a/tests/test_type_view.py b/tests/test_type_view.py index 41bb116..7bc7698 100644 --- a/tests/test_type_view.py +++ b/tests/test_type_view.py @@ -19,6 +19,7 @@ import pytest from typing_extensions import Annotated, NotRequired, Required, get_type_hints +from typing_extensions import Literal as ExtensionsLiteral from type_lens import TypeView from type_lens.types.builtins import NoneType @@ -349,6 +350,9 @@ def test_repr() -> None: assert repr(TypeView(int)) == "TypeView(int)" assert repr(TypeView(Optional[str])) == "TypeView(Union[str, NoneType])" assert repr(TypeView(Literal["1", 2, True])) == "TypeView(Literal['1', 2, True])" + assert repr(TypeView(Tuple[Literal["1"], int])) == "TypeView(Tuple[Literal['1'], int])" + assert repr(TypeView(ExtensionsLiteral["1", 2, True])) == "TypeView(Literal['1', 2, True])" + assert repr(TypeView(Any)) == "TypeView(Any)" def test_is_none_type() -> None: diff --git a/type_lens/type_view.py b/type_lens/type_view.py index eeb74a3..6c6ef7c 100644 --- a/type_lens/type_view.py +++ b/type_lens/type_view.py @@ -2,9 +2,10 @@ from collections import abc from collections.abc import Collection, Mapping -from typing import Any, AnyStr, Final, ForwardRef, Generic, Literal, TypeVar, Union +from typing import Any, AnyStr, Final, ForwardRef, Generic, Literal, TypeVar, Union, _SpecialForm from typing_extensions import Annotated, NotRequired, Required, get_args, get_origin +from typing_extensions import Literal as ExtensionsLiteral from type_lens.types.builtins import UNION_TYPES, NoneType from type_lens.utils import INSTANTIABLE_TYPE_MAPPING, SAFE_GENERIC_ORIGIN_MAP, unwrap_annotation @@ -71,13 +72,22 @@ def __repr__(self) -> str: @property def repr_type(self) -> str: - if isinstance(self.annotation, type) or self.origin: - if self.is_literal: - name = "Literal" - elif self.is_union: - name = "Union" - else: - name = self.annotation.__name__ + """Returns a consistent, canonical repr of the contained annotation. + + Removes preceding `typing.` prefix for built-in typing constructs. Python's + native repr for `typing` types is inconsistent across python versions! + """ + # Literal/Union both appear to have no name on some versions of python. + if self.is_literal: + name = "Literal" + elif self.is_union: + name = "Union" + elif isinstance(self.annotation, (type, _SpecialForm)) or self.origin: + try: + name = self.annotation.__name__ # pyright: ignore[reportAttributeAccessIssue] + except AttributeError: + # Certain _SpecialForm items have no __name__ python 3.8. + name = self.annotation._name # pyright: ignore[reportAttributeAccessIssue] else: name = repr(self.annotation) @@ -128,7 +138,7 @@ def is_forward_ref(self) -> bool: @property def is_literal(self) -> bool: """Whether the annotation is a literal value or not.""" - return self.origin is Literal + return self.origin in {Literal, ExtensionsLiteral} @property def is_mapping(self) -> bool: