-
-
Notifications
You must be signed in to change notification settings - Fork 124
Concatenate to support Ellipsis argument #481
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 17 commits
7e0aac8
73d3952
2e5fc74
c716ff1
70d5205
8e4f0be
0991b42
de6a2c6
3fb63a3
3112a80
1eabcb6
3e6ec0a
5b9e4cb
a77d93f
8cba5ab
65270c6
97bdfd1
ff79863
9fda1a1
c092d7e
4e8ef02
c5229f8
c06a9c7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1720,12 +1720,15 @@ 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)) | ||
with self.subTest("Concatenate[int, ...]"): | ||
self.assertEqual(get_args(Callable[Concatenate[int, ...], int]), | ||
(Concatenate[int, ...], int)) | ||
Daraan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
class CollectionsAbcTests(BaseTestCase): | ||
|
@@ -5267,6 +5270,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]] | ||
|
@@ -5362,6 +5369,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') | ||
|
@@ -5371,13 +5383,27 @@ def test_valid_uses(self): | |
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] | ||
with self.subTest("typing.Callable with Ellipsis"): | ||
JelleZijlstra marked this conversation as resolved.
Show resolved
Hide resolved
|
||
C3 = Callable[Concatenate[int, ...], int] | ||
C4 = Callable[Concatenate[int, T, ...], T] | ||
self.assertEqual(C3.__origin__, C4.__origin__) | ||
self.assertNotEqual(C3, C4) | ||
|
||
@skipUnless(TYPING_3_9_0, "Needs PEP 585") | ||
def test_pep585_collections_callable(self): | ||
P = ParamSpec('P') | ||
T = TypeVar('T') | ||
# Test collections.abc.Callable too. | ||
C5 = collections.abc.Callable[Concatenate[int, P], int] | ||
C6 = collections.abc.Callable[Concatenate[int, T, P], T] | ||
self.assertEqual(C5.__origin__, C6.__origin__) | ||
self.assertNotEqual(C5, C6) | ||
|
||
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') | ||
|
@@ -5390,25 +5416,50 @@ 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] | ||
with self.assertRaisesRegex( | ||
TypeError, | ||
'is not a generic class', | ||
): | ||
Callable[Concatenate[int, ...], Any][Any] | ||
|
||
def test_invalid_use(self): | ||
# 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_11_0 or (3, 10, 0) <= sys.version_info < (3, 10, 2), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this case I think I'd want to block it on all 3.10 versions. It's a bad experience if something works on early 3.10 and breaks if people upgrade to a later bugfix release. Better to allow it only on 3.11+. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, the version check seems wrong here, the change was made in 3.10.3 not 3.10.2. |
||
"Cannot be backported to <=3.9. See issue #48" | ||
"Cannot use ... with typing._ConcatenateGenericAlias after 3.10.2") | ||
def test_alias_subscription_with_ellipsis(self): | ||
P = ParamSpec('P') | ||
X = Callable[Concatenate[int, P], Any] | ||
|
||
C1 = X[...] | ||
self.assertEqual(C1.__parameters__, ()) | ||
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') | ||
|
@@ -5419,6 +5470,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): | ||
|
@@ -6089,7 +6147,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', | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1795,28 +1795,56 @@ def __parameters__(self): | |
return tuple( | ||
tp for tp in self.__args__ if isinstance(tp, (typing.TypeVar, ParamSpec)) | ||
) | ||
# 3.10+ | ||
else: | ||
_ConcatenateGenericAlias = typing._ConcatenateGenericAlias | ||
|
||
# 3.8-3.9.2 | ||
class _EllipsisDummy: ... | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you make this inherit from ParamSpec, can it fix 3.10.3+? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I might be missing something here. To what case are you referring here? Is it possibly related to the following test I jointly introduced also in #479? @skipUnless(TYPING_3_11_0, # <-- Needs PR #479 to backport for 3.10
"Cannot be backported to <=3.9. See issue #48"
"Cannot use ... with typing._ConcatenateGenericAlias after 3.10.2")
def test_alias_subscription_with_ellipsis(self):
P = ParamSpec('P')
X = Callable[Concatenate[int, P], Any]
C1 = X[...] # <-- Needs PR #479 for 3.10.3+
self.assertEqual(get_args(C1), (Concatenate[int, ...], Any)) # <-- needs this PR That aside; It is possible to do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm referring to the comment right above here, about the test that works only in 3.10.0-3.10.2 but not 3.10.3+. Making There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am sorry for the confusion. I do think we talk about the same location :), if not correct me please. I made the necessary edits above. #479 allows to backport that test to all versions of 3.10, however for #479 the test There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see, thanks. #479 is merged now, so would you mind updating this PR? There's a merge conflict right now. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure. Unified the code and the test we talked about, aside from that added one more test for invalid usage. |
||
|
||
# 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.9 | ||
|
||
# 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,) | ||
if not 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.") | ||
"ParamSpec variable or ellipsis.") | ||
msg = "Concatenate[arg, ...]: each arg must be a type." | ||
parameters = tuple(typing._type_check(p, msg) for p in parameters) | ||
return _ConcatenateGenericAlias(self, parameters) | ||
|
||
parameters = (*(typing._type_check(p, msg) for p in parameters[:-1]), | ||
parameters[-1]) | ||
return _create_concatenate_alias(self, parameters) | ||
|
||
# 3.10+ | ||
if hasattr(typing, 'Concatenate'): | ||
# 3.11+; Concatenate does not accept ellipsis in 3.10 | ||
if hasattr(typing, 'Concatenate') and sys.version_info >= (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): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.