From a4022c2fbf80ee44b261cb6e7e7434fcff47834b Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 19 May 2023 22:57:06 +0100 Subject: [PATCH 1/3] Backport changes to the repr of `typing.Unpack` that were made in Python 3.12 --- CHANGELOG.md | 3 ++ README.md | 2 + src/test_typing_extensions.py | 12 +++--- src/typing_extensions.py | 71 +++++++++++++++++++++++------------ 4 files changed, 59 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc5abe74..4e24f90b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,9 @@ - Backport the implementation of `NewType` from 3.10 (where it is implemented as a class rather than a function). This allows user-defined `NewType`s to be pickled. Patch by Alex Waygood. +- Backport changes to the repr of `typing.Unpack` that were made in order to + implement [PEP 692](https://peps.python.org/pep-0692/) (backport of + https://github.com/python/cpython/pull/104048). Patch by Alex Waygood. # Release 4.5.0 (February 14, 2023) diff --git a/README.md b/README.md index 11434d18..0b888e90 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,8 @@ Certain objects were changed after they were added to `typing`, and - `NewType` has been in the `typing` module since Python 3.5.2, but user-defined `NewType`s are only pickleable on Python 3.10+. `typing_extensions.NewType` backports this feature to all Python versions. +- `Unpack` was added in Python 3.11, but the repr was changed in Python 3.12; + `typing_extensions.Unpack` has the newer repr on all versions. There are a few types whose interface was modified between different versions of typing. For example, `typing.Sequence` was modified to diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 469c31b6..a1dd0723 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -3531,10 +3531,7 @@ def test_basic_plain(self): def test_repr(self): Ts = TypeVarTuple('Ts') - if TYPING_3_11_0: - self.assertEqual(repr(Unpack[Ts]), '*Ts') - else: - self.assertEqual(repr(Unpack[Ts]), 'typing_extensions.Unpack[Ts]') + self.assertEqual(repr(Unpack[Ts]), 'typing_extensions.Unpack[Ts]') def test_cannot_subclass_vars(self): with self.assertRaises(TypeError): @@ -3656,7 +3653,10 @@ def test_args_and_parameters(self): Ts = TypeVarTuple('Ts') t = Tuple[tuple(Ts)] - self.assertEqual(t.__args__, (Unpack[Ts],)) + if sys.version_info >= (3, 11): + self.assertEqual(t.__args__, (typing.Unpack[Ts],)) + else: + self.assertEqual(t.__args__, (Unpack[Ts],)) self.assertEqual(t.__parameters__, (Ts,)) def test_pickle(self): @@ -3922,7 +3922,7 @@ def test_typing_extensions_defers_when_possible(self): exclude |= { 'Protocol', 'runtime_checkable', 'SupportsAbs', 'SupportsBytes', 'SupportsComplex', 'SupportsFloat', 'SupportsIndex', 'SupportsInt', - 'SupportsRound', 'TypedDict', 'is_typeddict', 'NamedTuple', + 'SupportsRound', 'TypedDict', 'is_typeddict', 'NamedTuple', 'Unpack', } for item in typing_extensions.__all__: if item not in exclude and hasattr(typing, item): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index dd12cfb8..042048a0 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1960,7 +1960,49 @@ class Movie(TypedDict): """) -if hasattr(typing, "Unpack"): # 3.11+ +_UNPACK_DOC = """\ +Type unpack operator. + +The type unpack operator takes the child types from some container type, +such as `tuple[int, str]` or a `TypeVarTuple`, and 'pulls them out'. For +example: + + # For some generic class `Foo`: + Foo[Unpack[tuple[int, str]]] # Equivalent to Foo[int, str] + + Ts = TypeVarTuple('Ts') + # Specifies that `Bar` is generic in an arbitrary number of types. + # (Think of `Ts` as a tuple of an arbitrary number of individual + # `TypeVar`s, which the `Unpack` is 'pulling out' directly into the + # `Generic[]`.) + class Bar(Generic[Unpack[Ts]]): ... + Bar[int] # Valid + Bar[int, str] # Also valid + +From Python 3.11, this can also be done using the `*` operator: + + Foo[*tuple[int, str]] + class Bar(Generic[*Ts]): ... + +The operator can also be used along with a `TypedDict` to annotate +`**kwargs` in a function signature. For instance: + + class Movie(TypedDict): + name: str + year: int + + # This function expects two keyword arguments - *name* of type `str` and + # *year* of type `int`. + def foo(**kwargs: Unpack[Movie]): ... + +Note that there is only some runtime checking of this operator. Not +everything the runtime allows may be accepted by static type checkers. + +For more information, see PEP 646. +""" + + +if sys.version_info >= (3, 12): # PEP 692 changed the repr of Unpack[] Unpack = typing.Unpack def _is_unpack(obj): @@ -1968,6 +2010,10 @@ def _is_unpack(obj): elif sys.version_info[:2] >= (3, 9): class _UnpackSpecialForm(typing._SpecialForm, _root=True): + def __init__(self, getitem): + super().__init__(getitem) + self.__doc__ = _UNPACK_DOC + def __repr__(self): return 'typing_extensions.' + self._name @@ -1976,16 +2022,6 @@ class _UnpackAlias(typing._GenericAlias, _root=True): @_UnpackSpecialForm def Unpack(self, parameters): - """A special typing construct to unpack a variadic type. For example: - - Shape = TypeVarTuple('Shape') - Batch = NewType('Batch', int) - - def add_batch_axis( - x: Array[Unpack[Shape]] - ) -> Array[Batch, Unpack[Shape]]: ... - - """ item = typing._type_check(parameters, f'{self._name} accepts only a single type.') return _UnpackAlias(self, (item,)) @@ -2005,18 +2041,7 @@ def __getitem__(self, parameters): f'{self._name} accepts only a single type.') return _UnpackAlias(self, (item,)) - Unpack = _UnpackForm( - 'Unpack', - doc="""A special typing construct to unpack a variadic type. For example: - - Shape = TypeVarTuple('Shape') - Batch = NewType('Batch', int) - - def add_batch_axis( - x: Array[Unpack[Shape]] - ) -> Array[Batch, Unpack[Shape]]: ... - - """) + Unpack = _UnpackForm('Unpack', doc=_UNPACK_DOC) def _is_unpack(obj): return isinstance(obj, _UnpackAlias) From 6e37b972972fdb77c4fecb876cab85faccdb459f Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 19 May 2023 23:03:41 +0100 Subject: [PATCH 2/3] Update src/test_typing_extensions.py --- src/test_typing_extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index a1dd0723..a2a56a9d 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -3531,7 +3531,7 @@ def test_basic_plain(self): def test_repr(self): Ts = TypeVarTuple('Ts') - self.assertEqual(repr(Unpack[Ts]), 'typing_extensions.Unpack[Ts]') + self.assertEqual(repr(Unpack[Ts]), f'{Unpack.__module__}.Unpack[Ts]') def test_cannot_subclass_vars(self): with self.assertRaises(TypeError): From 03ad9d4d9ed5c55b03b7ac105db6567df76c4c21 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 19 May 2023 23:06:56 +0100 Subject: [PATCH 3/3] Update src/typing_extensions.py Co-authored-by: Jelle Zijlstra --- src/typing_extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 042048a0..433d140d 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1998,7 +1998,7 @@ def foo(**kwargs: Unpack[Movie]): ... Note that there is only some runtime checking of this operator. Not everything the runtime allows may be accepted by static type checkers. -For more information, see PEP 646. +For more information, see PEP 646 and PEP 692. """