Skip to content

Commit eecde3d

Browse files
committed
feat: Add support for identifying and stripping python 3.12 explicit type aliases.
1 parent 897f329 commit eecde3d

File tree

3 files changed

+75
-1
lines changed

3 files changed

+75
-1
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ classifiers = [
3838
"Intended Audience :: Developers",
3939
]
4040
name = "type-lens"
41-
version = "0.2.4"
41+
version = "0.2.5"
4242
description = "type-lens is a Python template project designed to simplify the setup of a new project."
4343
readme = "README.md"
4444
license = { text = "MIT" }

tests/test_type_view.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,3 +426,41 @@ def test_fallback_origin() -> None:
426426

427427
if sys.version_info >= (3, 9):
428428
assert TypeView(set[bool]).fallback_origin == set
429+
430+
431+
@pytest.mark.skipif(
432+
sys.version_info < (3, 12), reason="Requires Python 3.12 or higher for TypeAliasType"
433+
)
434+
def test_is_type_alias() -> None:
435+
from typing import TypeAliasType # type: ignore[attr-defined]
436+
437+
Foo = TypeAliasType("Foo", int) # pyright: ignore
438+
assert TypeView(Foo).is_type_alias is True # pyright: ignore
439+
assert TypeView(int).is_type_alias is False
440+
441+
442+
def test_is_type_alias_extensions() -> None:
443+
from typing_extensions import TypeAliasType
444+
445+
Foo = TypeAliasType("Foo", int) # pyright: ignore
446+
assert TypeView(Foo).is_type_alias is True
447+
assert TypeView(int).is_type_alias is False
448+
449+
450+
@pytest.mark.skipif(
451+
sys.version_info < (3, 12), reason="Requires Python 3.12 or higher for TypeAliasType"
452+
)
453+
def test_strip_type_alias() -> None:
454+
from typing import TypeAliasType # type: ignore[attr-defined]
455+
456+
Foo = TypeAliasType("Foo", int) # pyright: ignore
457+
assert TypeView(Foo).strip_type_alias() == TypeView(int) # pyright: ignore
458+
assert TypeView(int).strip_type_alias() == TypeView(int)
459+
460+
461+
def test_strip_type_alias_extensions() -> None:
462+
from typing_extensions import TypeAliasType
463+
464+
Foo = TypeAliasType("Foo", int) # pyright: ignore
465+
assert TypeView(Foo).strip_type_alias() == TypeView(int)
466+
assert TypeView(int).strip_type_alias() == TypeView(int)

type_lens/type_view.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from __future__ import annotations
2+
import sys
23

34
from collections import abc
45
from collections.abc import Collection, Mapping
6+
import typing
57
from typing import (
68
Any,
79
AnyStr,
@@ -14,6 +16,7 @@
1416
_SpecialForm, # pyright: ignore[reportPrivateUsage]
1517
)
1618

19+
import typing_extensions
1720
from typing_extensions import Annotated, NotRequired, Required, get_args, get_origin
1821
from typing_extensions import Literal as ExtensionsLiteral
1922

@@ -204,6 +207,19 @@ def is_variadic_tuple(self) -> bool:
204207
"""
205208
return self.is_tuple and len(self.args) == 2 and self.args[1] == ... # pyright: ignore
206209

210+
if sys.version_info < (3, 12):
211+
@property
212+
def is_type_alias(self) -> bool:
213+
"""Whether the annotation is a new-style `type Type = ...` alias or not.
214+
"""
215+
return _is_typing_extensins_type_alias(self)
216+
else:
217+
@property
218+
def is_type_alias(self) -> bool:
219+
"""Whether the annotation is a new-style `type Type = ...` alias or not.
220+
"""
221+
return isinstance(self.annotation, typing.TypeAliasType) or _is_typing_extensins_type_alias(self)
222+
207223
@property
208224
def safe_generic_origin(self) -> Any:
209225
"""A type, safe to be used as a generic type across all supported Python versions.
@@ -260,6 +276,8 @@ def is_subclass_of(self, typ: Any | tuple[Any, ...], /) -> bool:
260276
return isinstance(self.fallback_origin, type) and issubclass(self.fallback_origin, typ)
261277

262278
def strip_optional(self) -> TypeView[Any]:
279+
"""Remove the "Optional" component of an `Optional[T]` or `Union[T, None]` type.
280+
"""
263281
if not self.is_optional:
264282
return self
265283

@@ -269,3 +287,21 @@ def strip_optional(self) -> TypeView[Any]:
269287
args = tuple(a for a in self.args if a is not NoneType)
270288
non_optional = Union[args] # type: ignore[valid-type]
271289
return TypeView(non_optional)
290+
291+
def strip_type_alias(self) -> TypeView[Any]:
292+
"""Remove the type alias from a `type Type = T` type alias.
293+
294+
Examples:
295+
>>> type Foo = int
296+
>>> TypeAlias(Foo).strip_type_alias()
297+
TypeView(int)
298+
"""
299+
if not self.is_type_alias:
300+
return self
301+
return TypeView(self.annotation.__value__)
302+
303+
304+
def _is_typing_extensins_type_alias(type_view: TypeView[Any]) -> bool:
305+
if hasattr(typing_extensions, "TypeAliasType"):
306+
return isinstance(type_view.annotation, typing_extensions.TypeAliasType)
307+
return False

0 commit comments

Comments
 (0)