Skip to content

Commit

Permalink
Support Ellipsis argument to Concatenate (#481)
Browse files Browse the repository at this point in the history
  • Loading branch information
Daraan authored Oct 22, 2024
1 parent 80958f3 commit 3ebe884
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 32 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
- Backport to Python 3.10 the ability to substitute `...` in generic `Callable`
aliases that have a `Concatenate` special form as their argument.
Patch by [Daraan](https://github.com/Daraan).
- Extended the `Concatenate` backport for Python 3.8-3.10 to now accept
`Ellipsis` as an argument. Patch by [Daraan](https://github.com/Daraan).
- Fix error in subscription of `Unpack` aliases causing nested Unpacks
to not be resolved correctly. Patch by [Daraan](https://github.com/Daraan).

Expand Down
2 changes: 1 addition & 1 deletion doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ Special typing primitives
See :py:data:`typing.Concatenate` and :pep:`612`. In ``typing`` since 3.10.

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

.. data:: Final

Expand Down
83 changes: 60 additions & 23 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1720,12 +1720,14 @@ class C(Generic[T]): pass
# In 3.9 and lower we use typing_extensions's hacky implementation
# of ParamSpec, which gets incorrectly wrapped in a list
self.assertIn(get_args(Callable[P, int]), [(P, int), ([P], int)])
self.assertEqual(get_args(Callable[Concatenate[int, P], int]),
(Concatenate[int, P], int))
self.assertEqual(get_args(Required[int]), (int,))
self.assertEqual(get_args(NotRequired[int]), (int,))
self.assertEqual(get_args(Unpack[Ts]), (Ts,))
self.assertEqual(get_args(Unpack), ())
self.assertEqual(get_args(Callable[Concatenate[int, P], int]),
(Concatenate[int, P], int))
self.assertEqual(get_args(Callable[Concatenate[int, ...], int]),
(Concatenate[int, ...], int))


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

G3 = klass[int, Concatenate[int, ...]]
self.assertEqual(G3.__args__, (int, Concatenate[int, ...]))
self.assertEqual(G3.__parameters__, ())

# The following are some valid uses cases in PEP 612 that don't work:
# These do not work in 3.9, _type_check blocks the list and ellipsis.
# G3 = X[int, [int, bool]]
Expand Down Expand Up @@ -5362,21 +5368,28 @@ class MyClass: ...
c = Concatenate[MyClass, P]
self.assertNotEqual(c, Concatenate)

# Test Ellipsis Concatenation
d = Concatenate[MyClass, ...]
self.assertNotEqual(d, c)
self.assertNotEqual(d, Concatenate)

def test_valid_uses(self):
P = ParamSpec('P')
T = TypeVar('T')
for callable_variant in (Callable, collections.abc.Callable):
with self.subTest(callable_variant=callable_variant):
if not TYPING_3_9_0 and callable_variant is collections.abc.Callable:
self.skipTest("Needs PEP 585")

C1 = Callable[Concatenate[int, P], int]
C2 = Callable[Concatenate[int, T, P], T]
self.assertEqual(C1.__origin__, C2.__origin__)
self.assertNotEqual(C1, C2)
C1 = callable_variant[Concatenate[int, P], int]
C2 = callable_variant[Concatenate[int, T, P], T]
self.assertEqual(C1.__origin__, C2.__origin__)
self.assertNotEqual(C1, C2)

# Test collections.abc.Callable too.
if sys.version_info[:2] >= (3, 9):
C3 = collections.abc.Callable[Concatenate[int, P], int]
C4 = collections.abc.Callable[Concatenate[int, T, P], T]
self.assertEqual(C3.__origin__, C4.__origin__)
self.assertNotEqual(C3, C4)
C3 = callable_variant[Concatenate[int, ...], int]
C4 = callable_variant[Concatenate[int, T, ...], T]
self.assertEqual(C3.__origin__, C4.__origin__)
self.assertNotEqual(C3, C4)

def test_invalid_uses(self):
P = ParamSpec('P')
Expand All @@ -5390,16 +5403,30 @@ def test_invalid_uses(self):

with self.assertRaisesRegex(
TypeError,
'The last parameter to Concatenate should be a ParamSpec variable',
'The last parameter to Concatenate should be a ParamSpec variable or ellipsis',
):
Concatenate[P, T]

if not TYPING_3_11_0:
with self.assertRaisesRegex(
TypeError,
'each arg must be a type',
):
Concatenate[1, P]
# Test with tuple argument
with self.assertRaisesRegex(
TypeError,
"The last parameter to Concatenate should be a ParamSpec variable or ellipsis.",
):
Concatenate[(P, T)]

with self.assertRaisesRegex(
TypeError,
'is not a generic class',
):
Callable[Concatenate[int, ...], Any][Any]

# Assure that `_type_check` is called.
P = ParamSpec('P')
with self.assertRaisesRegex(
TypeError,
"each arg must be a type",
):
Concatenate[(str,), P]

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

C1 = X[...]
self.assertEqual(C1.__parameters__, ())
with self.subTest("Compare Concatenate[int, ...]"):
if sys.version_info[:2] == (3, 10):
self.skipTest("Needs Issue #110 | PR #481: construct Concatenate with ...")
self.assertEqual(get_args(C1), (Concatenate[int, ...], Any))
self.assertEqual(get_args(C1), (Concatenate[int, ...], Any))

def test_basic_introspection(self):
P = ParamSpec('P')
C1 = Concatenate[int, P]
C2 = Concatenate[int, T, P]
C3 = Concatenate[int, ...]
C4 = Concatenate[int, T, ...]
self.assertEqual(C1.__origin__, Concatenate)
self.assertEqual(C1.__args__, (int, P))
self.assertEqual(C2.__origin__, Concatenate)
self.assertEqual(C2.__args__, (int, T, P))
self.assertEqual(C3.__origin__, Concatenate)
self.assertEqual(C3.__args__, (int, Ellipsis))
self.assertEqual(C4.__origin__, Concatenate)
self.assertEqual(C4.__args__, (int, T, Ellipsis))

def test_eq(self):
P = ParamSpec('P')
Expand All @@ -5431,6 +5461,13 @@ def test_eq(self):
self.assertEqual(hash(C1), hash(C2))
self.assertNotEqual(C1, C3)

C4 = Concatenate[int, ...]
C5 = Concatenate[int, ...]
C6 = Concatenate[int, T, ...]
self.assertEqual(C4, C5)
self.assertEqual(hash(C4), hash(C5))
self.assertNotEqual(C4, C6)


class TypeGuardTests(BaseTestCase):
def test_basics(self):
Expand Down
41 changes: 33 additions & 8 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1818,26 +1818,51 @@ def copy_with(self, params):
return super(_typing_ConcatenateGenericAlias, self).copy_with(params)


# 3.8-3.9.2
class _EllipsisDummy: ...


# 3.8-3.10
def _create_concatenate_alias(origin, parameters):
if parameters[-1] is ... and sys.version_info < (3, 9, 2):
# Hack: Arguments must be types, replace it with one.
parameters = (*parameters[:-1], _EllipsisDummy)
if sys.version_info >= (3, 10, 2):
concatenate = _ConcatenateGenericAlias(origin, parameters,
_typevar_types=(TypeVar, ParamSpec),
_paramspec_tvars=True)
else:
concatenate = _ConcatenateGenericAlias(origin, parameters)
if parameters[-1] is not _EllipsisDummy:
return concatenate
# Remove dummy again
concatenate.__args__ = tuple(p if p is not _EllipsisDummy else ...
for p in concatenate.__args__)
if sys.version_info < (3, 10):
# backport needs __args__ adjustment only
return concatenate
concatenate.__parameters__ = tuple(p for p in concatenate.__parameters__
if p is not _EllipsisDummy)
return concatenate


# 3.8-3.10
@typing._tp_cache
def _concatenate_getitem(self, parameters):
if parameters == ():
raise TypeError("Cannot take a Concatenate of no types.")
if not isinstance(parameters, tuple):
parameters = (parameters,)
elif not (parameters[-1] is ... or isinstance(parameters[-1], ParamSpec)):
if not (parameters[-1] is ... or isinstance(parameters[-1], ParamSpec)):
raise TypeError("The last parameter to Concatenate should be a "
"ParamSpec variable or ellipsis.")
msg = "Concatenate[arg, ...]: each arg must be a type."
parameters = tuple(typing._type_check(p, msg) for p in parameters)
if (3, 10, 2) < sys.version_info < (3, 11):
return _ConcatenateGenericAlias(self, parameters,
_typevar_types=(TypeVar, ParamSpec),
_paramspec_tvars=True)
return _ConcatenateGenericAlias(self, parameters)
parameters = (*(typing._type_check(p, msg) for p in parameters[:-1]),
parameters[-1])
return _create_concatenate_alias(self, parameters)


# 3.11+
# 3.11+; Concatenate does not accept ellipsis in 3.10
if sys.version_info >= (3, 11):
Concatenate = typing.Concatenate
# 3.9-3.10
Expand Down

0 comments on commit 3ebe884

Please sign in to comment.