diff --git a/Doc/deprecations/pending-removal-in-future.rst b/Doc/deprecations/pending-removal-in-future.rst index edb672ed8ad9a2..6c73deb8445dce 100644 --- a/Doc/deprecations/pending-removal-in-future.rst +++ b/Doc/deprecations/pending-removal-in-future.rst @@ -122,11 +122,6 @@ 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. diff --git a/Doc/library/functools.rst b/Doc/library/functools.rst index beec9b942afc0f..2e032c2dc92a64 100644 --- a/Doc/library/functools.rst +++ b/Doc/library/functools.rst @@ -526,7 +526,7 @@ The :mod:`functools` module defines the following functions: ... for i, elem in enumerate(arg): ... print(i, elem) - :class:`typing.Union` can also be used:: + :class:`types.UnionType` and :data:`typing.Union` can also be used:: >>> @fun.register ... def _(arg: int | float, verbose=False): @@ -662,8 +662,8 @@ The :mod:`functools` module defines the following functions: The :func:`~singledispatch.register` attribute now supports using type annotations. .. versionchanged:: 3.11 - The :func:`~singledispatch.register` attribute now supports - :class:`typing.Union` as a type annotation. + The :func:`~singledispatch.register` attribute now supports :class:`types.UnionType` + and :data:`typing.Union` as type annotations. .. class:: singledispatchmethod(func) diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index bc4036a6795a13..776a0d68a396e8 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -5649,7 +5649,7 @@ Union Type A union object holds the value of the ``|`` (bitwise or) operation on multiple :ref:`type objects `. These types are intended primarily for :term:`type annotations `. The union type expression -enables cleaner type hinting syntax compared to subscripting :class:`typing.Union`. +enables cleaner type hinting syntax compared to :data:`typing.Union`. .. describe:: X | Y | ... @@ -5685,10 +5685,9 @@ enables cleaner type hinting syntax compared to subscripting :class:`typing.Unio int | str == str | int - * It creates instances of :class:`typing.Union`:: + * It is compatible with :data:`typing.Union`:: int | str == typing.Union[int, str] - type(int | str) is typing.Union * Optional types can be spelled as a union with ``None``:: @@ -5714,15 +5713,16 @@ enables cleaner type hinting syntax compared to subscripting :class:`typing.Unio TypeError: isinstance() argument 2 cannot be a parameterized generic The user-exposed type for the union object can be accessed from -:class:`typing.Union` and used for :func:`isinstance` checks:: +:class:`types.UnionType` and used for :func:`isinstance` checks. An object cannot be +instantiated from the type:: - >>> import typing - >>> isinstance(int | str, typing.Union) + >>> import types + >>> isinstance(int | str, types.UnionType) True - >>> typing.Union() + >>> types.UnionType() Traceback (most recent call last): File "", line 1, in - TypeError: cannot create 'typing.Union' instances + TypeError: cannot create 'types.UnionType' instances .. note:: The :meth:`!__or__` method for type objects was added to support the syntax @@ -5749,11 +5749,6 @@ The user-exposed type for the union object can be accessed from .. versionadded:: 3.10 -.. versionchanged:: 3.14 - - Union objects are now instances of :class:`typing.Union`. Previously, they were instances - of :class:`types.UnionType`, which remains an alias for :class:`typing.Union`. - .. _typesother: diff --git a/Doc/library/types.rst b/Doc/library/types.rst index 207024a7619902..25d63191989b66 100644 --- a/Doc/library/types.rst +++ b/Doc/library/types.rst @@ -314,10 +314,6 @@ 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__``. diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 022c76b084c08c..14cc1abbf2769a 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -1091,7 +1091,7 @@ Special forms These can be used as types in annotations. They all support subscription using ``[]``, but each has a unique syntax. -.. class:: Union +.. data:: Union Union type; ``Union[X, Y]`` is equivalent to ``X | Y`` and means either X or Y. @@ -1132,14 +1132,6 @@ 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`. - .. 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]``). diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index 8db57f6f22fc8d..05d56c22a0d6a7 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -723,10 +723,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 :class:`typing.Union`, especially in type hints. +using :data:`typing.Union`, especially in type hints. In previous versions of Python, to apply a type hint for functions accepting -arguments of multiple types, :class:`typing.Union` was used:: +arguments of multiple types, :data:`typing.Union` was used:: def square(number: Union[int, float]) -> Union[int, float]: return number ** 2 diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index 2dd205dd2b8831..b1037dbaf23171 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -741,7 +741,7 @@ functools --------- * :func:`functools.singledispatch` now supports :class:`types.UnionType` - and :class:`typing.Union` as annotations to the dispatch argument.:: + and :data:`typing.Union` as annotations to the dispatch argument.:: >>> from functools import singledispatch >>> @singledispatch diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 5462c5dc425e2e..053ff6f2ed0c06 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -2071,56 +2071,9 @@ turtle (Contributed by Marie Roald and Yngve Mardal Moe in :gh:`126350`.) -types ------ - -* :class:`types.UnionType` is now an alias for :class:`typing.Union`. - See :ref:`below ` for more details. - (Contributed by Jelle Zijlstra in :gh:`105499`.) - - typing ------ -.. _whatsnew314-typing-union: - -* :class:`types.UnionType` and :class:`typing.Union` are now aliases for each other, - meaning that both old-style unions (created with ``Union[int, str]``) and new-style - unions (``int | str``) now create instances of the same runtime type. This unifies - the behavior between the two syntaxes, but leads to some differences in behavior that - may affect users who introspect types at runtime: - - - Both syntaxes for creating a union now produce the same string representation in - ``repr()``. For example, ``repr(Union[int, str])`` - is now ``"int | str"`` instead of ``"typing.Union[int, str]"``. - - Unions created using the old syntax are no longer cached. Previously, running - ``Union[int, str]`` multiple times would return the same object - (``Union[int, str] is Union[int, str]`` would be ``True``), but now it will - return two different objects. Users should use ``==`` to compare unions for equality, not - ``is``. New-style unions have never been cached this way. - This change could increase memory usage for some programs that use a large number of - unions created by subscripting ``typing.Union``. However, several factors offset this cost: - unions used in annotations are no longer evaluated by default in Python 3.14 - because of :pep:`649`; an instance of :class:`types.UnionType` is - itself much smaller than the object returned by ``Union[]`` was on prior Python - versions; and removing the cache also saves some space. It is therefore - unlikely that this change will cause a significant increase in memory usage for most - users. - - Previously, old-style unions were implemented using the private class - ``typing._UnionGenericAlias``. This class is no longer needed for the implementation, - but it has been retained for backward compatibility, with removal scheduled for Python - 3.17. Users should use documented introspection helpers like :func:`typing.get_origin` - and :func:`typing.get_args` instead of relying on private implementation details. - - It is now possible to use :class:`typing.Union` itself in :func:`isinstance` checks. - For example, ``isinstance(int | str, typing.Union)`` will return ``True``; previously - this raised :exc:`TypeError`. - - The ``__args__`` attribute of :class:`typing.Union` objects is no longer writable. - - It is no longer possible to set any attributes on :class:`typing.Union` objects. - This only ever worked for dunder attributes on previous versions, was never - documented to work, and was subtly broken in many cases. - - (Contributed by Jelle Zijlstra in :gh:`105499`.) - * :class:`typing.TypeAliasType` now supports star unpacking. @@ -3189,11 +3142,6 @@ Changes in the Python API This temporary change affects other threads. (Contributed by Serhiy Storchaka in :gh:`69998`.) -* :class:`types.UnionType` is now an alias for :class:`typing.Union`, - causing changes in some behaviors. - See :ref:`above ` for more details. - (Contributed by Jelle Zijlstra in :gh:`105499`.) - * The runtime behavior of annotations has changed in various ways; see :ref:`above ` for details. While most code that interacts with annotations should continue to work, some undocumented details may behave diff --git a/Include/internal/pycore_unionobject.h b/Include/internal/pycore_unionobject.h index 4bd36f6504d42c..6ece7134cdeca0 100644 --- a/Include/internal/pycore_unionobject.h +++ b/Include/internal/pycore_unionobject.h @@ -18,7 +18,6 @@ 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 } diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index bee019cd51591e..ff31c7376ee1bf 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -284,10 +284,12 @@ def __hash__(self): )) def __or__(self, other): - return types.UnionType[self, other] + import typing + return typing.Union[self, other] def __ror__(self, other): - return types.UnionType[other, self] + import typing + return typing.Union[other, self] def __repr__(self): extra = [] diff --git a/Lib/functools.py b/Lib/functools.py index a92844ba7227b0..86920eee95e034 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -929,11 +929,16 @@ 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 - return (isinstance(cls, UnionType) and - all(isinstance(arg, type) for arg in cls.__args__)) + from typing import get_args + return (_is_union_type(cls) and + all(isinstance(arg, type) for arg in get_args(cls))) def register(cls, func=None): """generic_func.register(cls, func) -> func @@ -965,7 +970,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 isinstance(cls, UnionType): + if _is_union_type(cls): raise TypeError( f"Invalid annotation for {argname!r}. " f"{cls!r} not all arguments are classes." @@ -981,8 +986,10 @@ def register(cls, func=None): f"{cls!r} is not a class." ) - if isinstance(cls, UnionType): - for arg in cls.__args__: + if _is_union_type(cls): + from typing import get_args + + for arg in get_args(cls): registry[arg] = func else: registry[cls] = func diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index 88e0d611647f28..0ac27f0e01fdd9 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -137,7 +137,8 @@ class UnionForwardrefs: str | int, ) union = annos["union"] - self.assertIsInstance(union, Union) + self.assertIs(typing.get_origin(union), Union) + # self.assertIsInstance(union, Union) arg1, arg2 = typing.get_args(union) self.assertIs(arg1, str) self.assertEqual( diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index 6bf5e5b3e5554b..a76c270ef9f99d 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -2317,7 +2317,7 @@ def test_docstring_one_field_with_default_none(self): class C: x: Union[int, type(None)] = None - self.assertDocStrEqual(C.__doc__, "C(x:int|None=None)") + self.assertDocStrEqual(C.__doc__, "C(x:Optional[int]=None)") def test_docstring_list_field(self): @dataclass diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index f7e09fd771eaf2..4ad5db03e735c8 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -3203,7 +3203,7 @@ def _(arg: typing.Union[int, typing.Iterable[str]]): "Invalid annotation for 'arg'." ) self.assertEndsWith(str(exc.exception), - 'int | typing.Iterable[str] not all arguments are classes.' + 'typing.Union[int, typing.Iterable[str]] not all arguments are classes.' ) def test_invalid_positional_argument(self): diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 555efb78dcc6aa..68bf583c26418c 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -1779,14 +1779,14 @@ class C(metaclass=M): class TestFormatAnnotation(unittest.TestCase): def test_typing_replacement(self): from test.typinganndata.ann_module9 import A, ann, ann1 - self.assertEqual(inspect.formatannotation(ann), 'List[str] | int') - self.assertEqual(inspect.formatannotation(ann1), 'List[testModule.typing.A] | int') + self.assertEqual(inspect.formatannotation(ann), 'Union[List[str], int]') + self.assertEqual(inspect.formatannotation(ann1), 'Union[List[testModule.typing.A], int]') self.assertEqual(inspect.formatannotation(A, 'testModule.typing'), 'A') self.assertEqual(inspect.formatannotation(A, 'other'), 'testModule.typing.A') self.assertEqual( inspect.formatannotation(ann1, 'testModule.typing'), - 'List[testModule.typing.A] | int', + 'Union[List[testModule.typing.A], int]', ) def test_forwardref(self): @@ -1824,7 +1824,7 @@ class B: ... # Not an instance of "type": self.assertEqual( inspect.formatannotationrelativeto(A)(ann1), - 'List[testModule.typing.A] | int', + 'Union[List[testModule.typing.A], int]', ) diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py index 3b50ead00bdd31..9fb6c595a23c3a 100644 --- a/Lib/test/test_pydoc/test_pydoc.py +++ b/Lib/test/test_pydoc/test_pydoc.py @@ -128,7 +128,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 = int | str + type_union1 = typing.Union[int, str] type_union2 = int | str VERSION @@ -215,7 +215,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 = int | str + type_union1 = typing.Union[int, str] type_union2 = int | str Author @@ -1444,17 +1444,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]), 'Union') + self.assertEqual(pydoc.describe(typing.Union[int, str]), '_UnionGenericAlias') doc = pydoc.render_doc(typing.Union[int, str], renderer=pydoc.plaintext) - self.assertIn('Union in module typing', doc) - self.assertIn('class Union(builtins.object)', doc) + self.assertIn('_UnionGenericAlias in module typing', doc) + self.assertIn('Union = typing.Union', doc) if typing.Union.__doc__: self.assertIn(typing.Union.__doc__.strip().splitlines()[0], doc) - self.assertEqual(pydoc.describe(int | str), 'Union') + self.assertEqual(pydoc.describe(int | str), 'UnionType') doc = pydoc.render_doc(int | str, renderer=pydoc.plaintext) - self.assertIn('Union in module typing', doc) - self.assertIn('class Union(builtins.object)', doc) + self.assertIn('UnionType in module types object', doc) + self.assertIn('\nclass UnionType(builtins.object)', doc) if not MISSING_C_DOCSTRINGS: self.assertIn(types.UnionType.__doc__.strip().splitlines()[0], doc) diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index 9b0ae709d7968d..ae787a93f18d09 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -2,7 +2,7 @@ from test.support import ( run_with_locale, cpython_only, no_rerun, - MISSING_C_DOCSTRINGS, EqualToForwardRef, check_disallow_instantiation, + MISSING_C_DOCSTRINGS, ) from test.support.script_helper import assert_python_ok from test.support.import_helper import import_fresh_module @@ -784,6 +784,10 @@ 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] + self.assertEqual(x, y) def test_hash(self): self.assertEqual(hash(int | str), hash(str | int)) @@ -798,40 +802,17 @@ class B(metaclass=UnhashableMeta): ... self.assertEqual((A | B).__args__, (A, B)) union1 = A | B - with self.assertRaisesRegex(TypeError, "unhashable type: 'UnhashableMeta'"): + with self.assertRaises(TypeError): hash(union1) union2 = int | B - with self.assertRaisesRegex(TypeError, "unhashable type: 'UnhashableMeta'"): + with self.assertRaises(TypeError): hash(union2) union3 = A | int - with self.assertRaisesRegex(TypeError, "unhashable type: 'UnhashableMeta'"): + with self.assertRaises(TypeError): 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): @@ -1015,7 +996,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']), - (Forward, int)) + (int, Forward)) def test_or_type_operator_with_Protocol(self): class Proto(typing.Protocol): @@ -1109,14 +1090,9 @@ def __eq__(self, other): return 1 / 0 bt = BadType('bt', (), {}) - bt2 = BadType('bt2', (), {}) # Comparison should fail and errors should propagate out for bad types. - union1 = int | bt - union2 = int | bt2 - with self.assertRaises(ZeroDivisionError): - union1 == union2 with self.assertRaises(ZeroDivisionError): - bt | bt2 + list[int] | list[bt] union_ga = (list[str] | int, collections.abc.Callable[..., str] | int, d | int) @@ -1159,19 +1135,6 @@ def test_or_type_operator_reference_cycle(self): self.assertLessEqual(sys.gettotalrefcount() - before, leeway, msg='Check for union reference leak.') - def test_instantiation(self): - check_disallow_instantiation(self, types.UnionType) - self.assertIs(int, types.UnionType[int]) - self.assertIs(int, types.UnionType[int, int]) - self.assertEqual(int | str, types.UnionType[int, str]) - - for obj in ( - int | typing.ForwardRef("str"), - typing.Union[int, "str"], - ): - self.assertIsInstance(obj, types.UnionType) - self.assertEqual(obj.__args__, (int, EqualToForwardRef("str"))) - class MappingProxyTests(unittest.TestCase): mappingproxy = types.MappingProxyType diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 8238c62f0715f8..6deb924c6ad9f5 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -509,7 +509,7 @@ def test_cannot_instantiate_vars(self): def test_bound_errors(self): with self.assertRaises(TypeError): - TypeVar('X', bound=Optional) + TypeVar('X', bound=Union) with self.assertRaises(TypeError): TypeVar('X', str, float, bound=Employee) with self.assertRaisesRegex(TypeError, @@ -549,7 +549,7 @@ def test_var_substitution(self): def test_bad_var_substitution(self): T = TypeVar('T') bad_args = ( - (), (int, str), Optional, + (), (int, str), Union, Generic, Generic[T], Protocol, Protocol[T], Final, Final[int], ClassVar, ClassVar[int], ) @@ -2055,6 +2055,10 @@ def test_union_issubclass(self): self.assertNotIsSubclass(int, Union[Any, str]) def test_union_issubclass_type_error(self): + with self.assertRaises(TypeError): + issubclass(int, Union) + with self.assertRaises(TypeError): + issubclass(Union, int) with self.assertRaises(TypeError): issubclass(Union[int, str], int) with self.assertRaises(TypeError): @@ -2129,40 +2133,41 @@ class B(metaclass=UnhashableMeta): ... self.assertEqual(Union[A, B].__args__, (A, B)) union1 = Union[A, B] - with self.assertRaisesRegex(TypeError, "unhashable type: 'UnhashableMeta'"): + with self.assertRaises(TypeError): hash(union1) union2 = Union[int, B] - with self.assertRaisesRegex(TypeError, "unhashable type: 'UnhashableMeta'"): + with self.assertRaises(TypeError): hash(union2) union3 = Union[A, int] - with self.assertRaisesRegex(TypeError, "unhashable type: 'UnhashableMeta'"): + with self.assertRaises(TypeError): hash(union3) def test_repr(self): + self.assertEqual(repr(Union), 'typing.Union') u = Union[Employee, int] - self.assertEqual(repr(u), f'{__name__}.Employee | int') + self.assertEqual(repr(u), 'typing.Union[%s.Employee, int]' % __name__) u = Union[int, Employee] - self.assertEqual(repr(u), f'int | {__name__}.Employee') + self.assertEqual(repr(u), 'typing.Union[int, %s.Employee]' % __name__) T = TypeVar('T') u = Union[T, int][int] self.assertEqual(repr(u), repr(int)) u = Union[List[int], int] - self.assertEqual(repr(u), 'typing.List[int] | int') + self.assertEqual(repr(u), 'typing.Union[typing.List[int], int]') u = Union[list[int], dict[str, float]] - self.assertEqual(repr(u), 'list[int] | dict[str, float]') + self.assertEqual(repr(u), 'typing.Union[list[int], dict[str, float]]') u = Union[int | float] - self.assertEqual(repr(u), 'int | float') + self.assertEqual(repr(u), 'typing.Union[int, float]') u = Union[None, str] - self.assertEqual(repr(u), 'None | str') + self.assertEqual(repr(u), 'typing.Optional[str]') u = Union[str, None] - self.assertEqual(repr(u), 'str | None') + self.assertEqual(repr(u), 'typing.Optional[str]') u = Union[None, str, int] - self.assertEqual(repr(u), 'None | str | int') + self.assertEqual(repr(u), 'typing.Union[NoneType, str, int]') u = Optional[str] - self.assertEqual(repr(u), 'str | None') + self.assertEqual(repr(u), 'typing.Optional[str]') def test_dir(self): dir_items = set(dir(Union[str, int])) @@ -2174,11 +2179,14 @@ def test_dir(self): def test_cannot_subclass(self): with self.assertRaisesRegex(TypeError, - r"type 'typing\.Union' is not an acceptable base type"): + r'Cannot subclass typing\.Union'): class C(Union): pass + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): + class D(type(Union)): + pass with self.assertRaisesRegex(TypeError, - r'Cannot subclass int \| str'): + r'Cannot subclass typing\.Union\[int, str\]'): class E(Union[int, str]): pass @@ -2224,7 +2232,8 @@ def f(x: u): ... def test_function_repr_union(self): def fun() -> int: ... - self.assertEqual(repr(Union[fun, int]), f'{__name__}.{fun.__qualname__} | int') + self.assertEqual(repr(Union[fun, int]), + f'typing.Union[{__name__}.{fun.__qualname__}, int]') def test_union_str_pattern(self): # Shouldn't crash; see http://bugs.python.org/issue25390 @@ -5046,11 +5055,11 @@ class Derived(Base): ... def test_extended_generic_rules_repr(self): T = TypeVar('T') self.assertEqual(repr(Union[Tuple, Callable]).replace('typing.', ''), - 'Tuple | Callable') + 'Union[Tuple, Callable]') self.assertEqual(repr(Union[Tuple, Tuple[int]]).replace('typing.', ''), - 'Tuple | Tuple[int]') + 'Union[Tuple, Tuple[int]]') self.assertEqual(repr(Callable[..., Optional[T]][int]).replace('typing.', ''), - 'Callable[..., int | None]') + 'Callable[..., Optional[int]]') self.assertEqual(repr(Callable[[], List[T]][int]).replace('typing.', ''), 'Callable[[], List[int]]') @@ -5230,9 +5239,9 @@ def __contains__(self, item): with self.assertRaises(TypeError): issubclass(Tuple[int, ...], typing.Iterable) - def test_fail_with_special_forms(self): + def test_fail_with_bare_union(self): with self.assertRaises(TypeError): - List[Final] + List[Union] with self.assertRaises(TypeError): Tuple[Optional] with self.assertRaises(TypeError): @@ -5776,6 +5785,8 @@ def test_subclass_special_form(self): for obj in ( ClassVar[int], Final[int], + Union[int, float], + Optional[int], Literal[1, 2], Concatenate[int, ParamSpec("P")], TypeGuard[int], @@ -5807,7 +5818,7 @@ class A: __parameters__ = (T,) # Bare classes should be skipped for a in (List, list): - for b in (A, int, TypeVar, TypeVarTuple, ParamSpec, types.GenericAlias, Union): + for b in (A, int, TypeVar, TypeVarTuple, ParamSpec, types.GenericAlias, types.UnionType): with self.subTest(generic=a, sub=b): with self.assertRaisesRegex(TypeError, '.* is not a generic class'): a[b][str] @@ -5826,7 +5837,7 @@ class A: for s in (int, G, A, List, list, TypeVar, TypeVarTuple, ParamSpec, - types.GenericAlias, Union): + types.GenericAlias, types.UnionType): for t in Tuple, tuple: with self.subTest(tuple=t, sub=s): @@ -7195,7 +7206,7 @@ class C(Generic[T]): pass self.assertIs(get_origin(Callable), collections.abc.Callable) self.assertIs(get_origin(list[int]), list) self.assertIs(get_origin(list), None) - self.assertIs(get_origin(list | str), Union) + self.assertIs(get_origin(list | str), types.UnionType) self.assertIs(get_origin(P.args), P) self.assertIs(get_origin(P.kwargs), P) self.assertIs(get_origin(Required[int]), Required) @@ -10524,6 +10535,7 @@ def test_special_attrs(self): typing.TypeGuard: 'TypeGuard', typing.TypeIs: 'TypeIs', typing.TypeVar: 'TypeVar', + typing.Union: 'Union', typing.Self: 'Self', # Subscripted special forms typing.Annotated[Any, "Annotation"]: 'Annotated', @@ -10534,7 +10546,7 @@ def test_special_attrs(self): typing.Literal[Any]: 'Literal', typing.Literal[1, 2]: 'Literal', typing.Literal[True, 2]: 'Literal', - typing.Optional[Any]: 'Union', + typing.Optional[Any]: 'Optional', typing.TypeGuard[Any]: 'TypeGuard', typing.TypeIs[Any]: 'TypeIs', typing.Union[Any]: 'Any', @@ -10553,10 +10565,7 @@ def test_special_attrs(self): for proto in range(pickle.HIGHEST_PROTOCOL + 1): s = pickle.dumps(cls, proto) loaded = pickle.loads(s) - if isinstance(cls, Union): - self.assertEqual(cls, loaded) - else: - self.assertIs(cls, loaded) + self.assertIs(cls, loaded) TypeName = typing.NewType('SpecialAttrsTests.TypeName', Any) @@ -10831,37 +10840,6 @@ def test_is_not_instance_of_iterable(self): self.assertNotIsInstance(type_to_test, collections.abc.Iterable) -class UnionGenericAliasTests(BaseTestCase): - def test_constructor(self): - # Used e.g. in typer, pydantic - with self.assertWarns(DeprecationWarning): - inst = typing._UnionGenericAlias(typing.Union, (int, str)) - self.assertEqual(inst, int | str) - with self.assertWarns(DeprecationWarning): - # name is accepted but ignored - inst = typing._UnionGenericAlias(typing.Union, (int, None), name="Optional") - self.assertEqual(inst, int | None) - - def test_isinstance(self): - # Used e.g. in pydantic - with self.assertWarns(DeprecationWarning): - self.assertTrue(isinstance(Union[int, str], typing._UnionGenericAlias)) - with self.assertWarns(DeprecationWarning): - self.assertFalse(isinstance(int, typing._UnionGenericAlias)) - - def test_eq(self): - # type(t) == _UnionGenericAlias is used in vyos - with self.assertWarns(DeprecationWarning): - self.assertEqual(Union, typing._UnionGenericAlias) - with self.assertWarns(DeprecationWarning): - self.assertEqual(typing._UnionGenericAlias, typing._UnionGenericAlias) - with self.assertWarns(DeprecationWarning): - self.assertNotEqual(int, typing._UnionGenericAlias) - - def test_hashable(self): - self.assertEqual(hash(typing._UnionGenericAlias), hash(Union)) - - def load_tests(loader, tests, pattern): import doctest tests.addTests(doctest.DocTestSuite(typing)) diff --git a/Lib/typing.py b/Lib/typing.py index babe3c44d9dc55..75a2a06cf29f37 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -38,7 +38,6 @@ ParamSpecKwargs, TypeAliasType, Generic, - Union, NoDefault, ) @@ -377,11 +376,41 @@ def _deduplicate(params, *, unhashable_fallback=False): if not unhashable_fallback: raise # Happens for cases like `Annotated[dict, {'x': IntValidator()}]` - new_unhashable = [] - for t in params: - if t not in new_unhashable: - new_unhashable.append(t) - return new_unhashable + return _deduplicate_unhashable(params) + +def _deduplicate_unhashable(unhashable_params): + new_unhashable = [] + for t in unhashable_params: + if t not in new_unhashable: + new_unhashable.append(t) + return new_unhashable + +def _compare_args_orderless(first_args, second_args): + first_unhashable = _deduplicate_unhashable(first_args) + second_unhashable = _deduplicate_unhashable(second_args) + t = list(second_unhashable) + try: + for elem in first_unhashable: + t.remove(elem) + except ValueError: + return False + return not t + +def _remove_dups_flatten(parameters): + """Internal helper for Union creation and substitution. + + Flatten Unions among parameters, then remove duplicates. + """ + # Flatten out Union[Union[...], ...]. + params = [] + for p in parameters: + if isinstance(p, (_UnionGenericAlias, types.UnionType)): + params.extend(p.__args__) + else: + params.append(p) + + return tuple(_deduplicate(params, unhashable_fallback=True)) + def _flatten_literal_params(parameters): """Internal helper for Literal creation: flatten Literals among parameters.""" @@ -469,7 +498,7 @@ def _eval_type(t, globalns, localns, type_params, *, recursive_guard=frozenset() return evaluate_forward_ref(t, globals=globalns, locals=localns, type_params=type_params, owner=owner, _recursive_guard=recursive_guard, format=format) - if isinstance(t, (_GenericAlias, GenericAlias, Union)): + if isinstance(t, (_GenericAlias, GenericAlias, types.UnionType)): if isinstance(t, GenericAlias): args = tuple( _make_forward_ref(arg, parent_fwdref=parent_fwdref) if isinstance(arg, str) else arg @@ -489,7 +518,7 @@ def _eval_type(t, globalns, localns, type_params, *, recursive_guard=frozenset() return t if isinstance(t, GenericAlias): return _rebuild_generic_alias(t, ev_args) - if isinstance(t, Union): + if isinstance(t, types.UnionType): return functools.reduce(operator.or_, ev_args) else: return t.copy_with(ev_args) @@ -743,6 +772,59 @@ class FastConnector(Connection): item = _type_check(parameters, f'{self} accepts only single type.', allow_special_forms=True) return _GenericAlias(self, (item,)) +@_SpecialForm +def Union(self, parameters): + """Union type; Union[X, Y] means either X or Y. + + On Python 3.10 and higher, the | operator + can also be used to denote unions; + X | Y means the same thing to the type checker as Union[X, Y]. + + To define a union, use e.g. Union[int, str]. Details: + - The arguments must be types and there must be at least one. + - None as an argument is a special case and is replaced by + type(None). + - Unions of unions are flattened, e.g.:: + + assert Union[Union[int, str], float] == Union[int, str, float] + + - Unions of a single argument vanish, e.g.:: + + assert Union[int] == int # The constructor actually returns int + + - Redundant arguments are skipped, e.g.:: + + assert Union[int, str, int] == Union[int, str] + + - When comparing unions, the argument order is ignored, e.g.:: + + assert Union[int, str] == Union[str, int] + + - You cannot subclass or instantiate a union. + - You can use Optional[X] as a shorthand for Union[X, None]. + """ + if parameters == (): + raise TypeError("Cannot take a Union of no types.") + if not isinstance(parameters, tuple): + parameters = (parameters,) + msg = "Union[arg, ...]: each arg must be a type." + parameters = tuple(_type_check(p, msg) for p in parameters) + parameters = _remove_dups_flatten(parameters) + if len(parameters) == 1: + return parameters[0] + if len(parameters) == 2 and type(None) in parameters: + return _UnionGenericAlias(self, parameters, name="Optional") + return _UnionGenericAlias(self, parameters) + +def _make_union(left, right): + """Used from the C implementation of TypeVar. + + TypeVar.__or__ calls this instead of returning types.UnionType + because we want to allow unions between TypeVars and strings + (forward references). + """ + return Union[left, right] + @_SpecialForm def Optional(self, parameters): """Optional[X] is equivalent to Union[X, None].""" @@ -1654,41 +1736,45 @@ def __getitem__(self, params): return self.copy_with(params) -class _UnionGenericAliasMeta(type): - def __instancecheck__(self, inst: object) -> bool: - import warnings - warnings._deprecated("_UnionGenericAlias", remove=(3, 17)) - return isinstance(inst, Union) - - def __subclasscheck__(self, inst: type) -> bool: - import warnings - warnings._deprecated("_UnionGenericAlias", remove=(3, 17)) - return issubclass(inst, Union) +class _UnionGenericAlias(_NotIterable, _GenericAlias, _root=True): + def copy_with(self, params): + return Union[params] def __eq__(self, other): - import warnings - warnings._deprecated("_UnionGenericAlias", remove=(3, 17)) - if other is _UnionGenericAlias or other is Union: - return True - return NotImplemented + if not isinstance(other, (_UnionGenericAlias, types.UnionType)): + return NotImplemented + try: # fast path + return set(self.__args__) == set(other.__args__) + except TypeError: # not hashable, slow path + return _compare_args_orderless(self.__args__, other.__args__) def __hash__(self): - return hash(Union) + return hash(frozenset(self.__args__)) + def __repr__(self): + args = self.__args__ + if len(args) == 2: + if args[0] is type(None): + return f'typing.Optional[{_type_repr(args[1])}]' + elif args[1] is type(None): + return f'typing.Optional[{_type_repr(args[0])}]' + return super().__repr__() -class _UnionGenericAlias(metaclass=_UnionGenericAliasMeta): - """Compatibility hack. + def __instancecheck__(self, obj): + for arg in self.__args__: + if isinstance(obj, arg): + return True + return False - A class named _UnionGenericAlias used to be used to implement - typing.Union. This class exists to serve as a shim to preserve - the meaning of some code that used to use _UnionGenericAlias - directly. + def __subclasscheck__(self, cls): + for arg in self.__args__: + if issubclass(cls, arg): + return True + return False - """ - def __new__(cls, self_cls, parameters, /, *, name=None): - import warnings - warnings._deprecated("_UnionGenericAlias", remove=(3, 17)) - return Union[parameters] + def __reduce__(self): + func, (origin, args) = super().__reduce__() + return func, (Union, args) def _value_and_type_iter(parameters): @@ -2451,7 +2537,7 @@ def _strip_annotations(t): if stripped_args == t.__args__: return t return _rebuild_generic_alias(t, stripped_args) - if isinstance(t, Union): + if isinstance(t, types.UnionType): stripped_args = tuple(_strip_annotations(a) for a in t.__args__) if stripped_args == t.__args__: return t @@ -2485,8 +2571,8 @@ def get_origin(tp): return tp.__origin__ if tp is Generic: return Generic - if isinstance(tp, Union): - return Union + if isinstance(tp, types.UnionType): + return types.UnionType return None @@ -2511,7 +2597,7 @@ def get_args(tp): if _should_unflatten_callable_args(tp, res): res = (list(res[:-1]), res[-1]) return res - if isinstance(tp, Union): + if isinstance(tp, types.UnionType): return tp.__args__ return () diff --git a/Misc/NEWS.d/3.14.0a4.rst b/Misc/NEWS.d/3.14.0a4.rst index 176ba72da65e4b..f5737d70ded009 100644 --- a/Misc/NEWS.d/3.14.0a4.rst +++ b/Misc/NEWS.d/3.14.0a4.rst @@ -164,8 +164,8 @@ with meta (i.e. :kbd:`Alt`), e.g. :kbd:`Alt-d` to ``kill-word`` or .. nonce: RIvgwc .. section: Library -Unify the instance check for :class:`typing.Union` and -:class:`types.UnionType`: :class:`!Union` now uses the instance checks +Unify the instance check in :data:`typing.Union` and +:class:`types.UnionType`: :data:`!Union` now uses the instance checks against its parameters instead of the subclass checks. .. diff --git a/Misc/NEWS.d/3.14.0a6.rst b/Misc/NEWS.d/3.14.0a6.rst index 9064402bcf71f6..12c88e648b0b97 100644 --- a/Misc/NEWS.d/3.14.0a6.rst +++ b/Misc/NEWS.d/3.14.0a6.rst @@ -834,7 +834,7 @@ performance. Patch by Romain Morotti. .. nonce: 7jV6cP .. section: Library -Make :class:`types.UnionType` an alias for :class:`typing.Union`. Both ``int +[Reverted in :gh:`137065`] Make :class:`types.UnionType` an alias for :data:`typing.Union`. Both ``int | str`` and ``Union[int, str]`` now create instances of the same type. Patch by Jelle Zijlstra. diff --git a/Misc/NEWS.d/3.14.0b1.rst b/Misc/NEWS.d/3.14.0b1.rst index 5d03d429f9ee14..ddeda4c5e6004d 100644 --- a/Misc/NEWS.d/3.14.0b1.rst +++ b/Misc/NEWS.d/3.14.0b1.rst @@ -407,7 +407,7 @@ Speedup pasting in ``PyREPL`` on Windows. Fix by Chris Eibl. .. nonce: 6zoyp5 .. section: Library -Fix copying of :class:`typing.Union` objects containing objects that do not +Fix copying of :data:`typing.Union` objects containing objects that do not support the ``|`` operator. .. diff --git a/Misc/NEWS.d/next/Library/2025-09-16-11-49-21.gh-issue-137065.W2-dKY.rst b/Misc/NEWS.d/next/Library/2025-09-16-11-49-21.gh-issue-137065.W2-dKY.rst new file mode 100644 index 00000000000000..600ee91e7acd50 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-16-11-49-21.gh-issue-137065.W2-dKY.rst @@ -0,0 +1,2 @@ +Revert :gh:`105499`. Restore :data:`typing.Union` and +:class:`types.UnionType`. diff --git a/Modules/_typingmodule.c b/Modules/_typingmodule.c index e51279c808a2e1..09fbb3c5e8b91d 100644 --- a/Modules/_typingmodule.c +++ b/Modules/_typingmodule.c @@ -5,10 +5,9 @@ #endif #include "Python.h" -#include "internal/pycore_interp.h" -#include "internal/pycore_typevarobject.h" -#include "internal/pycore_unionobject.h" // _PyUnion_Type +#include "pycore_interp.h" #include "pycore_pystate.h" // _PyInterpreterState_GET() +#include "pycore_typevarobject.h" #include "clinic/_typingmodule.c.h" /*[clinic input] @@ -64,9 +63,6 @@ _typing_exec(PyObject *m) if (PyModule_AddObjectRef(m, "TypeAliasType", (PyObject *)&_PyTypeAlias_Type) < 0) { return -1; } - if (PyModule_AddObjectRef(m, "Union", (PyObject *)&_PyUnion_Type) < 0) { - return -1; - } if (PyModule_AddObjectRef(m, "NoDefault", (PyObject *)&_Py_NoDefaultStruct) < 0) { return -1; } diff --git a/Objects/typevarobject.c b/Objects/typevarobject.c index 522e9fd9c955af..6aa24fd8b6beeb 100644 --- a/Objects/typevarobject.c +++ b/Objects/typevarobject.c @@ -4,7 +4,7 @@ #include "pycore_object.h" // _PyObject_GC_TRACK/UNTRACK, PyAnnotateFormat #include "pycore_typevarobject.h" #include "pycore_unicodeobject.h" // _PyUnicode_EqualToASCIIString() -#include "pycore_unionobject.h" // _Py_union_type_or, _Py_union_from_tuple +#include "pycore_unionobject.h" // _Py_union_type_or #include "structmember.h" /*[clinic input] @@ -372,13 +372,9 @@ type_check(PyObject *arg, const char *msg) static PyObject * make_union(PyObject *self, PyObject *other) { - PyObject *args = PyTuple_Pack(2, self, other); - if (args == NULL) { - return NULL; - } - PyObject *u = _Py_union_from_tuple(args); - Py_DECREF(args); - return u; + PyObject *args[2] = {self, other}; + PyObject *result = call_typing_func_object("_make_union", args, 2); + return result; } static PyObject * diff --git a/Objects/unionobject.c b/Objects/unionobject.c index 2206ed80ef03fd..d2f9508a510f8f 100644 --- a/Objects/unionobject.c +++ b/Objects/unionobject.c @@ -1,4 +1,4 @@ -// typing.Union -- used to represent e.g. Union[int, str], int | str +// types.UnionType -- used to represent e.g. Union[int, str], int | str #include "Python.h" #include "pycore_object.h" // _PyObject_GC_TRACK/UNTRACK #include "pycore_typevarobject.h" // _PyTypeAlias_Type, _Py_typing_type_repr @@ -7,13 +7,13 @@ #include "pycore_weakref.h" // FT_CLEAR_WEAKREFS() +static PyObject *make_union(PyObject *); + + typedef struct { PyObject_HEAD - PyObject *args; // all args (tuple) - PyObject *hashable_args; // frozenset or NULL - PyObject *unhashable_args; // tuple or NULL + PyObject *args; PyObject *parameters; - PyObject *weakreflist; } unionobject; static void @@ -22,11 +22,8 @@ unionobject_dealloc(PyObject *self) unionobject *alias = (unionobject *)self; _PyObject_GC_UNTRACK(self); - FT_CLEAR_WEAKREFS(self, alias->weakreflist); Py_XDECREF(alias->args); - Py_XDECREF(alias->hashable_args); - Py_XDECREF(alias->unhashable_args); Py_XDECREF(alias->parameters); Py_TYPE(self)->tp_free(self); } @@ -36,8 +33,6 @@ union_traverse(PyObject *self, visitproc visit, void *arg) { unionobject *alias = (unionobject *)self; Py_VISIT(alias->args); - Py_VISIT(alias->hashable_args); - Py_VISIT(alias->unhashable_args); Py_VISIT(alias->parameters); return 0; } @@ -46,67 +41,13 @@ static Py_hash_t union_hash(PyObject *self) { unionobject *alias = (unionobject *)self; - // If there are any unhashable args, treat this union as unhashable. - // Otherwise, two unions might compare equal but have different hashes. - if (alias->unhashable_args) { - // Attempt to get an error from one of the values. - assert(PyTuple_CheckExact(alias->unhashable_args)); - Py_ssize_t n = PyTuple_GET_SIZE(alias->unhashable_args); - for (Py_ssize_t i = 0; i < n; i++) { - PyObject *arg = PyTuple_GET_ITEM(alias->unhashable_args, i); - Py_hash_t hash = PyObject_Hash(arg); - if (hash == -1) { - return -1; - } - } - // The unhashable values somehow became hashable again. Still raise - // an error. - PyErr_Format(PyExc_TypeError, "union contains %d unhashable elements", n); - return -1; - } - return PyObject_Hash(alias->hashable_args); -} - -static int -unions_equal(unionobject *a, unionobject *b) -{ - int result = PyObject_RichCompareBool(a->hashable_args, b->hashable_args, Py_EQ); - if (result == -1) { - return -1; - } - if (result == 0) { - return 0; - } - if (a->unhashable_args && b->unhashable_args) { - Py_ssize_t n = PyTuple_GET_SIZE(a->unhashable_args); - if (n != PyTuple_GET_SIZE(b->unhashable_args)) { - return 0; - } - for (Py_ssize_t i = 0; i < n; i++) { - PyObject *arg_a = PyTuple_GET_ITEM(a->unhashable_args, i); - int result = PySequence_Contains(b->unhashable_args, arg_a); - if (result == -1) { - return -1; - } - if (!result) { - return 0; - } - } - for (Py_ssize_t i = 0; i < n; i++) { - PyObject *arg_b = PyTuple_GET_ITEM(b->unhashable_args, i); - int result = PySequence_Contains(a->unhashable_args, arg_b); - if (result == -1) { - return -1; - } - if (!result) { - return 0; - } - } - } - else if (a->unhashable_args || b->unhashable_args) { - return 0; + PyObject *args = PyFrozenSet_New(alias->args); + if (args == NULL) { + return (Py_hash_t)-1; } - return 1; + Py_hash_t hash = PyObject_Hash(args); + Py_DECREF(args); + return hash; } static PyObject * @@ -116,130 +57,95 @@ union_richcompare(PyObject *a, PyObject *b, int op) Py_RETURN_NOTIMPLEMENTED; } - int equal = unions_equal((unionobject*)a, (unionobject*)b); - if (equal == -1) { + PyObject *a_set = PySet_New(((unionobject*)a)->args); + if (a_set == NULL) { return NULL; } - if (op == Py_EQ) { - return PyBool_FromLong(equal); - } - else { - return PyBool_FromLong(!equal); + PyObject *b_set = PySet_New(((unionobject*)b)->args); + if (b_set == NULL) { + Py_DECREF(a_set); + return NULL; } + PyObject *result = PyObject_RichCompare(a_set, b_set, op); + Py_DECREF(b_set); + Py_DECREF(a_set); + return result; } -typedef struct { - PyObject *args; // list - PyObject *hashable_args; // set - PyObject *unhashable_args; // list or NULL - bool is_checked; // whether to call type_check() -} unionbuilder; - -static bool unionbuilder_add_tuple(unionbuilder *, PyObject *); -static PyObject *make_union(unionbuilder *); -static PyObject *type_check(PyObject *, const char *); - -static bool -unionbuilder_init(unionbuilder *ub, bool is_checked) +static int +is_same(PyObject *left, PyObject *right) { - ub->args = PyList_New(0); - if (ub->args == NULL) { - return false; - } - ub->hashable_args = PySet_New(NULL); - if (ub->hashable_args == NULL) { - Py_DECREF(ub->args); - return false; - } - ub->unhashable_args = NULL; - ub->is_checked = is_checked; - return true; + int is_ga = _PyGenericAlias_Check(left) && _PyGenericAlias_Check(right); + return is_ga ? PyObject_RichCompareBool(left, right, Py_EQ) : left == right; } -static void -unionbuilder_finalize(unionbuilder *ub) +static int +contains(PyObject **items, Py_ssize_t size, PyObject *obj) { - Py_DECREF(ub->args); - Py_DECREF(ub->hashable_args); - Py_XDECREF(ub->unhashable_args); + for (Py_ssize_t i = 0; i < size; i++) { + int is_duplicate = is_same(items[i], obj); + if (is_duplicate) { // -1 or 1 + return is_duplicate; + } + } + return 0; } -static bool -unionbuilder_add_single_unchecked(unionbuilder *ub, PyObject *arg) +static PyObject * +merge(PyObject **items1, Py_ssize_t size1, + PyObject **items2, Py_ssize_t size2) { - Py_hash_t hash = PyObject_Hash(arg); - if (hash == -1) { - PyErr_Clear(); - if (ub->unhashable_args == NULL) { - ub->unhashable_args = PyList_New(0); - if (ub->unhashable_args == NULL) { - return false; - } + PyObject *tuple = NULL; + Py_ssize_t pos = 0; + + for (Py_ssize_t i = 0; i < size2; i++) { + PyObject *arg = items2[i]; + int is_duplicate = contains(items1, size1, arg); + if (is_duplicate < 0) { + Py_XDECREF(tuple); + return NULL; + } + if (is_duplicate) { + continue; } - else { - int contains = PySequence_Contains(ub->unhashable_args, arg); - if (contains < 0) { - return false; + + if (tuple == NULL) { + tuple = PyTuple_New(size1 + size2 - i); + if (tuple == NULL) { + return NULL; } - if (contains == 1) { - return true; + for (; pos < size1; pos++) { + PyObject *a = items1[pos]; + PyTuple_SET_ITEM(tuple, pos, Py_NewRef(a)); } } - if (PyList_Append(ub->unhashable_args, arg) < 0) { - return false; - } + PyTuple_SET_ITEM(tuple, pos, Py_NewRef(arg)); + pos++; } - else { - int contains = PySet_Contains(ub->hashable_args, arg); - if (contains < 0) { - return false; - } - if (contains == 1) { - return true; - } - if (PySet_Add(ub->hashable_args, arg) < 0) { - return false; - } + + if (tuple) { + (void) _PyTuple_Resize(&tuple, pos); } - return PyList_Append(ub->args, arg) == 0; + return tuple; } -static bool -unionbuilder_add_single(unionbuilder *ub, PyObject *arg) +static PyObject ** +get_types(PyObject **obj, Py_ssize_t *size) { - if (Py_IsNone(arg)) { - arg = (PyObject *)&_PyNone_Type; // immortal, so no refcounting needed - } - else if (_PyUnion_Check(arg)) { - PyObject *args = ((unionobject *)arg)->args; - return unionbuilder_add_tuple(ub, args); + if (*obj == Py_None) { + *obj = (PyObject *)&_PyNone_Type; } - if (ub->is_checked) { - PyObject *type = type_check(arg, "Union[arg, ...]: each arg must be a type."); - if (type == NULL) { - return false; - } - bool result = unionbuilder_add_single_unchecked(ub, type); - Py_DECREF(type); - return result; + if (_PyUnion_Check(*obj)) { + PyObject *args = ((unionobject *) *obj)->args; + *size = PyTuple_GET_SIZE(args); + return &PyTuple_GET_ITEM(args, 0); } else { - return unionbuilder_add_single_unchecked(ub, arg); + *size = 1; + return obj; } } -static bool -unionbuilder_add_tuple(unionbuilder *ub, PyObject *tuple) -{ - Py_ssize_t n = PyTuple_GET_SIZE(tuple); - for (Py_ssize_t i = 0; i < n; i++) { - if (!unionbuilder_add_single(ub, PyTuple_GET_ITEM(tuple, i))) { - return false; - } - } - return true; -} - static int is_unionable(PyObject *obj) { @@ -260,18 +166,19 @@ _Py_union_type_or(PyObject* self, PyObject* other) Py_RETURN_NOTIMPLEMENTED; } - unionbuilder ub; - // unchecked because we already checked is_unionable() - if (!unionbuilder_init(&ub, false)) { - return NULL; - } - if (!unionbuilder_add_single(&ub, self) || - !unionbuilder_add_single(&ub, other)) { - unionbuilder_finalize(&ub); - return NULL; + Py_ssize_t size1, size2; + PyObject **items1 = get_types(&self, &size1); + PyObject **items2 = get_types(&other, &size2); + PyObject *tuple = merge(items1, size1, items2, size2); + if (tuple == NULL) { + if (PyErr_Occurred()) { + return NULL; + } + return Py_NewRef(self); } - PyObject *new_union = make_union(&ub); + PyObject *new_union = make_union(tuple); + Py_DECREF(tuple); return new_union; } @@ -350,7 +257,21 @@ union_getitem(PyObject *self, PyObject *item) return NULL; } - PyObject *res = _Py_union_from_tuple(newargs); + PyObject *res; + Py_ssize_t nargs = PyTuple_GET_SIZE(newargs); + if (nargs == 0) { + res = make_union(newargs); + } + else { + res = Py_NewRef(PyTuple_GET_ITEM(newargs, 0)); + for (Py_ssize_t iarg = 1; iarg < nargs; iarg++) { + PyObject *arg = PyTuple_GET_ITEM(newargs, iarg); + Py_SETREF(res, PyNumber_Or(res, arg)); + if (res == NULL) { + break; + } + } + } Py_DECREF(newargs); return res; } @@ -369,25 +290,7 @@ union_parameters(PyObject *self, void *Py_UNUSED(unused)) return Py_NewRef(alias->parameters); } -static PyObject * -union_name(PyObject *Py_UNUSED(self), void *Py_UNUSED(ignored)) -{ - return PyUnicode_FromString("Union"); -} - -static PyObject * -union_origin(PyObject *Py_UNUSED(self), void *Py_UNUSED(ignored)) -{ - return Py_NewRef(&_PyUnion_Type); -} - static PyGetSetDef union_properties[] = { - {"__name__", union_name, NULL, - PyDoc_STR("Name of the type"), NULL}, - {"__qualname__", union_name, NULL, - PyDoc_STR("Qualified name of the type"), NULL}, - {"__origin__", union_origin, NULL, - PyDoc_STR("Always returns the type"), NULL}, {"__parameters__", union_parameters, NULL, PyDoc_STR("Type variables in the types.UnionType."), NULL}, {0} @@ -426,88 +329,10 @@ _Py_union_args(PyObject *self) return ((unionobject *) self)->args; } -static PyObject * -call_typing_func_object(const char *name, PyObject **args, size_t nargs) -{ - PyObject *typing = PyImport_ImportModule("typing"); - if (typing == NULL) { - return NULL; - } - PyObject *func = PyObject_GetAttrString(typing, name); - if (func == NULL) { - Py_DECREF(typing); - return NULL; - } - PyObject *result = PyObject_Vectorcall(func, args, nargs, NULL); - Py_DECREF(func); - Py_DECREF(typing); - return result; -} - -static PyObject * -type_check(PyObject *arg, const char *msg) -{ - if (Py_IsNone(arg)) { - // NoneType is immortal, so don't need an INCREF - return (PyObject *)Py_TYPE(arg); - } - // Fast path to avoid calling into typing.py - if (is_unionable(arg)) { - return Py_NewRef(arg); - } - PyObject *message_str = PyUnicode_FromString(msg); - if (message_str == NULL) { - return NULL; - } - PyObject *args[2] = {arg, message_str}; - PyObject *result = call_typing_func_object("_type_check", args, 2); - Py_DECREF(message_str); - return result; -} - -PyObject * -_Py_union_from_tuple(PyObject *args) -{ - unionbuilder ub; - if (!unionbuilder_init(&ub, true)) { - return NULL; - } - if (PyTuple_CheckExact(args)) { - if (!unionbuilder_add_tuple(&ub, args)) { - return NULL; - } - } - else { - if (!unionbuilder_add_single(&ub, args)) { - return NULL; - } - } - return make_union(&ub); -} - -static PyObject * -union_class_getitem(PyObject *cls, PyObject *args) -{ - return _Py_union_from_tuple(args); -} - -static PyObject * -union_mro_entries(PyObject *self, PyObject *args) -{ - return PyErr_Format(PyExc_TypeError, - "Cannot subclass %R", self); -} - -static PyMethodDef union_methods[] = { - {"__mro_entries__", union_mro_entries, METH_O}, - {"__class_getitem__", union_class_getitem, METH_O|METH_CLASS, PyDoc_STR("See PEP 585")}, - {0} -}; - PyTypeObject _PyUnion_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) - .tp_name = "typing.Union", - .tp_doc = PyDoc_STR("Represent a union type\n" + .tp_name = "types.UnionType", + .tp_doc = PyDoc_STR("Represent a PEP 604 union type\n" "\n" "E.g. for int | str"), .tp_basicsize = sizeof(unionobject), @@ -519,64 +344,25 @@ PyTypeObject _PyUnion_Type = { .tp_hash = union_hash, .tp_getattro = union_getattro, .tp_members = union_members, - .tp_methods = union_methods, .tp_richcompare = union_richcompare, .tp_as_mapping = &union_as_mapping, .tp_as_number = &union_as_number, .tp_repr = union_repr, .tp_getset = union_properties, - .tp_weaklistoffset = offsetof(unionobject, weakreflist), }; static PyObject * -make_union(unionbuilder *ub) +make_union(PyObject *args) { - Py_ssize_t n = PyList_GET_SIZE(ub->args); - if (n == 0) { - PyErr_SetString(PyExc_TypeError, "Cannot take a Union of no types."); - unionbuilder_finalize(ub); - return NULL; - } - if (n == 1) { - PyObject *result = PyList_GET_ITEM(ub->args, 0); - Py_INCREF(result); - unionbuilder_finalize(ub); - return result; - } - - PyObject *args = NULL, *hashable_args = NULL, *unhashable_args = NULL; - args = PyList_AsTuple(ub->args); - if (args == NULL) { - goto error; - } - hashable_args = PyFrozenSet_New(ub->hashable_args); - if (hashable_args == NULL) { - goto error; - } - if (ub->unhashable_args != NULL) { - unhashable_args = PyList_AsTuple(ub->unhashable_args); - if (unhashable_args == NULL) { - goto error; - } - } + assert(PyTuple_CheckExact(args)); unionobject *result = PyObject_GC_New(unionobject, &_PyUnion_Type); if (result == NULL) { - goto error; + return NULL; } - unionbuilder_finalize(ub); result->parameters = NULL; - result->args = args; - result->hashable_args = hashable_args; - result->unhashable_args = unhashable_args; - result->weakreflist = NULL; + result->args = Py_NewRef(args); _PyObject_GC_TRACK(result); return (PyObject*)result; -error: - Py_XDECREF(args); - Py_XDECREF(hashable_args); - Py_XDECREF(unhashable_args); - unionbuilder_finalize(ub); - return NULL; }