Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit ca41832

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

File tree

Expand file treeCollapse file tree

2 files changed

+342
-15
lines changed
Filter options
Expand file treeCollapse file tree

2 files changed

+342
-15
lines changed

‎src/test_typing_extensions.py

Copy file name to clipboardExpand all lines: src/test_typing_extensions.py
+171-5Lines 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')
@@ -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)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.