Skip to content

Commit

Permalink
[3.14] Address invalid inputs of TypeAliasType (#477)
Browse files Browse the repository at this point in the history
Co-authored-by: Jelle Zijlstra <[email protected]>
  • Loading branch information
Daraan and JelleZijlstra authored Oct 25, 2024
1 parent 82d512a commit 139ac68
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 1 deletion.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ aliases that have a `Concatenate` special form as their argument.
`Ellipsis` as an argument. Patch by [Daraan](https://github.com/Daraan).
- Fix error in subscription of `Unpack` aliases causing nested Unpacks
to not be resolved correctly. Patch by [Daraan](https://github.com/Daraan).
- Backport of CPython PR [#124795](https://github.com/python/cpython/pull/124795)
and fix that `TypeAliasType` not raising an error on non-tupple inputs for `type_params`.
Patch by [Daraan](https://github.com/Daraan).

# Release 4.12.2 (June 7, 2024)

Expand Down
78 changes: 78 additions & 0 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6192,6 +6192,10 @@ def test_typing_extensions_defers_when_possible(self):
'AsyncGenerator', 'ContextManager', 'AsyncContextManager',
'ParamSpec', 'TypeVar', 'TypeVarTuple', 'get_type_hints',
}
if sys.version_info < (3, 14):
exclude |= {
'TypeAliasType'
}
if not typing_extensions._PEP_728_IMPLEMENTED:
exclude |= {'TypedDict', 'is_typeddict'}
for item in typing_extensions.__all__:
Expand Down Expand Up @@ -7402,6 +7406,80 @@ def test_no_instance_subclassing(self):
class MyAlias(TypeAliasType):
pass

def test_type_var_compatibility(self):
# Regression test to assure compatibility with typing variants
typingT = typing.TypeVar('typingT')
T1 = TypeAliasType("TypingTypeVar", ..., type_params=(typingT,))
self.assertEqual(T1.__type_params__, (typingT,))

# Test typing_extensions backports
textT = TypeVar('textT')
T2 = TypeAliasType("TypingExtTypeVar", ..., type_params=(textT,))
self.assertEqual(T2.__type_params__, (textT,))

textP = ParamSpec("textP")
T3 = TypeAliasType("TypingExtParamSpec", ..., type_params=(textP,))
self.assertEqual(T3.__type_params__, (textP,))

textTs = TypeVarTuple("textTs")
T4 = TypeAliasType("TypingExtTypeVarTuple", ..., type_params=(textTs,))
self.assertEqual(T4.__type_params__, (textTs,))

@skipUnless(TYPING_3_10_0, "typing.ParamSpec is not available before 3.10")
def test_param_spec_compatibility(self):
# Regression test to assure compatibility with typing variant
typingP = typing.ParamSpec("typingP")
T5 = TypeAliasType("TypingParamSpec", ..., type_params=(typingP,))
self.assertEqual(T5.__type_params__, (typingP,))

@skipUnless(TYPING_3_12_0, "typing.TypeVarTuple is not available before 3.12")
def test_type_var_tuple_compatibility(self):
# Regression test to assure compatibility with typing variant
typingTs = typing.TypeVarTuple("typingTs")
T6 = TypeAliasType("TypingTypeVarTuple", ..., type_params=(typingTs,))
self.assertEqual(T6.__type_params__, (typingTs,))

def test_type_params_possibilities(self):
T = TypeVar('T')
# Test not a tuple
with self.assertRaisesRegex(TypeError, "type_params must be a tuple"):
TypeAliasType("InvalidTypeParams", List[T], type_params=[T])

# Test default order and other invalid inputs
T_default = TypeVar('T_default', default=int)
Ts = TypeVarTuple('Ts')
Ts_default = TypeVarTuple('Ts_default', default=Unpack[Tuple[str, int]])
P = ParamSpec('P')
P_default = ParamSpec('P_default', default=[str, int])

# NOTE: PEP 696 states: "TypeVars with defaults cannot immediately follow TypeVarTuples"
# this is currently not enforced for the type statement and is not tested.
# PEP 695: Double usage of the same name is also not enforced and not tested.
valid_cases = [
(T, P, Ts),
(T, Ts_default),
(P_default, T_default),
(P, T_default, Ts_default),
(T_default, P_default, Ts_default),
]
invalid_cases = [
((T_default, T), f"non-default type parameter {T!r} follows default"),
((P_default, P), f"non-default type parameter {P!r} follows default"),
((Ts_default, T), f"non-default type parameter {T!r} follows default"),
# Only type params are accepted
((1,), "Expected a type param, got 1"),
((str,), f"Expected a type param, got {str!r}"),
# Unpack is not a TypeVar but isinstance(Unpack[Ts], TypeVar) is True in Python < 3.12
((Unpack[Ts],), f"Expected a type param, got {re.escape(repr(Unpack[Ts]))}"),
]

for case in valid_cases:
with self.subTest(type_params=case):
TypeAliasType("OkCase", List[T], type_params=case)
for case, msg in invalid_cases:
with self.subTest(type_params=case):
with self.assertRaisesRegex(TypeError, msg):
TypeAliasType("InvalidCase", List[T], type_params=case)

class DocTests(BaseTestCase):
def test_annotation(self):
Expand Down
21 changes: 20 additions & 1 deletion src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3528,8 +3528,9 @@ def __ror__(self, other):
return typing.Union[other, self]


if hasattr(typing, "TypeAliasType"):
if sys.version_info >= (3, 14):
TypeAliasType = typing.TypeAliasType
# 3.8-3.13
else:
def _is_unionable(obj):
"""Corresponds to is_unionable() in unionobject.c in CPython."""
Expand Down Expand Up @@ -3602,11 +3603,29 @@ class TypeAliasType:
def __init__(self, name: str, value, *, type_params=()):
if not isinstance(name, str):
raise TypeError("TypeAliasType name must be a string")
if not isinstance(type_params, tuple):
raise TypeError("type_params must be a tuple")
self.__value__ = value
self.__type_params__ = type_params

default_value_encountered = False
parameters = []
for type_param in type_params:
if (
not isinstance(type_param, (TypeVar, TypeVarTuple, ParamSpec))
# 3.8-3.11
# Unpack Backport passes isinstance(type_param, TypeVar)
or _is_unpack(type_param)
):
raise TypeError(f"Expected a type param, got {type_param!r}")
has_default = (
getattr(type_param, '__default__', NoDefault) is not NoDefault
)
if default_value_encountered and not has_default:
raise TypeError(f'non-default type parameter {type_param!r}'
' follows default type parameter')
if has_default:
default_value_encountered = True
if isinstance(type_param, TypeVarTuple):
parameters.extend(type_param)
else:
Expand Down

0 comments on commit 139ac68

Please sign in to comment.