8000 Fix Concatenate and Generic with ParamSpec substitution (#489) · python/typing_extensions@ca41832 · GitHub
[go: up one dir, main page]

Skip to content

Commit ca41832

Browse files
authored
Fix Concatenate and Generic with ParamSpec substitution (#489)
1 parent 700eadd commit ca41832

File tree

2 files changed

+342
-15
lines changed

2 files changed

+342
-15
lines changed

src/test_typing_extensions.py

Lines changed: 171 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3705,6 +3705,10 @@ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T: ...
37053705
self.assertEqual(Y.__parameters__, ())
37063706
self.assertEqual(Y.__args__, ((int, str, str), bytes, memoryview))
37073707

3708+
# Regression test; fixing #126 might cause an error here
3709+
with self.assertRaisesRegex(TypeError, "not a generic class"):
3710+
Y[int]
3711+
37083712
def test_protocol_generic_over_typevartuple(self):
37093713
Ts = TypeVarTuple("Ts")
37103714
T = TypeVar("T")
@@ -5259,6 +5263,7 @@ class X(Generic[T, P]):
52595263
class Y(Protocol[T, P]):
52605264
pass
52615265

5266+
things = "arguments" if sys.version_info >= (3, 10) else "parameters"
52625267
for klass in X, Y:
52635268
with self.subTest(klass=klass.__name__):
52645269
G1 = klass[int, P_2]
@@ -5273,20 +5278,146 @@ class Y(Protocol[T, P]):
52735278
self.assertEqual(G3.__args__, (int, Concatenate[int, ...]))
52745279
self.assertEqual(G3.__parameters__, ())
52755280

5281+
with self.assertRaisesRegex(
5282+
TypeError,
5283+
f"Too few {things} for {klass}"
5284+
):
5285+
klass[int]
5286+
52765287
# The following are some valid uses cases in PEP 612 that don't work:
52775288
# These do not work in 3.9, _type_check blocks the list and ellipsis.
52785289
# G3 = X[int, [int, bool]]
52795290
# G4 = X[int, ...]
52805291
# G5 = Z[[int, str, bool]]
5281-
# Not working because this is special-cased in 3.10.
5282-
# G6 = Z[int, str, bool]
5292+
5293+
def test_single_argument_generic(self):
5294+
P = ParamSpec("P")
5295+
T = TypeVar("T")
5296+
P_2 = ParamSpec("P_2")
5297+
5298+
class Z(Generic[P]):
5299+
pass
5300+
5301+
class ProtoZ(Protocol[P]):
5302+
pass
5303+
5304+
for klass in Z, ProtoZ:
5305+
with self.subTest(klass=klass.__name__):
5306+
# Note: For 3.10+ __args__ are nested tuples here ((int, ),) instead of (int, )
5307+
G6 = klass[int, str, T]
5308+
G6args = G6.__args__[0] if sys.version_info >= (3, 10) else G6.__args__
5309+
self.assertEqual(G6args, (int, str, T))
5310+
self.assertEqual(G6.__parameters__, (T,))
5311+
5312+
# P = [int]
5313+
G7 = klass[int]
5314+
G7args = G7.__args__[0] if sys.version_info >= (3, 10) else G7.__args__
5315+
self.assertEqual(G7args, (int,))
5316+
self.assertEqual(G7.__parameters__, ())
5317+
5318+
G8 = klass[Concatenate[T, ...]]
5319+
self.assertEqual(G8.__args__, (Concatenate[T, ...], ))
5320+
self.assertEqual(G8.__parameters__, (T,))
5321+
5322+
G9 = klass[Concatenate[T, P_2]]
5323+
self.assertEqual(G9.__args__, (Concatenate[T, P_2], ))
5324+
5325+
# This is an invalid form but useful for testing correct subsitution
5326+
G10 = klass[int, Concatenate[str, P]]
5327+
G10args = G10.__args__[0] if sys.version_info >= (3, 10) else G10.__args__
5328+
self.assertEqual(G10args, (int, Concatenate[str, P], ))
5329+
5330+
@skipUnless(TYPING_3_10_0, "ParamSpec not present before 3.10")
5331+
def test_is_param_expr(self):
5332+
P = ParamSpec("P")
5333+
P_typing = typing.ParamSpec("P_typing")
5334+
self.assertTrue(typing_extensions._is_param_expr(P))
5335+
self.assertTrue(typing_extensions._is_param_expr(P_typing))
5336+
if hasattr(typing, "_is_param_expr"):
5337+
self.assertTrue(typing._is_param_expr(P))
5338+
self.assertTrue(typing._is_param_expr(P_typing))
5339+
5340+
def test_single_argument_generic_with_parameter_expressions(self):
5341+
P = ParamSpec("P")
5342+
T = TypeVar("T")
5343+
P_2 = ParamSpec("P_2")
52835344

52845345
class Z(Generic[P]):
52855346
pass
52865347

52875348
class ProtoZ(Protocol[P]):
52885349
pass
52895350

5351+
things = "arguments" if sys.version_info >= (3, 10) else "parameters"
5352+
for klass in Z, ProtoZ:
5353+
with self.subTest(klass=klass.__name__):
5354+
G8 = klass[Concatenate[T, ...]]
5355+
5356+
H8_1 = G8[int]
5357+
self.assertEqual(H8_1.__parameters__, ())
5358+
with self.assertRaisesRegex(TypeError, "not a generic class"):
5359+
H8_1[str]
5360+
5361+
H8_2 = G8[T][int]
5362+
self.assertEqual(H8_2.__parameters__, ())
5363+
with self.assertRaisesRegex(TypeError, "not a generic class"):
5364+
H8_2[str]
5365+
5366+
G9 = klass[Concatenate[T, P_2]]
5367+
self.assertEqual(G9.__parameters__, (T, P_2))
5368+
5369+
with self.assertRaisesRegex(TypeError,
5370+
"The last parameter to Concatenate should be a ParamSpec variable or ellipsis."
5371+
if sys.version_info < (3, 10) else
5372+
# from __typing_subst__
5373+
"Expected a list of types, an ellipsis, ParamSpec, or Concatenate"
5374+
):
5375+
G9[int, int]
5376+
5377+
with self.assertRaisesRegex(TypeError, f"Too few {things}"):
5378+
G9[int]
5379+
5380+
with self.subTest("Check list as parameter expression", klass=klass.__name__):
5381+
if sys.version_info < (3, 10):
5382+
self.skipTest("Cannot pass non-types")
5383+
G5 = klass[[int, str, T]]
5384+
self.assertEqual(G5.__parameters__, (T,))
5385+
self.assertEqual(G5.__args__, ((int, str, T),))
5386+
5387+
H9 = G9[int, [T]]
5388+
self.assertEqual(H9.__parameters__, (T,))
5389+
5390+
# This is an invalid parameter expression but useful for testing correct subsitution
5391+
G10 = klass[int, Concatenate[str, P]]
5392+
with self.subTest("Check invalid form substitution"):
5393+
self.assertEqual(G10.__parameters__, (P, ))
5394+
if sys.version_info < (3, 9):
5395+
self.skipTest("3.8 typing._type_subst does not support this substitution process")
5396+
H10 = G10[int]
5397+
if (3, 10) <= sys.version_info < (3, 11, 3):
5398+
self.skipTest("3.10-3.11.2 does not substitute Concatenate here")
5399+
self.assertEqual(H10.__parameters__, ())
5400+
H10args = H10.__args__[0] if sys.version_info >= (3, 10) else H10.__args__
5401+
self.assertEqual(H10args, (int, (str, int)))
5402+
5403+
@skipUnless(TYPING_3_10_0, "ParamSpec not present before 3.10")
5404+
def test_substitution_with_typing_variants(self):
5405+
# verifies substitution and typing._check_generic working with typing variants
5406+
P = ParamSpec("P")
5407+
typing_P = typing.ParamSpec("typing_P")
5408+
typing_Concatenate = typing.Concatenate[int, P]
5409+
5410+
class Z(Generic[typing_P]):
5411+
pass
5412+
5413+
P1 = Z[typing_P]
5414+
self.assertEqual(P1.__parameters__, (typing_P,))
5415+
self.assertEqual(P1.__args__, (typing_P,))
5416+
5417+
C1 = Z[typing_Concatenate]
5418+
self.assertEqual(C1.__parameters__, (P,))
5419+
self.assertEqual(C1.__args__, (typing_Concatenate,))
5420+
52905421
def test_pickle(self):
52915422
global P, P_co, P_contra, P_default
52925423
P = ParamSpec('P')
< F987 /div>
@@ -5468,6 +5599,43 @@ def test_eq(self):
54685599
self.assertEqual(hash(C4), hash(C5))
54695600
self.assertNotEqual(C4, C6)
54705601

5602+
def test_substitution(self):
5603+
T = TypeVar('T')
5604+
P = ParamSpec('P')
5605+
Ts = TypeVarTuple("Ts")
5606+
5607+
C1 = Concatenate[str, T, ...]
5608+
self.assertEqual(C1[int], Concatenate[str, int, ...])
5609+
5610+
C2 = Concatenate[str, P]
5611+
self.assertEqual(C2[...], Concatenate[str, ...])
5612+
self.assertEqual(C2[int], (str, int))
5613+
U1 = Unpack[Tuple[int, str]]
5614+
U2 = Unpack[Ts]
5615+
self.assertEqual(C2[U1], (str, int, str))
5616+
self.assertEqual(C2[U2], (str, Unpack[Ts]))
5617+
self.assertEqual(C2["U2"], (str, typing.ForwardRef("U2")))
5618+
5619+
if (3, 12, 0) <= sys.version_info < (3, 12, 4):
5620+
with self.assertRaises(AssertionError):
5621+
C2[Unpack[U2]]
5622+
else:
5623+
with self.assertRaisesRegex(TypeError, "must be used with a tuple type"):
5624+
C2[Unpack[U2]]
5625+
5626+
C3 = Concatenate[str, T, P]
5627+
self.assertEqual(C3[int, [bool]], (str, int, bool))
5628+
5629+
@skipUnless(TYPING_3_10_0, "Concatenate not present before 3.10")
5630+
def test_is_param_expr(self):
5631+
P = ParamSpec('P')
5632+
concat = Concatenate[str, P]
5633+
typing_concat = typing.Concatenate[str, P]
5634+
self.assertTrue(typing_extensions._is_param_expr(concat))
5635+
self.assertTrue(typing_extensions._is_param_expr(typing_concat))
5636+
if hasattr(typing, "_is_param_expr"):
5637+
self.assertTrue(typing._is_param_expr(concat))
5638+
self.assertTrue(typing._is_param_expr(typing_concat))
54715639

54725640
class TypeGuardTests(BaseTestCase):
54735641
def test_basics(self):
@@ -7465,11 +7633,9 @@ def test_callable_with_concatenate(self):
74657633
self.assertEqual(callable_concat.__parameters__, (P2,))
74667634
concat_usage = callable_concat[str]
74677635
with self.subTest("get_args of Concatenate in TypeAliasType"):
7468-
if not TYPING_3_9_0:
7636+
if not TYPING_3_10_0:
74697637
# args are: ([<class 'int'>, ~P2],)
74707638
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")
74737639
self.assertEqual(get_args(concat_usage), ((int, str),))
74747640
with self.subTest("Equality of parameter_expression without []"):
74757641
if not TYPING_3_10_0:

0 commit comments

Comments
 (0)
0