Skip to content
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

gh-105499: Merge typing.Union and types.UnionType #105511

Open
wants to merge 50 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
8c8b1dd
Unify UnionType and Union
JelleZijlstra Jun 8, 2023
aba63eb
test_typing succeeds
JelleZijlstra Jun 8, 2023
f2f23a0
Merge remote-tracking branch 'upstream/main' into unifyunion
JelleZijlstra Jun 8, 2023
e75282f
blurb
JelleZijlstra Jun 8, 2023
540b04b
Fix test_types
JelleZijlstra Jun 8, 2023
ae8fa62
Documentation
JelleZijlstra Jun 8, 2023
7569c48
stray f
JelleZijlstra Jun 8, 2023
8bcb930
Fix tests
JelleZijlstra Jun 8, 2023
291953e
No more typing._make_union
JelleZijlstra Jun 8, 2023
f7ca8d4
Add tp_new
JelleZijlstra Jun 9, 2023
0f1ab18
fix a refleak
JelleZijlstra Jun 9, 2023
eb47a0b
Fix test
JelleZijlstra Jun 9, 2023
eaa4e79
Update Lib/test/test_typing.py
JelleZijlstra Jun 9, 2023
4a0235f
Merge remote-tracking branch 'upstream/main' into unifyunion
JelleZijlstra Jun 13, 2023
c71e8c3
Add back _UnionGenericAlias
JelleZijlstra Jun 13, 2023
0475415
Merge remote-tracking branch 'upstream/main' into unifyunion
JelleZijlstra Jun 16, 2023
bb50899
Remove unnecessary NewRef
JelleZijlstra Jun 16, 2023
c564672
Make typing.Union the canonical name
JelleZijlstra Jun 16, 2023
9f58421
docs
JelleZijlstra Jun 16, 2023
8884a94
fix test_pydoc
JelleZijlstra Jun 16, 2023
bcc2e6a
Update Objects/unionobject.c
JelleZijlstra Jun 16, 2023
c4b217b
Update Doc/library/typing.rst
JelleZijlstra Jun 17, 2023
5eb2a0c
Update Doc/library/stdtypes.rst
JelleZijlstra Jun 17, 2023
64ac293
Update Doc/library/stdtypes.rst
JelleZijlstra Jun 17, 2023
0ba8551
Merge remote-tracking branch 'upstream/main' into unifyunion
JelleZijlstra Jun 17, 2023
aacf2b0
Add __mro_entries__
JelleZijlstra Jun 17, 2023
f46c0c6
Improve docs, expose it in _typing
JelleZijlstra Jun 17, 2023
8c04441
Update Doc/library/functools.rst
JelleZijlstra Jun 20, 2023
04df4d0
Merge branch 'main' into unifyunion
JelleZijlstra Jun 20, 2023
c363de7
Merge remote-tracking branch 'upstream/main' into unifyunion
JelleZijlstra Oct 11, 2023
98b634a
Post-merge cleanup
JelleZijlstra Oct 11, 2023
f2f961b
No need for tp_new
JelleZijlstra Oct 12, 2023
932f8e5
Merge branch 'main' into unifyunion
JelleZijlstra Oct 28, 2023
c3edd87
Merge remote-tracking branch 'upstream/main' into unifyunion
JelleZijlstra Feb 18, 2024
9ae102d
Add back doctest
JelleZijlstra Feb 18, 2024
e0924c6
Merge branch 'main' into unifyunion
JelleZijlstra Feb 19, 2024
7086879
Merge branch 'main' into unifyunion
JelleZijlstra Feb 28, 2024
b0e057f
Merge branch 'main' into unifyunion
JelleZijlstra Mar 12, 2024
55d0c97
Merge branch 'main' into unifyunion
JelleZijlstra Apr 26, 2024
8a80fe0
Fix two issues
JelleZijlstra Apr 26, 2024
f910e88
in progress: hashable unions
JelleZijlstra Apr 26, 2024
cae36c7
Merge remote-tracking branch 'upstream/main' into unifyunion
JelleZijlstra Sep 28, 2024
65da3f1
fixup
JelleZijlstra Sep 28, 2024
f500f5f
Make union support unhashable objects
JelleZijlstra Sep 28, 2024
5f9e599
simplify, extend docs
JelleZijlstra Sep 28, 2024
9927b38
fix more tests
JelleZijlstra Sep 28, 2024
eea6eca
another
JelleZijlstra Sep 28, 2024
0785951
change hash
JelleZijlstra Sep 29, 2024
1a39ded
Merge remote-tracking branch 'upstream/main' into unifyunion
JelleZijlstra Sep 29, 2024
eddbfde
Merge branch 'main' into unifyunion
JelleZijlstra Nov 5, 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
5 changes: 5 additions & 0 deletions Doc/deprecations/pending-removal-in-future.rst
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ although there is currently no date scheduled for their removal.

* :class:`typing.Text` (:gh:`92332`).

* The internal class ``typing._UnionGenericAlias`` is no longer used to implement
:class:`typing.Union`. To preserve compatibility with users using this private
class, a compatibility shim will be provided until at least Python 3.17. (Contributed by
Jelle Zijlstra in :gh:`105499`.)

* :class:`unittest.IsolatedAsyncioTestCase`: it is deprecated to return a value
that is not ``None`` from a test case.

Expand Down
6 changes: 3 additions & 3 deletions Doc/library/functools.rst
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,7 @@
... for i, elem in enumerate(arg):
... print(i, elem)

:data:`types.UnionType` and :data:`typing.Union` can also be used::
:class:`typing.Union` can also be used::

>>> @fun.register
... def _(arg: int | float, verbose=False):
Expand Down Expand Up @@ -651,8 +651,8 @@
The :func:`register` attribute now supports using type annotations.

.. versionchanged:: 3.11
The :func:`register` attribute now supports :data:`types.UnionType`
and :data:`typing.Union` as type annotations.
The :func:`register` attribute now supports

Check warning on line 654 in Doc/library/functools.rst

View workflow job for this annotation

GitHub Actions / Docs / Docs

py:func reference target not found: register [ref.func]
:class:`typing.Union` as a type annotation.


.. class:: singledispatchmethod(func)
Expand Down
16 changes: 8 additions & 8 deletions Doc/library/stdtypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5211,7 +5211,7 @@ Union Type
A union object holds the value of the ``|`` (bitwise or) operation on
multiple :ref:`type objects <bltin-type-objects>`. These types are intended
primarily for :term:`type annotations <annotation>`. The union type expression
enables cleaner type hinting syntax compared to :data:`typing.Union`.
enables cleaner type hinting syntax compared to subscripting :class:`typing.Union`.

.. describe:: X | Y | ...

Expand Down Expand Up @@ -5247,9 +5247,10 @@ enables cleaner type hinting syntax compared to :data:`typing.Union`.

int | str == str | int

* It is compatible with :data:`typing.Union`::
* It creates instances of :class:`typing.Union`::

int | str == typing.Union[int, str]
type(int | str) is typing.Union

* Optional types can be spelled as a union with ``None``::

Expand All @@ -5275,16 +5276,15 @@ enables cleaner type hinting syntax compared to :data:`typing.Union`.
TypeError: isinstance() argument 2 cannot be a parameterized generic

The user-exposed type for the union object can be accessed from
:data:`types.UnionType` and used for :func:`isinstance` checks. An object cannot be
instantiated from the type::
:class:`typing.Union` and used for :func:`isinstance` checks::

>>> import types
>>> isinstance(int | str, types.UnionType)
>>> import typing
>>> isinstance(int | str, typing.Union)
True
>>> types.UnionType()
>>> typing.Union()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: cannot create 'types.UnionType' instances
TypeError: cannot create 'typing.Union' instances

.. note::
The :meth:`!__or__` method for type objects was added to support the syntax
Expand Down
4 changes: 4 additions & 0 deletions Doc/library/types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,10 @@ Standard names are defined for the following types:

.. versionadded:: 3.10

.. versionchanged:: 3.14

This is now an alias for :class:`typing.Union`.

.. class:: TracebackType(tb_next, tb_frame, tb_lasti, tb_lineno)

The type of traceback objects such as found in ``sys.exception().__traceback__``.
Expand Down
10 changes: 9 additions & 1 deletion Doc/library/typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1086,7 +1086,7 @@ Special forms
These can be used as types in annotations. They all support subscription using
``[]``, but each has a unique syntax.

.. data:: Union
.. class:: Union

Union type; ``Union[X, Y]`` is equivalent to ``X | Y`` and means either X or Y.

Expand Down Expand Up @@ -1121,6 +1121,14 @@ These can be used as types in annotations. They all support subscription using
Unions can now be written as ``X | Y``. See
:ref:`union type expressions<types-union>`.

.. versionchanged:: 3.14
:class:`types.UnionType` is now an alias for :class:`Union`, and both
``Union[int, str]`` and ``int | str`` create instances of the same class.
To check whether an object is a ``Union`` at runtime, use
``isinstance(obj, Union)``. For compatibility with earlier versions of
Python, use
``get_origin(obj) is typing.Union or get_origin(obj) is types.UnionType``.

.. data:: Optional

``Optional[X]`` is equivalent to ``X | None`` (or ``Union[X, None]``).
Expand Down
4 changes: 2 additions & 2 deletions Doc/whatsnew/3.10.rst
Original file line number Diff line number Diff line change
Expand Up @@ -722,10 +722,10 @@ PEP 604: New Type Union Operator

A new type union operator was introduced which enables the syntax ``X | Y``.
This provides a cleaner way of expressing 'either type X or type Y' instead of
using :data:`typing.Union`, especially in type hints.
using :class:`typing.Union`, especially in type hints.

In previous versions of Python, to apply a type hint for functions accepting
arguments of multiple types, :data:`typing.Union` was used::
arguments of multiple types, :class:`typing.Union` was used::

def square(number: Union[int, float]) -> Union[int, float]:
return number ** 2
Expand Down
4 changes: 2 additions & 2 deletions Doc/whatsnew/3.11.rst
Original file line number Diff line number Diff line change
Expand Up @@ -740,8 +740,8 @@ fractions
functools
---------

* :func:`functools.singledispatch` now supports :data:`types.UnionType`
and :data:`typing.Union` as annotations to the dispatch argument.::
* :func:`functools.singledispatch` now supports :class:`types.UnionType`
and :class:`typing.Union` as annotations to the dispatch argument.::

>>> from functools import singledispatch
>>> @singledispatch
Expand Down
1 change: 0 additions & 1 deletion Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1488,7 +1488,6 @@ Optimizations
Removed Modules And APIs
========================


.. _whatsnew313-pep594:

PEP 594: Remove "dead batteries" from the standard library
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_unionobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ PyAPI_FUNC(PyObject *) _Py_union_type_or(PyObject *, PyObject *);
extern PyObject *_Py_subs_parameters(PyObject *, PyObject *, PyObject *, PyObject *);
extern PyObject *_Py_make_parameters(PyObject *);
extern PyObject *_Py_union_args(PyObject *self);
extern PyObject *_Py_union_from_tuple(PyObject *args);

#ifdef __cplusplus
}
Expand Down
17 changes: 5 additions & 12 deletions Lib/functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -928,16 +928,11 @@ def dispatch(cls):
dispatch_cache[cls] = impl
return impl

def _is_union_type(cls):
from typing import get_origin, Union
return get_origin(cls) in {Union, UnionType}

def _is_valid_dispatch_type(cls):
if isinstance(cls, type):
return True
from typing import get_args
return (_is_union_type(cls) and
all(isinstance(arg, type) for arg in get_args(cls)))
return (isinstance(cls, UnionType) and
all(isinstance(arg, type) for arg in cls.__args__))

def register(cls, func=None):
"""generic_func.register(cls, func) -> func
Expand Down Expand Up @@ -969,7 +964,7 @@ def register(cls, func=None):
from annotationlib import Format, ForwardRef
argname, cls = next(iter(get_type_hints(func, format=Format.FORWARDREF).items()))
if not _is_valid_dispatch_type(cls):
if _is_union_type(cls):
if isinstance(cls, UnionType):
raise TypeError(
f"Invalid annotation for {argname!r}. "
f"{cls!r} not all arguments are classes."
Expand All @@ -985,10 +980,8 @@ def register(cls, func=None):
f"{cls!r} is not a class."
)

if _is_union_type(cls):
from typing import get_args

for arg in get_args(cls):
if isinstance(cls, UnionType):
for arg in cls.__args__:
registry[arg] = func
else:
registry[cls] = func
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_dataclasses/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2313,7 +2313,7 @@ def test_docstring_one_field_with_default_none(self):
class C:
x: Union[int, type(None)] = None

self.assertDocStrEqual(C.__doc__, "C(x:Optional[int]=None)")
self.assertDocStrEqual(C.__doc__, "C(x:int|None=None)")

def test_docstring_list_field(self):
@dataclass
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -3033,7 +3033,7 @@ def _(arg: typing.Union[int, typing.Iterable[str]]):
"Invalid annotation for 'arg'."
))
self.assertTrue(str(exc.exception).endswith(
'typing.Union[int, typing.Iterable[str]] not all arguments are classes.'
'int | typing.Iterable[str] not all arguments are classes.'
))

def test_invalid_positional_argument(self):
Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -1736,8 +1736,8 @@ class C(metaclass=M):
class TestFormatAnnotation(unittest.TestCase):
def test_typing_replacement(self):
from test.typinganndata.ann_module9 import ann, ann1
self.assertEqual(inspect.formatannotation(ann), 'Union[List[str], int]')
self.assertEqual(inspect.formatannotation(ann1), 'Union[List[testModule.typing.A], int]')
self.assertEqual(inspect.formatannotation(ann), 'List[str] | int')
self.assertEqual(inspect.formatannotation(ann1), 'List[testModule.typing.A] | int')


class TestIsMethodDescriptor(unittest.TestCase):
Expand Down
16 changes: 8 additions & 8 deletions Lib/test/test_pydoc/test_pydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ class C(builtins.object)
c_alias = test.test_pydoc.pydoc_mod.C[int]
list_alias1 = typing.List[int]
list_alias2 = list[int]
type_union1 = typing.Union[int, str]
type_union1 = int | str
type_union2 = int | str

VERSION
Expand Down Expand Up @@ -222,7 +222,7 @@ class C(builtins.object)
c_alias = test.test_pydoc.pydoc_mod.C[int]
list_alias1 = typing.List[int]
list_alias2 = list[int]
type_union1 = typing.Union[int, str]
type_union1 = int | str
type_union2 = int | str

Author
Expand Down Expand Up @@ -1363,17 +1363,17 @@ def test_generic_alias(self):
self.assertIn(list.__doc__.strip().splitlines()[0], doc)

def test_union_type(self):
self.assertEqual(pydoc.describe(typing.Union[int, str]), '_UnionGenericAlias')
self.assertEqual(pydoc.describe(typing.Union[int, str]), 'Union')
doc = pydoc.render_doc(typing.Union[int, str], renderer=pydoc.plaintext)
self.assertIn('_UnionGenericAlias in module typing', doc)
self.assertIn('Union = typing.Union', doc)
self.assertIn('Union in module typing', doc)
self.assertIn('class Union(builtins.object)', doc)
if typing.Union.__doc__:
self.assertIn(typing.Union.__doc__.strip().splitlines()[0], doc)

self.assertEqual(pydoc.describe(int | str), 'UnionType')
self.assertEqual(pydoc.describe(int | str), 'Union')
doc = pydoc.render_doc(int | str, renderer=pydoc.plaintext)
self.assertIn('UnionType in module types object', doc)
self.assertIn('\nclass UnionType(builtins.object)', doc)
self.assertIn('Union in module typing', doc)
self.assertIn('class Union(builtins.object)', doc)
if not MISSING_C_DOCSTRINGS:
self.assertIn(types.UnionType.__doc__.strip().splitlines()[0], doc)

Expand Down
50 changes: 41 additions & 9 deletions Lib/test/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,10 +709,6 @@ def test_or_types_operator(self):
y = int | bool
with self.assertRaises(TypeError):
x < y
# Check that we don't crash if typing.Union does not have a tuple in __args__
y = typing.Union[str, int]
y.__args__ = [str, int]
Copy link
Member Author

Choose a reason for hiding this comment

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

.__args__ is no longer writable.

self.assertEqual(x, y)

def test_hash(self):
self.assertEqual(hash(int | str), hash(str | int))
Expand All @@ -727,17 +723,40 @@ class B(metaclass=UnhashableMeta): ...

self.assertEqual((A | B).__args__, (A, B))
union1 = A | B
with self.assertRaises(TypeError):
with self.assertRaisesRegex(TypeError, "unhashable type: 'UnhashableMeta'"):
hash(union1)

union2 = int | B
with self.assertRaises(TypeError):
with self.assertRaisesRegex(TypeError, "unhashable type: 'UnhashableMeta'"):
hash(union2)

union3 = A | int
with self.assertRaises(TypeError):
with self.assertRaisesRegex(TypeError, "unhashable type: 'UnhashableMeta'"):
hash(union3)

def test_unhashable_becomes_hashable(self):
is_hashable = False
class UnhashableMeta(type):
def __hash__(self):
if is_hashable:
return 1
else:
raise TypeError("not hashable")

class A(metaclass=UnhashableMeta): ...
class B(metaclass=UnhashableMeta): ...

union = A | B
self.assertEqual(union.__args__, (A, B))

with self.assertRaisesRegex(TypeError, "not hashable"):
hash(union)

is_hashable = True

with self.assertRaisesRegex(TypeError, "union contains 2 unhashable elements"):
hash(union)

def test_instancecheck_and_subclasscheck(self):
for x in (int | str, typing.Union[int, str]):
with self.subTest(x=x):
Expand Down Expand Up @@ -921,7 +940,7 @@ def forward_before(x: ForwardBefore[int]) -> None: ...
self.assertEqual(typing.get_args(typing.get_type_hints(forward_after)['x']),
(int, Forward))
self.assertEqual(typing.get_args(typing.get_type_hints(forward_before)['x']),
(int, Forward))
(Forward, int))

def test_or_type_operator_with_Protocol(self):
class Proto(typing.Protocol):
Expand Down Expand Up @@ -1015,9 +1034,14 @@ def __eq__(self, other):
return 1 / 0

bt = BadType('bt', (), {})
bt2 = BadType('bt2', (), {})
# Comparison should fail and errors should propagate out for bad types.
Copy link
Member Author

Choose a reason for hiding this comment

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

With the new code there are fewer code paths that trigger the equality comparison.

union1 = int | bt
union2 = int | bt2
with self.assertRaises(ZeroDivisionError):
union1 == union2
with self.assertRaises(ZeroDivisionError):
list[int] | list[bt]
bt | bt2

union_ga = (list[str] | int, collections.abc.Callable[..., str] | int,
d | int)
Expand Down Expand Up @@ -1060,6 +1084,14 @@ def test_or_type_operator_reference_cycle(self):
self.assertLessEqual(sys.gettotalrefcount() - before, leeway,
msg='Check for union reference leak.')

def test_instantiation(self):
with self.assertRaises(TypeError):
types.UnionType()
self.assertIs(int, types.UnionType[int])
self.assertIs(int, types.UnionType[int, int])
self.assertEqual(int | str, types.UnionType[int, str])
self.assertEqual(int | typing.ForwardRef("str"), types.UnionType[int, "str"])


class MappingProxyTests(unittest.TestCase):
mappingproxy = types.MappingProxyType
Expand Down
Loading
Loading