8000 Support ParamSpec for TypeAliasType (#449) · python/typing_extensions@f2d0667 · GitHub
[go: up one dir, main page]

Skip to content

Commit f2d0667

Browse files
DaraanAlexWaygood
andauthored
Support ParamSpec for TypeAliasType (#449)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
1 parent b7d6353 commit f2d0667

File tree

3 files changed

+229
-9
lines changed

3 files changed

+229
-9
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ aliases that have a `Concatenate` special form as their argument.
2626
- Backport CPython PR [#124795](https://github.com/python/cpython/pull/124795):
2727
fix `TypeAliasType` not raising an error on non-tuple inputs for `type_params`.
2828
Patch by [Daraan](https://github.com/Daraan).
29+
- Fix that lists and ... could not be used for parameter expressions for `TypeAliasType`
30+
instances before Python 3.11.
31+
Patch by [Daraan](https://github.com/Daraan).
2932

3033
# Release 4.12.2 (June 7, 2024)
3134

src/test_typing_extensions.py

Lines changed: 191 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7267,6 +7267,80 @@ def test_attributes(self):
72677267
self.assertEqual(Variadic.__type_params__, (Ts,))
72687268
self.assertEqual(Variadic.__parameters__, tuple(iter(Ts)))
72697269

7270+
P = ParamSpec('P')
7271+
CallableP = TypeAliasType("CallableP", Callable[P, Any], type_params=(P, ))
7272+
self.assertEqual(CallableP.__name__, "CallableP")
7273+
self.assertEqual(CallableP.__value__, Callable[P, Any])
7274+
self.assertEqual(CallableP.__type_params__, (P,))
7275+
self.assertEqual(CallableP.__parameters__, (P,))
7276+
7277+
def test_alias_types_and_substitutions(self):
7278+
T = TypeVar('T')
7279+
T2 = TypeVar('T2')
7280+
T_default = TypeVar("T_default", default=int)
7281+
Ts = TypeVarTuple("Ts")
7282+
P = ParamSpec('P')
7283+
7284+
test_argument_cases = {
7285+
# arguments : expected parameters
7286+
int : (),
7287+
... : (),
7288+
None : (),
7289+
T2 : (T2,),
7290+
Union[int, List[T2]] : (T2,),
7291+
Tuple[int, str] : (),
7292+
Tuple[T, T_default, T2] : (T, T_default, T2),
7293+
Tuple[Unpack[Ts]] : (Ts,),
7294+
Callable[[Unpack[Ts]], T2] : (Ts, T2),
7295+
Callable[P, T2] : (P, T2),
7296+
Callable[Concatenate[T2, P], T_default] : (T2, P, T_default),
7297+
TypeAliasType("NestedAlias", List[T], type_params=(T,))[T2] : (T2,),
7298+
Unpack[Ts] : (Ts,),
7299+
Unpack[Tuple[int, T2]] : (T2,),
7300+
Concatenate[int, P] : (P,),
7301+
# Not tested usage of bare TypeVarTuple, would need 3.11+
7302+
# Ts : (Ts,), # invalid case
7303+
}
7304+
7305+
test_alias_cases = [
7306+
# Simple cases
7307+
TypeAliasType("ListT", List[T], type_params=(T,)),
7308+
TypeAliasType("UnionT", Union[int, List[T]], type_params=(T,)),
7309+
# Value has no parameter but in type_param
7310+
TypeAliasType("ValueWithoutT", int, type_params=(T,)),
7311+
# Callable
7312+
TypeAliasType("CallableP", Callable[P, Any], type_params=(P, )),
7313+
TypeAliasType("CallableT", Callable[..., T], type_params=(T, )),
7314+
TypeAliasType("CallableTs", Callable[[Unpack[Ts]], Any], type_params=(Ts, )),
7315+
# TypeVarTuple
7316+
TypeAliasType("Variadic", Tuple[int, Unpack[Ts]], type_params=(Ts,)),
7317+
# TypeVar with default
7318+
TypeAliasType("TupleT_default", Tuple[T_default, T], type_params=(T, T_default)),
7319+
TypeAliasType("CallableT_default", Callable[[T], T_default], type_params=(T, T_default)),
7320+
]
7321+
7322+
for alias in test_alias_cases:
7323+
with self.subTest(alias=alias, args=[]):
7324+
subscripted = alias[[]]
7325+
self.assertEqual(get_args(subscripted), ([],))
7326+
self.assertEqual(subscripted.__parameters__, ())
7327+
with self.subTest(alias=alias, args=()):
7328+
subscripted = alias[()]
7329+
self.assertEqual(get_args(subscripted), ())
7330+
self.assertEqual(subscripted.__parameters__, ())
7331+
with self.subTest(alias=alias, args=(int, float)):
7332+
subscripted = alias[int, float]
7333+
self.assertEqual(get_args(subscripted), (int, float))
7334+
self.assertEqual(subscripted.__parameters__, ())
7335+
with self.subTest(alias=alias, args=[int, float]):
7336+
subscripted = alias[[int, float]]
7337+
self.assertEqual(get_args(subscripted), ([int, float],))
7338+
self.assertEqual(subscripted.__parameters__, ())
7339+
for expected_args, expected_parameters in test_argument_cases.items():
7340+
with self.subTest(alias=alias, args=expected_args):
7341+
self.assertEqual(get_args(alias[expected_args]), (expected_args,))
7342+
self.assertEqual(alias[expected_args].__parameters__, expected_parameters)
7343+
72707344
def test_cannot_set_attributes(self):
72717345
Simple = TypeAliasType("Simple", int)
72727346
with self.assertRaisesRegex(AttributeError, "readonly attribute"):
@@ -7327,12 +7401,19 @@ def test_or(self):
73277401
Alias | "Ref"
73287402

73297403
def test_getitem(self):
7404+
T = TypeVar('T')
73307405
ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,))
73317406
subscripted = ListOrSetT[int]
73327407
self.assertEqual(get_args(subscripted), (int,))
73337408
self.assertIs(get_origin(subscripted), ListOrSetT)
7334-
with self.assertRaises(TypeError):
7335-
subscripted[str]
7409+
with self.assertRaisesRegex(TypeError,
7410+
"not a generic class"
7411+
# types.GenericAlias raises a different error in 3.10
7412+
if sys.version_info[:2] != (3, 10)
7413+
else "There are no type variables left in ListOrSetT"
7414+
):
7415+
subscripted[int]
7416+
73367417

73377418
still_generic = ListOrSetT[Iterable[T]]
73387419
self.assertEqual(get_args(still_generic), (Iterable[T],))
@@ -7341,6 +7422,114 @@ def test_getitem(self):
73417422
self.assertEqual(get_args(fully_subscripted), (Iterable[float],))
73427423
self.assertIs(get_origin(fully_subscripted), ListOrSetT)
73437424

7425+
ValueWithoutTypeVar = TypeAliasType("ValueWithoutTypeVar", int, type_params=(T,))
7426+
still_subscripted = ValueWithoutTypeVar[str]
7427+
self.assertEqual(get_args(still_subscripted), (str,))
7428+
7429+
def test_callable_without_concatenate(self):
7430+
P = ParamSpec('P')
7431+
CallableP = TypeAliasType("CallableP", Callable[P, Any], type_params=(P,))
7432+
get_args_test_cases = [
7433+
# List of (alias, expected_args)
7434+
# () -> Any
7435+
(CallableP[()], ()),
7436+
(CallableP[[]], ([],)),
7437+
# (int) -> Any
7438+
(CallableP[int], (int,)),
7439+
(CallableP[[int]], ([int],)),
7440+
# (int, int) -> Any
7441+
(CallableP[int, int], (int, int)),
7442+
(CallableP[[int, int]], ([int, int],)),
7443+
# (...) -> Any
7444+
(CallableP[...], (...,)),
7445+
# (int, ...) -> Any
7446+
(CallableP[[int, ...]], ([int, ...],)),
7447+
]
7448+
7449+
for index, (expression, expected_args) in enumerate(get_args_test_cases):
7450+
with self.subTest(index=index, expression=expression):
7451+
self.assertEqual(get_args(expression), expected_args)
7452+
7453+
self.assertEqual(CallableP[...], CallableP[(...,)])
7454+
# (T) -> Any
7455+
CallableT = CallableP[T]
7456+
self.assertEqual(get_args(CallableT), (T,))
7457+
self.assertEqual(CallableT.__parameters__, (T,))
7458+
7459+
def test_callable_with_concatenate(self):
7460+
P = ParamSpec('P')
7461+
P2 = ParamSpec('P2')
7462+
CallableP = TypeAliasType("CallableP", Callable[P, Any], type_params=(P,))
7463+
7464+
callable_concat = CallableP[Concatenate[int, P2]]
7465+
self.assertEqual(callable_concat.__parameters__, (P2,))
7466+
concat_usage = callable_concat[str]
7467+
with self.subTest("get_args of Concatenate in TypeAliasType"):
7468+
if not TYPING_3_9_0:
7469+
# args are: ([<class 'int'>, ~P2],)
7470+
self.skipTest("Nested ParamSpec is not substituted")
7471+
if sys.version_info < (3, 10, 2):
7472+
self.skipTest("GenericAlias keeps Concatenate in __args__ prior to 3.10.2")
7473+
self.assertEqual(get_args(concat_usage), ((int, str),))
7474+
with self.subTest("Equality of parameter_expression without []"):
7475+
if not TYPING_3_10_0:
7476+
self.skipTest("Nested list is invalid type form")
7477+
self.assertEqual(concat_usage, callable_concat[[str]])
7478+
7479+
def test_substitution(self):
7480+
T = TypeVar('T')
7481+
Ts = TypeVarTuple("Ts")
7482+
7483+
CallableTs = TypeAliasType("CallableTs", Callable[[Unpack[Ts]], Any], type_params=(Ts, ))
7484+
unpack_callable = CallableTs[Unpack[Tuple[int, T]]]
7485+
self.assertEqual(get_args(unpack_callable), (Unpack[Tuple[int, T]],))
7486+
7487+
P = ParamSpec('P')
7488+
CallableP = TypeAliasType("CallableP", Callable[P, T], type_params=(P, T))
7489+
callable_concat = CallableP[Concatenate[int, P], Any]
7490+
self.assertEqual(get_args(callable_concat), (Concatenate[int, P], Any))
7491+
7492+
def test_wrong_amount_of_parameters(self):
7493+
T = TypeVar('T')
7494+
T2 = TypeVar("T2")
7495+
P = ParamSpec('P')
7496+
ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,))
7497+
TwoT = TypeAliasType("TwoT", Union[List[T], Set[T2]], type_params=(T, T2))
7498+
CallablePT = TypeAliasType("CallablePT", Callable[P, T], type_params=(P, T))
7499+
7500+
# Not enough parameters
7501+
test_cases = [
7502+
# not_enough
7503+
(TwoT[int], [(int,), ()]),
7504+
(TwoT[T], [(T,), (T,)]),
7505+
# callable and not enough
7506+
(CallablePT[int], [(int,), ()]),
7507+
# too many
7508+
(ListOrSetT[int, bool], [(int, bool), ()]),
7509+
# callable and too many
7510+
(CallablePT[str, float, int], [(str, float, int), ()]),
7511+
# Check if TypeVar is still present even if over substituted
7512+
(ListOrSetT[int, T], [(int, T), (T,)]),
7513+
# With and without list for ParamSpec
7514+
(CallablePT[str, float, T], [(str, float, T), (T,)]),
7515+
(CallablePT[[str], float, int, T2], [([str], float, int, T2), (T2,)]),
7516+
]
7517+
7518+
for index, (alias, [expected_args, expected_params]) in enumerate(test_cases):
7519+
with self.subTest(index=index, alias=alias):
7520+
self.assertEqual(get_args(alias), expected_args)
7521+
self.assertEqual(alias.__parameters__, expected_params)
7522+
7523+
# The condition should align with the version of GeneriAlias usage in __getitem__ or be 3.11+
7524+
@skipIf(TYPING_3_10_0, "Most arguments are allowed in 3.11+ or with GenericAlias")
7525+
def test_invalid_cases_before_3_10(self):
7526+
T = TypeVar('T')
7527+
ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,))
7528+
with self.assertRaises(TypeError):
7529+
ListOrSetT[Generic[T]]
7530+
with self.assertRaises(TypeError):
7531+
ListOrSetT[(Generic[T], )]
7532+
73447533
def test_unpack_parameter_collection(self):
73457534
Ts = TypeVarTuple("Ts")
73467535

src/typing_extensions.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3662,6 +3662,33 @@ def _raise_attribute_error(self, name: str) -> Never:
36623662
def __repr__(self) -> str:
36633663
return self.__name__
36643664

3665+
if sys.version_info < (3, 11):
3666+
def _check_single_param(self, param, recursion=0):
3667+
# Allow [], [int], [int, str], [int, ...], [int, T]
3668+
if param is ...:
3669+
return ...
3670+
if param is None:
3671+
return None
3672+
# Note in <= 3.9 _ConcatenateGenericAlias inherits from list
3673+
if isinstance(param, list) and recursion == 0:
3674+
return [self._check_single_param(arg, recursion+1)
3675+
for arg in param]
3676+
return typing._type_check(
3677+
param, f'Subscripting {self.__name__} requires a type.'
3678+
)
3679+
3680+
def _check_parameters(self, parameters):
3681+
if sys.version_info < (3, 11):
3682+
return tuple(
3683+
self._check_single_param(item)
3684+
for item in parameters
3685+
)
3686+
return tuple(typing._type_check(
3687+
item, f'Subscripting {self.__name__} requires a type.'
3688+
)
3689+
for item in parameters
3690+
)
3691+
36653692
def __getitem__(self, parameters):
36663693
if not self.__type_params__:
36673694
raise TypeError("Only generic type aliases are subscriptable")
@@ -3670,13 +3697,14 @@ def __getitem__(self, parameters):
36703697
# Using 3.9 here will create problems with Concatenate
36713698
if sys.version_info >= (3, 10):
36723699
return _types.GenericAlias(self, parameters)
3673-
parameters = tuple(
3674-
typing._type_check(
3675-
item, f'Subscripting {self.__name__} requires a type.'
3676-
)
3677-
for item in parameters
3678-
)
3679-
return _TypeAliasGenericAlias(self, parameters)
3700+
type_vars = _collect_type_vars(parameters)
3701+
parameters = self._check_parameters(parameters)
3702+
alias = _TypeAliasGenericAlias(self, parameters)
3703+
# alias.__parameters__ is not complete if Concatenate is present
3704+
# as it is converted to a list from which no parameters are extracted.
3705+
if alias.__parameters__ != type_vars:
3706+
alias.__parameters__ = type_vars
3707+
return alias
36803708

36813709
def __reduce__(self):
36823710
return self.__name__

0 commit comments

Comments
 (0)
0