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 all 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 @@ -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: ...
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.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
Loading