Skip to content

Commit

Permalink
Concatenate supports Ellipsis in python3.9-3.10
Browse files Browse the repository at this point in the history
  • Loading branch information
Daraan committed Aug 9, 2024
1 parent 70cec91 commit dd275c0
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 22 deletions.
76 changes: 72 additions & 4 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1681,12 +1681,16 @@ 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))
if sys.version_info >= (3, 9):
# Cannot construct Callable[Concatenate[int, ...] with non-types
self.assertEqual(get_args(Callable[Concatenate[int, ...], int]),
(Concatenate[int, ...], int))


class CollectionsAbcTests(BaseTestCase):
Expand Down Expand Up @@ -5228,6 +5232,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 @@ -5323,6 +5331,11 @@ 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')
Expand All @@ -5339,6 +5352,21 @@ def test_valid_uses(self):
self.assertEqual(C3.__origin__, C4.__origin__)
self.assertNotEqual(C3, C4)

# Callable with Ellipsis cannot be constructed in Python3.8
if sys.version_info[:2] <= (3, 8):
return

C5 = Callable[Concatenate[int, ...], int]
C6 = Callable[Concatenate[int, T, ...], T]
self.assertEqual(C5.__origin__, C6.__origin__)
self.assertNotEqual(C5, C6)

# Test collections.abc.Callable too.
C7 = collections.abc.Callable[Concatenate[int, ...], int]
C8 = collections.abc.Callable[Concatenate[int, T, ...], T]
self.assertEqual(C7.__origin__, C8.__origin__)
self.assertNotEqual(C7, C8)

def test_invalid_uses(self):
P = ParamSpec('P')
T = TypeVar('T')
Expand All @@ -5351,25 +5379,58 @@ 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 sys.version_info[:2] >= (3, 9):
# Cannot construct a Callable with Ellipsis in Python3.8 as args must be types
with self.assertRaisesRegex(
TypeError,
'is not a generic class',
):
Callable[Concatenate[int, ...], Any][T]


if not TYPING_3_11_0:
with self.assertRaisesRegex(
TypeError,
'each arg must be a type',
):
Concatenate[1, P]

with self.assertRaisesRegex(
TypeError,
'each arg must be a type.',
):
Concatenate[1, ..., P]

@skipUnless(TYPING_3_11_0, "Cannot be backported to <=3.9"
"Cannot use typing._ConcatenateGenericAlias in 3.10")
def test_alias(self):
P = ParamSpec("P")
C1 = Callable[Concatenate[int, P], Any]
# Python <= 3.9 fails because parameters to generic types must be types.
# For Python 3.10 & typing._ConcatenateGenericAlias will
# as Ellipsis is not supported for ParamSpec
# Fallback to 3.10 & typing_extensions._ConcatenateGenericAlias not implemented
C1[...]

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 @@ -5380,6 +5441,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 Expand Up @@ -6050,7 +6118,7 @@ def test_typing_extensions_defers_when_possible(self):
if sys.version_info < (3, 10, 1):
exclude |= {"Literal"}
if sys.version_info < (3, 11):
exclude |= {'final', 'Any', 'NewType', 'overload'}
exclude |= {'final', 'Any', 'NewType', 'overload', 'Concatenate'}
if sys.version_info < (3, 12):
exclude |= {
'SupportsAbs', 'SupportsBytes',
Expand Down
71 changes: 53 additions & 18 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1795,28 +1795,63 @@ def __parameters__(self):
return tuple(
tp for tp in self.__args__ if isinstance(tp, (typing.TypeVar, ParamSpec))
)
# 3.10+
else:
_ConcatenateGenericAlias = typing._ConcatenateGenericAlias

# 3.10
if sys.version_info[:2] == (3, 10):
_ellipsis_dummy = ParamSpec('_ellipsis_dummy')

# 3.8-3.9
@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,)
if not isinstance(parameters[-1], ParamSpec):
raise TypeError("The last parameter to Concatenate should be a "
"ParamSpec variable.")
msg = "Concatenate[arg, ...]: each arg must be a type."
parameters = tuple(typing._type_check(p, msg) for p in parameters)
return _ConcatenateGenericAlias(self, parameters)

@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,)
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 = (*(typing._type_check(p, msg) for p in parameters[:-1]),
parameters[-1])
if parameters[-1] is Ellipsis:
# Hack: Need ParamSpec as last parameter when passing to typing class in 3.10
parameters = parameters[:-1] + (_ellipsis_dummy,)
concatenate = _ConcatenateGenericAlias(self, parameters,
_typevar_types=(TypeVar, ParamSpec),
_paramspec_tvars=True)
# Remove dummy and replace with Ellipsis again
concatenate.__args__ = tuple(p if p is not _ellipsis_dummy else ...
for p in concatenate.__args__)
concatenate.__parameters__ = tuple(p for p in concatenate.__parameters__
if p is not _ellipsis_dummy)
return concatenate

return _ConcatenateGenericAlias(self, parameters,
_typevar_types=(TypeVar, ParamSpec),
_paramspec_tvars=True)

# 3.10+
if hasattr(typing, 'Concatenate'):
# 3.8-3.9
else:
@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,)
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 = (*(typing._type_check(p, msg) for p in parameters[:-1]),
parameters[-1])
return _ConcatenateGenericAlias(self, parameters)

# 3.11+; Concatenate does not accept ellipsis in 3.10
if hasattr(typing, 'Concatenate') and sys.version_info[:2] >= (3, 11):
Concatenate = typing.Concatenate
_ConcatenateGenericAlias = typing._ConcatenateGenericAlias
# 3.9
# 3.9-3.10
elif sys.version_info[:2] >= (3, 9):
@_ExtensionsSpecialForm
def Concatenate(self, parameters):
Expand Down

0 comments on commit dd275c0

Please sign in to comment.