Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support ParamSpec for TypeAliasType #449

Open
wants to merge 63 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
b6bc323
Support ParamSpec for TypeAliasType
Daraan Aug 21, 2024
79985f3
Removed trailing whitespaces
Daraan Aug 21, 2024
02025d4
Merge branch 'main' into TypeAliasType-extension
Daraan Sep 9, 2024
3199b7b
raise TypeError for invalid type_params like typing.TypeAliasType
Daraan Sep 18, 2024
f7d79d9
Keep dunder attributes like typing.TypeAliasType
Daraan Sep 18, 2024
c4c0e68
Added test to catch invalid cases, fix wrong intendation
Daraan Sep 18, 2024
d6af983
removed pyright pragma
Daraan Sep 18, 2024
0a5039b
fix whitespaces
Daraan Sep 18, 2024
e532429
Avoid incompatible global types
Daraan Sep 19, 2024
9911ac7
Add __name__ for TypeAliasType for <=3.10
Daraan Sep 19, 2024
e1b3095
Added missing support for Unpack to TypeAliasType variant
Daraan Sep 19, 2024
408ae2e
Added more invalid test cases
Daraan Sep 19, 2024
621085f
Merge branch 'main' into TypeAliasType-extension
Daraan Sep 19, 2024
b3e6b7a
Unpack invalid Concatenate correctly
Daraan Sep 19, 2024
c3d98c6
Removed parameter checking from TypeAliasType._check_parameter
Daraan Sep 23, 2024
8255667
extended and reworked tests
Daraan Sep 23, 2024
255de76
Clean duplicated tests
Daraan Sep 23, 2024
3037923
Removed duplicated or valid cases from invalid
Daraan Sep 23, 2024
b8ae82e
Slightly more refined tests covering more cases
Daraan Sep 23, 2024
8d2ec0a
Removed cases that is mentioned elsewhere
Daraan Sep 23, 2024
7029d51
Revert change of global Protocol variables
Daraan Sep 24, 2024
02fd0ba
Raise TypeError on parameterless alias
Daraan Sep 24, 2024
5c0938c
Merge remote-tracking branch 'upstream/main' into TypeAliasType-exten…
Daraan Sep 24, 2024
b6fefb0
Correct subscription handling when type_params are empty
Daraan Sep 24, 2024
af0a133
Restructured test cases
Daraan Sep 24, 2024
f2aa35c
Added general and comprehensive test
Daraan Sep 24, 2024
6b1bafb
Subtests to test parameter amount
Daraan Sep 24, 2024
7c1fea7
unify __getitem__ again
Daraan Sep 24, 2024
efa1214
covered all cases for TypeErrors during subscription
Daraan Sep 25, 2024
66eebb1
Removed code addressing #468
Daraan Sep 25, 2024
df10751
Use _types.GenericAlias for TypeAliasType in 3.10+
Daraan Sep 25, 2024
889e9ae
Assure that args and parameter tests pass
Daraan Sep 25, 2024
b6b5a14
Remove code to fix dunder attributes -> other PR
Daraan Sep 26, 2024
9562635
small reordering
Daraan Sep 26, 2024
0033813
Merge remote-tracking branch 'upstream/main' into TypeAliasType-exten…
Daraan Sep 26, 2024
014109c
Remove invalid case
Daraan Sep 26, 2024
0b3ce7d
Updated to latest changes from main
Daraan Sep 26, 2024
0ae4c63
revert mistakes of wrong merge
Daraan Sep 26, 2024
144c7b8
Merge branch 'main' into TypeAliasType-extension
AlexWaygood Sep 26, 2024
e613294
No need to skip tests anymore
Daraan Sep 26, 2024
e44fdcc
Merge remote-tracking branch 'origin/TypeAliasType-extension' into Ty…
Daraan Sep 26, 2024
6bc1f57
removed tests related to: https://github.com/python/cpython/issues/12…
Daraan Sep 30, 2024
e8bfa30
Removed tests related to #474
Daraan Sep 30, 2024
617656d
Removed invalid tests
Daraan Sep 30, 2024
41a87b8
minor comment update
Daraan Sep 30, 2024
a8c4bda
Merge branch 'main' into TypeAliasType-extension
Daraan Sep 30, 2024
249b869
More refined skip reason
Daraan Sep 30, 2024
e71902e
Remove type check for tuples; handled by #477
Daraan Oct 1, 2024
b8799ce
updated changelog
Daraan Oct 1, 2024
5bc1360
3.8, 3.9 use collected parameters more explicitly
Daraan Oct 1, 2024
3ae9e35
Correct error message for 3.10
Daraan Oct 2, 2024
9878cc0
Merge branch 'main' into TypeAliasType-extension
Daraan Oct 11, 2024
3863c8b
Merge remote-tracking branch 'upstream/main' into TypeAliasType-exten…
Daraan Oct 21, 2024
8c86619
Merge remote-tracking branch 'upstream/main' into TypeAliasType-exten…
Daraan Oct 22, 2024
251d312
Merge remote-tracking branch 'upstream/main' into TypeAliasType-exten…
Daraan Oct 22, 2024
b3aa598
Generator not necessary anymore
Daraan Oct 22, 2024
07ba7b7
Merge remote-tracking branch 'upstream/main' into TypeAliasType-exten…
Daraan Oct 22, 2024
28d1e84
Merge remote-tracking branch 'upstream/main' into TypeAliasType-exten…
Daraan Oct 25, 2024
a365355
Merge and type correction
Daraan Oct 25, 2024
fb55b88
TODO: handle None
Daraan Oct 25, 2024
f2d6890
Merge remote-tracking branch 'upstream/main' into TypeAliasType-exten…
Daraan Oct 25, 2024
054f083
Merge remote-tracking branch 'upstream/main' into TypeAliasType-exten…
Daraan Oct 28, 2024
eb840ef
Handle None case correctly, also check list
Daraan Oct 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 239 additions & 2 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7173,6 +7173,89 @@ def test_attributes(self):
self.assertEqual(Variadic.__type_params__, (Ts,))
self.assertEqual(Variadic.__parameters__, tuple(iter(Ts)))

P = ParamSpec('P')
CallableP = TypeAliasType("CallableP", Callable[P, Any], type_params=(P, ))
self.assertEqual(CallableP.__name__, "CallableP")
self.assertEqual(CallableP.__value__, Callable[P, Any])
self.assertEqual(CallableP.__type_params__, (P,))
self.assertEqual(CallableP.__parameters__, (P,))

def test_alias_types_and_substitutions(self):
T = TypeVar('T')
T2 = TypeVar('T2')
T_default = TypeVar("T_default", default=int)
Ts = TypeVarTuple("Ts")
P = ParamSpec('P')

test_argument_cases = {
# arguments : expected parameters
int : (),
... : (),
T2 : (T2,),
Union[int, List[T2]] : (T2,),
Tuple[int, str] : (),
Tuple[T, T_default, T2] : (T, T_default, T2),
Tuple[Unpack[Ts]] : (Ts,),
Callable[[Unpack[Ts]], T2] : (Ts, T2),
Callable[P, T2] : (P, T2),
Callable[Concatenate[T2, P], T_default] : (T2, P, T_default),
TypeAliasType("NestedAlias", List[T], type_params=(T,))[T2] : (T2,),
}
# currently a limitation, these args are no longer unpacked in 3.11
# OK if GenericAlias in __getitem__ is used
test_argument_cases_310_plus = {
Unpack[Ts] : (Ts,),
Unpack[Tuple[int, T2]] : (T2,),
Concatenate[int, P] : (P,),
}
test_argument_cases_311_plus = {
Ts : (Ts,), # invalid case
}
test_argument_cases.update(test_argument_cases_310_plus)
test_argument_cases.update(test_argument_cases_311_plus)

test_alias_cases = [
# Simple cases
TypeAliasType("ListT", List[T], type_params=(T,)),
TypeAliasType("UnionT", Union[int, List[T]], type_params=(T,)),
# Value has no parameter but in type_param
TypeAliasType("ValueWithoutT", int, type_params=(T,)),
# Callable
TypeAliasType("CallableP", Callable[P, Any], type_params=(P, )),
TypeAliasType("CallableT", Callable[..., T], type_params=(T, )),
TypeAliasType("CallableTs", Callable[[Unpack[Ts]], Any], type_params=(Ts, )),
# TypeVarTuple
TypeAliasType("Variadic", Tuple[int, Unpack[Ts]], type_params=(Ts,)),
# TypeVar with default
TypeAliasType("TupleT_default", Tuple[T_default, T], type_params=(T, T_default)),
TypeAliasType("CallableT_default", Callable[[T], T_default], type_params=(T, T_default)),
# default order reversed
TypeAliasType("TupleT_default_reversed", Tuple[T_default, T], type_params=(T_default, T)),
JelleZijlstra marked this conversation as resolved.
Show resolved Hide resolved
TypeAliasType("CallableT_default_reversed", Callable[[T], T_default], type_params=(T_default, T)),
]

for alias in test_alias_cases:
with self.subTest(alias=alias, args=[]):
subscripted = alias[[]]
self.assertEqual(get_args(subscripted), ([],))
self.assertEqual(subscripted.__parameters__, ())
with self.subTest(alias=alias, args=()):
subscripted = alias[()]
self.assertEqual(get_args(subscripted), ())
self.assertEqual(subscripted.__parameters__, ())
with self.subTest(alias=alias, args=(int, float)):
subscripted = alias[int, float]
self.assertEqual(get_args(subscripted), (int, float))
self.assertEqual(subscripted.__parameters__, ())
for expected_args, expected_parameters in test_argument_cases.items():
with self.subTest(alias=alias, args=expected_args):
if expected_args in test_argument_cases_310_plus and sys.version_info < (3, 10):
self.skipTest("args are unpacked before 3.11 or need GenericAlias")
if expected_args in test_argument_cases_311_plus and sys.version_info < (3, 11):
self.skipTest("Case is not valid before 3.11")
self.assertEqual(get_args(alias[expected_args]), (expected_args,))
self.assertEqual(alias[expected_args].__parameters__, expected_parameters)

def test_cannot_set_attributes(self):
Simple = TypeAliasType("Simple", int)
with self.assertRaisesRegex(AttributeError, "readonly attribute"):
Expand Down Expand Up @@ -7233,12 +7316,13 @@ def test_or(self):
Alias | "Ref"

def test_getitem(self):
T = TypeVar('T')
ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,))
subscripted = ListOrSetT[int]
self.assertEqual(get_args(subscripted), (int,))
self.assertIs(get_origin(subscripted), ListOrSetT)
with self.assertRaises(TypeError):
subscripted[str]
with self.assertRaises(TypeError, msg="not a generic class"):
Daraan marked this conversation as resolved.
Show resolved Hide resolved
subscripted[int]

still_generic = ListOrSetT[Iterable[T]]
self.assertEqual(get_args(still_generic), (Iterable[T],))
Expand All @@ -7247,6 +7331,159 @@ def test_getitem(self):
self.assertEqual(get_args(fully_subscripted), (Iterable[float],))
self.assertIs(get_origin(fully_subscripted), ListOrSetT)

ValueWithoutTypeVar = TypeAliasType("ValueWithoutTypeParam", int, type_params=(T,))
still_subscripted = ValueWithoutTypeVar[str]
self.assertEqual(get_args(still_subscripted), (str,))

def test_callable_without_concatenate(self):
P = ParamSpec('P')
CallableP = TypeAliasType("CallableP", Callable[P, Any], type_params=(P,))
get_args_test_cases = [
# List of (alias, expected_args)
# () -> Any
(CallableP[()], ()),
(CallableP[[]], ([],)),
# (int) -> Any
(CallableP[int], (int,)),
(CallableP[[int]], ([int],)),
# (int, int) -> Any
(CallableP[int, int], (int, int)),
(CallableP[[int, int]], ([int, int],)),
# (...) -> Any
(CallableP[...], (...,)),
# (int, ...) -> Any
(CallableP[[int, ...]], ([int, ...],)),
]

for index, (expression, expected_args) in enumerate(get_args_test_cases):
with self.subTest(index=index, expression=expression):
self.assertEqual(get_args(expression), expected_args)

self.assertEqual(CallableP[...], CallableP[(...,)])
# (T) -> Any
CallableT = CallableP[T]
self.assertEqual(get_args(CallableT), (T,))
self.assertEqual(CallableT.__parameters__, (T,))

def test_callable_with_concatenate(self):
P = ParamSpec('P')
CallableP = TypeAliasType("CallableP", Callable[P, Any], type_params=(P,))

callable_concat = CallableP[Concatenate[int, P]]
self.assertEqual(callable_concat.__parameters__, (P,))
concat_usage = callable_concat[str]
with self.subTest("get_args of Concatenate in TypeAliasType"):
if sys.version_info < (3, 10, 2):
self.skipTest("GenericAlias keeps Concatenate in __args__ prior to 3.10.2")
self.assertEqual(get_args(concat_usage), ((int, str),))
with self.subTest("Equality of parameter_expression without []"):
if not TYPING_3_10_0:
self.skipTest("Nested list is invalid type form")
self.assertEqual(concat_usage, callable_concat[[str]])
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this block be removed? This is more about GenericAlias usage here and how Concatenate/Unpack is handled.

I think the self.assertEqual(concat_usage, callable_concat[[str]]) test is interesting, but I am not sure about the specifications if this should be valid and stay here.
See also the test_invalid_cases tests where this is redone with a TypeVar.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since using types.GenericAlias these tests became much clearer. Given that Concatenate might need some special treatmeant in TypeAliasType.__getitem__ (currently done by isinstance(param, list)) and this is the only test where this is looked into with a bit more detail, I think it should probaly stay.


def test_substitution(self):
# To pass these tests alias.__args__ in TypeAliasType.__getitem__ needs adjustment
# Unpack and Concatenate are unpacked in versions before
T = TypeVar('T')
Ts = TypeVarTuple("Ts")

CallableTs = TypeAliasType("CallableTs", Callable[[Unpack[Ts]], Any], type_params=(Ts, ))
unpack_callable = CallableTs[Unpack[Tuple[int, T]]]
self.assertEqual(get_args(unpack_callable), (Unpack[Tuple[int, T]],))

P = ParamSpec('P')
CallableP = TypeAliasType("CallableP", Callable[P, T], type_params=(P, T))
callable_concat = CallableP[Concatenate[int, P], Any]
self.assertEqual(get_args(callable_concat), (Concatenate[int, P], Any))

@skipUnless(TYPING_3_12_0, "__args__ behaves differently")
def test_substitution_312_plus(self):
# To pass these tests alias.__args__ in TypeAliasType.__getitem__ needs adjustment
# Would raise: TypeError: Substitution of bare TypeVarTuple is not supported
T = TypeVar('T')
Ts = TypeVarTuple("Ts")
Variadic = TypeAliasType("Variadic", Tuple[int, Unpack[Ts]], type_params=(Ts,))
Daraan marked this conversation as resolved.
Show resolved Hide resolved

subcriped_callable_tvt = Variadic[Callable[[Unpack[Ts]], T]]
variadic_tvt_callableA = subcriped_callable_tvt[str, object]
variadic_tvt_callableA2 = subcriped_callable_tvt[Unpack[Tuple[str]], object]
self.assertEqual(variadic_tvt_callableA, variadic_tvt_callableA2)

variadic_tvt_callableB = subcriped_callable_tvt[[str, int], object]
variadic_tvt_callableB2 = subcriped_callable_tvt[Unpack[Tuple[str, int]], object]
variadic_tvt_callableB3 = subcriped_callable_tvt[str, int, object]
self.assertNotEqual(variadic_tvt_callableB, variadic_tvt_callableB2)
self.assertEqual(variadic_tvt_callableB2, variadic_tvt_callableB3)

def test_wrong_amount_of_parameters(self):
T = TypeVar('T')
T2 = TypeVar("T2")
P = ParamSpec('P')
ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,))
TwoT = TypeAliasType("TwoT", Union[List[T], Set[T2]], type_params=(T, T2))
CallablePT = TypeAliasType("CallablePT", Callable[P, T], type_params=(P, T))

# Not enough parameters
test_cases = [
# not_enough
(TwoT[int], [(int,), ()]),
(TwoT[T], [(T,), (T,)]),
# callable and not enough
(CallablePT[int], [(int,), ()]),
# too many
(ListOrSetT[int, bool], [(int, bool), ()]),
# callable and too many
(CallablePT[str, float, int], [(str, float, int), ()]),
# Check if TypeVar is still present even if over substituted
(ListOrSetT[int, T], [(int, T), (T,)]),
# With and without list for ParamSpec
(CallablePT[str, float, T], [(str, float, T), (T,)]),
(CallablePT[[str], float, int, T2], [([str], float, int, T2), (T2,)]),
]

for index, (alias, [expected_args, expected_params]) in enumerate(test_cases):
with self.subTest(index=index, alias=alias):
self.assertEqual(get_args(alias), expected_args)
self.assertEqual(alias.__parameters__, expected_params)

def test_list_argument(self):
# NOTE: These cases could be seen as valid but result in a parameterless
# variable. If these tests fail the specificiation might have changed

# Callable
Daraan marked this conversation as resolved.
Show resolved Hide resolved
P = ParamSpec('P')
CallableP = TypeAliasType("CallableP", Callable[P, Any], type_params=(P,))
CallableT_list = CallableP[[T]]
self.assertEqual(get_args(CallableT_list), ([T],))
self.assertEqual(CallableT_list.__parameters__, ())
with self.assertRaises(TypeError, msg="is not a generic class"):
CallableT_list[str]

ImplicitConcatP = CallableP[[int, P]]
self.assertEqual(get_args(ImplicitConcatP), ([int, P],))
self.assertEqual(ImplicitConcatP.__parameters__, ())
with self.assertRaises(TypeError, msg="is not a generic class"):
ImplicitConcatP[str]

# TypeVarTuple
Ts = TypeVarTuple("Ts")
Variadic = TypeAliasType("Variadic", Tuple[int, Unpack[Ts]], type_params=(Ts,))
invalid_tupleT = Variadic[[int, T]]
self.assertEqual(get_args(invalid_tupleT), ([int, T],))
self.assertEqual(invalid_tupleT.__parameters__, ())
with self.assertRaises(TypeError, msg="is not a generic class"):
invalid_tupleT[str]

# The condition should align with the version of GeneriAlias usage in __getitem__
@skipIf(TYPING_3_10_0, "Most cases are allowed in 3.11+ or with GenericAlias")
def test_invalid_cases_before_3_11(self):
T = TypeVar('T')
ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,))
with self.assertRaises(TypeError):
ListOrSetT[Generic[T]]
with self.assertRaises(TypeError):
ListOrSetT[(Generic[T], )]

def test_alias_attributes(self):
T = TypeVar('T')
T2 = TypeVar('T2')
Expand Down
44 changes: 37 additions & 7 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3518,6 +3518,8 @@ def __init__(self, name: str, value, *, type_params=()):
self.__type_params__ = type_params

parameters = []
if not isinstance(type_params, tuple):
raise TypeError("type_params must be a tuple")
for type_param in type_params:
if isinstance(type_param, TypeVarTuple):
parameters.extend(type_param)
Expand Down Expand Up @@ -3555,6 +3557,34 @@ def _raise_attribute_error(self, name: str) -> Never:
def __repr__(self) -> str:
return self.__name__

if sys.version_info < (3, 11):
def _check_single_param(self, param, recursion=0):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't seem like this needs to be a generator, it always yields a single value. Let's make it return that value instead and simplify _check_parameters.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at it again. As the current cpython implementation does not not seems to use _type_convert or any restrictions, should we just drop all checks here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes in #489 are necessary to remove _check_parameters / _type_convert. More specifically, _ConcatenateGenericAlias needs _copy_with and __getitem__ if it is not converted to a list via the above functions to be handled correctly in 3.8 & 3.9.

# Allow [], [int], [int, str], [int, ...], [int, T]
if param is ...:
yield ...
# Note in < 3.9 _ConcatenateGenericAlias inherits from list
elif isinstance(param, list) and recursion == 0:
yield [checked
for arg in param
for checked in self._check_single_param(arg, recursion+1)]
else:
yield typing._type_check(
param, f'Subscripting {self.__name__} requires a type.'
)

def _check_parameters(self, parameters):
if sys.version_info < (3, 11):
return tuple(
checked
for item in parameters
for checked in self._check_single_param(item)
)
return tuple(typing._type_check(
item, f'Subscripting {self.__name__} requires a type.'
)
for item in parameters
)

def __getitem__(self, parameters):
if not self.__type_params__:
raise TypeError("Only generic type aliases are subscriptable")
Expand All @@ -3563,13 +3593,13 @@ def __getitem__(self, parameters):
# Using 3.9 here will create problems with Concatenate
if sys.version_info >= (3, 10):
return _types.GenericAlias(self, parameters)
parameters = tuple(
typing._type_check(
item, f'Subscripting {self.__name__} requires a type.'
)
for item in parameters
)
return _TypeAliasGenericAlias(self, parameters)
type_vars = _collect_type_vars(parameters)
parameters = self._check_parameters(parameters)
alias = _TypeAliasGenericAlias(self, parameters)
# If Concatenate is present its parameters were not collected
if len(alias.__parameters__) < len(type_vars):
alias.__parameters__ = tuple(type_vars)
return alias

def __reduce__(self):
return self.__name__
Expand Down