-
-
Notifications
You must be signed in to change notification settings - Fork 110
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
Changes from all commits
b6bc323
79985f3
02025d4
3199b7b
f7d79d9
c4c0e68
d6af983
0a5039b
e532429
9911ac7
e1b3095
408ae2e
621085f
b3e6b7a
c3d98c6
8255667
255de76
3037923
b8ae82e
8d2ec0a
7029d51
02fd0ba
5c0938c
b6fefb0
af0a133
f2aa35c
6b1bafb
7c1fea7
efa1214
66eebb1
df10751
889e9ae
b6b5a14
9562635
0033813
014109c
0b3ce7d
0ae4c63
144c7b8
e613294
e44fdcc
6bc1f57
e8bfa30
617656d
41a87b8
a8c4bda
249b869
e71902e
b8799ce
5bc1360
3ae9e35
9878cc0
3863c8b
8c86619
251d312
b3aa598
07ba7b7
28d1e84
a365355
fb55b88
f2d6890
054f083
eb840ef
02efbdc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7267,6 +7267,80 @@ 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 : (), | ||
... : (), | ||
None : (), | ||
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,), | ||
Unpack[Ts] : (Ts,), | ||
Unpack[Tuple[int, T2]] : (T2,), | ||
Concatenate[int, P] : (P,), | ||
# Not tested usage of bare TypeVarTuple, would need 3.11+ | ||
# Ts : (Ts,), # invalid case | ||
} | ||
|
||
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)), | ||
] | ||
|
||
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__, ()) | ||
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): | ||
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"): | ||
|
@@ -7327,12 +7401,19 @@ 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.assertRaisesRegex(TypeError, | ||
"not a generic class" | ||
# types.GenericAlias raises a different error in 3.10 | ||
if sys.version_info[:2] != (3, 10) | ||
else "There are no type variables left in ListOrSetT" | ||
): | ||
subscripted[int] | ||
|
||
|
||
still_generic = ListOrSetT[Iterable[T]] | ||
self.assertEqual(get_args(still_generic), (Iterable[T],)) | ||
|
@@ -7341,6 +7422,114 @@ def test_getitem(self): | |
self.assertEqual(get_args(fully_subscripted), (Iterable[float],)) | ||
self.assertIs(get_origin(fully_subscripted), ListOrSetT) | ||
|
||
ValueWithoutTypeVar = TypeAliasType("ValueWithoutTypeVar", 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') | ||
P2 = ParamSpec('P2') | ||
CallableP = TypeAliasType("CallableP", Callable[P, Any], type_params=(P,)) | ||
|
||
callable_concat = CallableP[Concatenate[int, P2]] | ||
self.assertEqual(callable_concat.__parameters__, (P2,)) | ||
concat_usage = callable_concat[str] | ||
with self.subTest("get_args of Concatenate in TypeAliasType"): | ||
if not TYPING_3_9_0: | ||
# args are: ([<class 'int'>, ~P2],) | ||
self.skipTest("Nested ParamSpec is not substituted") | ||
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]]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since using |
||
|
||
def test_substitution(self): | ||
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)) | ||
|
||
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) | ||
|
||
# The condition should align with the version of GeneriAlias usage in __getitem__ or be 3.11+ | ||
@skipIf(TYPING_3_10_0, "Most arguments are allowed in 3.11+ or with GenericAlias") | ||
def test_invalid_cases_before_3_10(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_unpack_parameter_collection(self): | ||
Ts = TypeVarTuple("Ts") | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3662,6 +3662,33 @@ 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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changes in #489 are necessary to remove |
||
# Allow [], [int], [int, str], [int, ...], [int, T] | ||
if param is ...: | ||
return ... | ||
if param is None: | ||
return None | ||
# Note in <= 3.9 _ConcatenateGenericAlias inherits from list | ||
if isinstance(param, list) and recursion == 0: | ||
return [self._check_single_param(arg, recursion+1) | ||
for arg in param] | ||
return typing._type_check( | ||
param, f'Subscripting {self.__name__} requires a type.' | ||
) | ||
|
||
def _check_parameters(self, parameters): | ||
if sys.version_info < (3, 11): | ||
return tuple( | ||
self._check_single_param(item) | ||
for item in parameters | ||
) | ||
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") | ||
|
@@ -3670,13 +3697,14 @@ 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) | ||
# alias.__parameters__ is not complete if Concatenate is present | ||
# as it is converted to a list from which no parameters are extracted. | ||
if alias.__parameters__ != type_vars: | ||
alias.__parameters__ = type_vars | ||
return alias | ||
|
||
def __reduce__(self): | ||
return self.__name__ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to check if #489 or #491 allow to execute this test.