Skip to content

Commit

Permalink
Concatenate supports Ellipsis in python3.9-3.10
Browse files Browse the repository at this point in the history
Added tests for Ellipsis and Generics
  • Loading branch information
Daraan committed Aug 8, 2024
1 parent 70cec91 commit 0a2b170
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 24 deletions.
84 changes: 79 additions & 5 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 @@ -5312,7 +5320,7 @@ def run():

# The actual test:
self.assertEqual(result1, result2)


class ConcatenateTests(BaseTestCase):
def test_basics(self):
Expand All @@ -5322,6 +5330,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')
Expand All @@ -5339,6 +5352,32 @@ 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, 9):

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)


@skipIf(TYPING_3_9_0, "Python 3.8 does not support Callable non-type args, i.e. Ellipsis")
def test_callable_with_concat_3_8(self):
# This will fail on Python 3.8
try:
Callable[Concatenate[int, ...], int]
except TypeError as e:
if "each arg must be a type." in str(e):
self.fail("Python 3.8 does not support Callable with non-type args, i.e. Ellipsis")
else:
raise e

def test_invalid_uses(self):
P = ParamSpec('P')
T = TypeVar('T')
Expand All @@ -5351,25 +5390,53 @@ 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]

def test_alias(self):
P = ParamSpec("P")
C1 = Callable[Concatenate[int, P], Any]

c1 : 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 @@ -5379,6 +5446,13 @@ def test_eq(self):
self.assertEqual(C1, C2)
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):
Expand Down Expand Up @@ -6050,7 +6124,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
72 changes: 53 additions & 19 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1773,7 +1773,7 @@ class _ConcatenateGenericAlias(list):
# Flag in 3.8.
_special = False

def __init__(self, origin, args):
def __init__(self, origin, args, _typevar_types=(TypeVar, ParamSpec), _paramspec_tvars=True):
super().__init__(args)
self.__origin__ = origin
self.__args__ = args
Expand All @@ -1795,28 +1795,62 @@ def __parameters__(self):
return tuple(
tp for tp in self.__args__ if isinstance(tp, (typing.TypeVar, ParamSpec))
)
# 3.10+
else:
_ConcatenateGenericAlias = typing._ConcatenateGenericAlias # noqa: F811

# 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:
# need ParamSpec to pass ParamSpec type check only
concatenate = _ConcatenateGenericAlias(self, parameters[:-1] + (_ellipsis_dummy,),
_typevar_types=(TypeVar, ParamSpec),
_paramspec_tvars=True)
concatenate.__parameters__ = tuple(p for p in concatenate.__parameters__ if p is not _ellipsis_dummy)
concatenate.__args__ = tuple(p if p is not _ellipsis_dummy else ... for p in concatenate.__args__)
return concatenate

# 3.10+
if hasattr(typing, 'Concatenate'):
else:
concatenate = _ConcatenateGenericAlias(self, parameters,
_typevar_types=(TypeVar, ParamSpec),
_paramspec_tvars=True)
return 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])
concatenate = _ConcatenateGenericAlias(self, parameters,
_typevar_types=(TypeVar, ParamSpec),
_paramspec_tvars=True)
return concatenate


# 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 0a2b170

Please sign in to comment.