Skip to content

Backport changes to the repr of typing.Unpack that were made in Python 3.12 #163

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 3 commits into from
May 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]), f'{Unpack.__module__}.Unpack[Ts]')

def test_cannot_subclass_vars(self):
with self.assertRaises(TypeError):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
71 changes: 48 additions & 23 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1960,14 +1960,60 @@ 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 and PEP 692.
"""


if sys.version_info >= (3, 12): # PEP 692 changed the repr of Unpack[]
Unpack = typing.Unpack

def _is_unpack(obj):
return get_origin(obj) is Unpack

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

Expand All @@ -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,))

Expand All @@ -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)
Expand Down