Skip to content

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

Merged
merged 23 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7e0aac8
Backport Ellipsis in Concatenate for 3.9-3.10
Daraan Aug 9, 2024
73d3952
Extended ellipsis test, removed version_info slices
Daraan Aug 12, 2024
2e5fc74
Merge branch 'main' into main
Daraan Sep 9, 2024
c716ff1
Merge 'upstream/main' into concatenate/ellipsis
Daraan Sep 25, 2024
70d5205
Properly skipped tests
Daraan Sep 25, 2024
8e4f0be
renamed function better reasoning
Daraan Sep 25, 2024
0991b42
Properly skipped tests
Daraan Sep 25, 2024
de6a2c6
Merge remote-tracking branch 'upstream/main' into concatenate/ellipsis
Daraan Sep 26, 2024
3fb63a3
Merge remote-tracking branch 'upstream/main' into concatenate/ellipsis
Daraan Sep 26, 2024
3112a80
Added info to changelog and docs
Daraan Oct 2, 2024
1eabcb6
Full backport to 3.8 to support Concatenate[...]
Daraan Oct 2, 2024
3e6ec0a
Updated changelog and docs
Daraan Oct 2, 2024
5b9e4cb
Merge branch 'main' into main
Daraan Oct 2, 2024
a77d93f
minor comment and name update
Daraan Oct 2, 2024
8cba5ab
Changed invalid use example to current behavior
Daraan Oct 5, 2024
65270c6
Changed ref to new PR
Daraan Oct 5, 2024
97bdfd1
Merge branch 'main' into concatenate/ellipsis-support-110
Daraan Oct 11, 2024
ff79863
Merge remote-tracking branch 'upstream/main' into concatenate/ellipsi…
Daraan Oct 21, 2024
9fda1a1
Unified typing.Callable and collections.abc.Callable tests
Daraan Oct 21, 2024
c092d7e
fix whitespace
Daraan Oct 21, 2024
4e8ef02
Merge remote-tracking branch 'upstream/main' into concatenate/ellipsi…
Daraan Oct 22, 2024
c5229f8
Minor changes
Daraan Oct 22, 2024
c06a9c7
unified invalid tests and additional test case
Daraan Oct 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
subscripted objects) had wrong parameters if they were directly
subscripted with an `Unpack` object.
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).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Extended the Concatenate backport for Python 3.8-3.10 to now accept
ellipsis as an 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).


# Release 4.12.2 (June 7, 2024)

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
86 changes: 72 additions & 14 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))


class CollectionsAbcTests(BaseTestCase):
Expand Down Expand Up @@ -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]]
Expand Down Expand Up @@ -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')
Expand All @@ -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"):
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')
Expand All @@ -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),
Copy link
Member

Choose a reason for hiding this comment

The 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+.

Copy link
Member

Choose a reason for hiding this comment

The 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')
Expand All @@ -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):
Expand Down Expand Up @@ -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',
Expand Down
48 changes: 38 additions & 10 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...
Copy link
Member

Choose a reason for hiding this comment

The 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+?

Copy link
Contributor Author

@Daraan Daraan Oct 21, 2024

Choose a reason for hiding this comment

The 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?
This backport of Concatenate[...] also works for all 3.10, as the critical part is in _concatenate_getitem

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 _EllipsisDummy = ParamSpec("_EllipsisDummy") if needed as subclassing ParamSpec is (currently) not possible. I thought using a new class might have less side-effects than using a ParamSpec instance.

Copy link
Member

Choose a reason for hiding this comment

The 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 _EllipsisDummy an instance of ParamSpec might fix that test for later versions of 3.10.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 self.assertEqual(get_args(C1), (Concatenate[int, ...], Any)) would need this PR to work without an error on 3.10 as well.
Both PRs are needed make this test work for the 3.10 version range.

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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):
Expand Down
Loading