Skip to content

Commit 3ebe884

Browse files
authored
Support Ellipsis argument to Concatenate (#481)
1 parent 80958f3 commit 3ebe884

File tree

4 files changed

+96
-32
lines changed

4 files changed

+96
-32
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
- Backport to Python 3.10 the ability to substitute `...` in generic `Callable`
2020
aliases that have a `Concatenate` special form as their argument.
2121
Patch by [Daraan](https://github.com/Daraan).
22+
- Extended the `Concatenate` backport for Python 3.8-3.10 to now accept
23+
`Ellipsis` as an argument. Patch by [Daraan](https://github.com/Daraan).
2224
- Fix error in subscription of `Unpack` aliases causing nested Unpacks
2325
to not be resolved correctly. Patch by [Daraan](https://github.com/Daraan).
2426

doc/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ Special typing primitives
178178
See :py:data:`typing.Concatenate` and :pep:`612`. In ``typing`` since 3.10.
179179

180180
The backport does not support certain operations involving ``...`` as
181-
a parameter; see :issue:`48` and :issue:`110` for details.
181+
a parameter; see :issue:`48` and :pr:`481` for details.
182182

183183
.. data:: Final
184184

src/test_typing_extensions.py

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1720,12 +1720,14 @@ class C(Generic[T]): pass
17201720
# In 3.9 and lower we use typing_extensions's hacky implementation
17211721
# of ParamSpec, which gets incorrectly wrapped in a list
17221722
self.assertIn(get_args(Callable[P, int]), [(P, int), ([P], int)])
1723-
self.assertEqual(get_args(Callable[Concatenate[int, P], int]),
1724-
(Concatenate[int, P], int))
17251723
self.assertEqual(get_args(Required[int]), (int,))
17261724
self.assertEqual(get_args(NotRequired[int]), (int,))
17271725
self.assertEqual(get_args(Unpack[Ts]), (Ts,))
17281726
self.assertEqual(get_args(Unpack), ())
1727+
self.assertEqual(get_args(Callable[Concatenate[int, P], int]),
1728+
(Concatenate[int, P], int))
1729+
self.assertEqual(get_args(Callable[Concatenate[int, ...], int]),
1730+
(Concatenate[int, ...], int))
17291731

17301732

17311733
class CollectionsAbcTests(BaseTestCase):
@@ -5267,6 +5269,10 @@ class Y(Protocol[T, P]):
52675269
self.assertEqual(G2.__args__, (int, Concatenate[int, P_2]))
52685270
self.assertEqual(G2.__parameters__, (P_2,))
52695271

5272+
G3 = klass[int, Concatenate[int, ...]]
5273+
self.assertEqual(G3.__args__, (int, Concatenate[int, ...]))
5274+
self.assertEqual(G3.__parameters__, ())
5275+
52705276
# The following are some valid uses cases in PEP 612 that don't work:
52715277
# These do not work in 3.9, _type_check blocks the list and ellipsis.
52725278
# G3 = X[int, [int, bool]]
@@ -5362,21 +5368,28 @@ class MyClass: ...
53625368
c = Concatenate[MyClass, P]
53635369
self.assertNotEqual(c, Concatenate)
53645370

5371+
# Test Ellipsis Concatenation
5372+
d = Concatenate[MyClass, ...]
5373+
self.assertNotEqual(d, c)
5374+
self.assertNotEqual(d, Concatenate)
5375+
53655376
def test_valid_uses(self):
53665377
P = ParamSpec('P')
53675378
T = TypeVar('T')
5379+
for callable_variant in (Callable, collections.abc.Callable):
5380+
with self.subTest(callable_variant=callable_variant):
5381+
if not TYPING_3_9_0 and callable_variant is collections.abc.Callable:
5382+
self.skipTest("Needs PEP 585")
53685383

5369-
C1 = Callable[Concatenate[int, P], int]
5370-
C2 = Callable[Concatenate[int, T, P], T]
5371-
self.assertEqual(C1.__origin__, C2.__origin__)
5372-
self.assertNotEqual(C1, C2)
5384+
C1 = callable_variant[Concatenate[int, P], int]
5385+
C2 = callable_variant[Concatenate[int, T, P], T]
5386+
self.assertEqual(C1.__origin__, C2.__origin__)
5387+
self.assertNotEqual(C1, C2)
53735388

5374-
# Test collections.abc.Callable too.
5375-
if sys.version_info[:2] >= (3, 9):
5376-
C3 = collections.abc.Callable[Concatenate[int, P], int]
5377-
C4 = collections.abc.Callable[Concatenate[int, T, P], T]
5378-
self.assertEqual(C3.__origin__, C4.__origin__)
5379-
self.assertNotEqual(C3, C4)
5389+
C3 = callable_variant[Concatenate[int, ...], int]
5390+
C4 = callable_variant[Concatenate[int, T, ...], T]
5391+
self.assertEqual(C3.__origin__, C4.__origin__)
5392+
self.assertNotEqual(C3, C4)
53805393

53815394
def test_invalid_uses(self):
53825395
P = ParamSpec('P')
@@ -5390,16 +5403,30 @@ def test_invalid_uses(self):
53905403

53915404
with self.assertRaisesRegex(
53925405
TypeError,
5393-
'The last parameter to Concatenate should be a ParamSpec variable',
5406+
'The last parameter to Concatenate should be a ParamSpec variable or ellipsis',
53945407
):
53955408
Concatenate[P, T]
53965409

5397-
if not TYPING_3_11_0:
5398-
with self.assertRaisesRegex(
5399-
TypeError,
5400-
'each arg must be a type',
5401-
):
5402-
Concatenate[1, P]
5410+
# Test with tuple argument
5411+
with self.assertRaisesRegex(
5412+
TypeError,
5413+
"The last parameter to Concatenate should be a ParamSpec variable or ellipsis.",
5414+
):
5415+
Concatenate[(P, T)]
5416+
5417+
with self.assertRaisesRegex(
5418+
TypeError,
5419+
'is not a generic class',
5420+
):
5421+
Callable[Concatenate[int, ...], Any][Any]
5422+
5423+
# Assure that `_type_check` is called.
5424+
P = ParamSpec('P')
5425+
with self.assertRaisesRegex(
5426+
TypeError,
5427+
"each arg must be a type",
5428+
):
5429+
Concatenate[(str,), P]
54035430

54045431
@skipUnless(TYPING_3_10_0, "Missing backport to <=3.9. See issue #48")
54055432
def test_alias_subscription_with_ellipsis(self):
@@ -5408,19 +5435,22 @@ def test_alias_subscription_with_ellipsis(self):
54085435

54095436
C1 = X[...]
54105437
self.assertEqual(C1.__parameters__, ())
5411-
with self.subTest("Compare Concatenate[int, ...]"):
5412-
if sys.version_info[:2] == (3, 10):
5413-
self.skipTest("Needs Issue #110 | PR #481: construct Concatenate with ...")
5414-
self.assertEqual(get_args(C1), (Concatenate[int, ...], Any))
5438+
self.assertEqual(get_args(C1), (Concatenate[int, ...], Any))
54155439

54165440
def test_basic_introspection(self):
54175441
P = ParamSpec('P')
54185442
C1 = Concatenate[int, P]
54195443
C2 = Concatenate[int, T, P]
5444+
C3 = Concatenate[int, ...]
5445+
C4 = Concatenate[int, T, ...]
54205446
self.assertEqual(C1.__origin__, Concatenate)
54215447
self.assertEqual(C1.__args__, (int, P))
54225448
self.assertEqual(C2.__origin__, Concatenate)
54235449
self.assertEqual(C2.__args__, (int, T, P))
5450+
self.assertEqual(C3.__origin__, Concatenate)
5451+
self.assertEqual(C3.__args__, (int, Ellipsis))
5452+
self.assertEqual(C4.__origin__, Concatenate)
5453+
self.assertEqual(C4.__args__, (int, T, Ellipsis))
54245454

54255455
def test_eq(self):
54265456
P = ParamSpec('P')
@@ -5431,6 +5461,13 @@ def test_eq(self):
54315461
self.assertEqual(hash(C1), hash(C2))
54325462
self.assertNotEqual(C1, C3)
54335463

5464+
C4 = Concatenate[int, ...]
5465+
C5 = Concatenate[int, ...]
5466+
C6 = Concatenate[int, T, ...]
5467+
self.assertEqual(C4, C5)
5468+
self.assertEqual(hash(C4), hash(C5))
5469+
self.assertNotEqual(C4, C6)
5470+
54345471

54355472
class TypeGuardTests(BaseTestCase):
54365473
def test_basics(self):

src/typing_extensions.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1818,26 +1818,51 @@ def copy_with(self, params):
18181818
return super(_typing_ConcatenateGenericAlias, self).copy_with(params)
18191819

18201820

1821+
# 3.8-3.9.2
1822+
class _EllipsisDummy: ...
1823+
1824+
1825+
# 3.8-3.10
1826+
def _create_concatenate_alias(origin, parameters):
1827+
if parameters[-1] is ... and sys.version_info < (3, 9, 2):
1828+
# Hack: Arguments must be types, replace it with one.
1829+
parameters = (*parameters[:-1], _EllipsisDummy)
1830+
if sys.version_info >= (3, 10, 2):
1831+
concatenate = _ConcatenateGenericAlias(origin, parameters,
1832+
_typevar_types=(TypeVar, ParamSpec),
1833+
_paramspec_tvars=True)
1834+
else:
1835+
concatenate = _ConcatenateGenericAlias(origin, parameters)
1836+
if parameters[-1] is not _EllipsisDummy:
1837+
return concatenate
1838+
# Remove dummy again
1839+
concatenate.__args__ = tuple(p if p is not _EllipsisDummy else ...
1840+
for p in concatenate.__args__)
1841+
if sys.version_info < (3, 10):
1842+
# backport needs __args__ adjustment only
1843+
return concatenate
1844+
concatenate.__parameters__ = tuple(p for p in concatenate.__parameters__
1845+
if p is not _EllipsisDummy)
1846+
return concatenate
1847+
1848+
18211849
# 3.8-3.10
18221850
@typing._tp_cache
18231851
def _concatenate_getitem(self, parameters):
18241852
if parameters == ():
18251853
raise TypeError("Cannot take a Concatenate of no types.")
18261854
if not isinstance(parameters, tuple):
18271855
parameters = (parameters,)
1828-
elif not (parameters[-1] is ... or isinstance(parameters[-1], ParamSpec)):
1856+
if not (parameters[-1] is ... or isinstance(parameters[-1], ParamSpec)):
18291857
raise TypeError("The last parameter to Concatenate should be a "
18301858
"ParamSpec variable or ellipsis.")
18311859
msg = "Concatenate[arg, ...]: each arg must be a type."
1832-
parameters = tuple(typing._type_check(p, msg) for p in parameters)
1833-
if (3, 10, 2) < sys.version_info < (3, 11):
1834-
return _ConcatenateGenericAlias(self, parameters,
1835-
_typevar_types=(TypeVar, ParamSpec),
1836-
_paramspec_tvars=True)
1837-
return _ConcatenateGenericAlias(self, parameters)
1860+
parameters = (*(typing._type_check(p, msg) for p in parameters[:-1]),
1861+
parameters[-1])
1862+
return _create_concatenate_alias(self, parameters)
18381863

18391864

1840-
# 3.11+
1865+
# 3.11+; Concatenate does not accept ellipsis in 3.10
18411866
if sys.version_info >= (3, 11):
18421867
Concatenate = typing.Concatenate
18431868
# 3.9-3.10

0 commit comments

Comments
 (0)