From f65e31fee3b55dfb6ed5398179d5c5d6b502dee5 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 18 May 2018 16:00:38 -0700 Subject: [PATCH] bpo-28556: Don't simplify unions at runtime (GH-6841) --- Doc/library/typing.rst | 7 ++-- Lib/test/test_typing.py | 24 +++++++------- Lib/typing.py | 32 ++----------------- .../2018-05-17-22-53-08.bpo-28556.C6Hnd1.rst | 3 ++ 4 files changed, 21 insertions(+), 45 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2018-05-17-22-53-08.bpo-28556.C6Hnd1.rst diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 142e169b89..be6636eea0 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -961,16 +961,15 @@ The module defines the following classes, functions and decorators: Union[int, str] == Union[str, int] - * When a class and its subclass are present, the latter is skipped, e.g.:: - - Union[int, object] == object - * You cannot subclass or instantiate a union. * You cannot write ``Union[X][Y]``. * You can use ``Optional[X]`` as a shorthand for ``Union[X, None]``. + .. versionchanged:: 3.7 + Don't remove explicit subclasses from unions at runtime. + .. data:: Optional Optional type. diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index be768f12fb..904cd93691 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -253,10 +253,11 @@ class UnionTests(BaseTestCase): def test_union_object(self): u = Union[object] self.assertEqual(u, object) - u = Union[int, object] - self.assertEqual(u, object) - u = Union[object, int] - self.assertEqual(u, object) + u1 = Union[int, object] + u2 = Union[object, int] + self.assertEqual(u1, u2) + self.assertNotEqual(u1, object) + self.assertNotEqual(u2, object) def test_unordered(self): u1 = Union[int, float] @@ -267,13 +268,11 @@ class UnionTests(BaseTestCase): t = Union[Employee] self.assertIs(t, Employee) - def test_base_class_disappears(self): - u = Union[Employee, Manager, int] - self.assertEqual(u, Union[int, Employee]) - u = Union[Manager, int, Employee] - self.assertEqual(u, Union[int, Employee]) + def test_base_class_kept(self): u = Union[Employee, Manager] - self.assertIs(u, Employee) + self.assertNotEqual(u, Employee) + self.assertIn(Employee, u.__args__) + self.assertIn(Manager, u.__args__) def test_union_union(self): u = Union[int, float] @@ -317,7 +316,8 @@ class UnionTests(BaseTestCase): def test_union_generalization(self): self.assertFalse(Union[str, typing.Iterable[int]] == str) self.assertFalse(Union[str, typing.Iterable[int]] == typing.Iterable[int]) - self.assertTrue(Union[str, typing.Iterable] == typing.Iterable) + self.assertIn(str, Union[str, typing.Iterable[int]].__args__) + self.assertIn(typing.Iterable[int], Union[str, typing.Iterable[int]].__args__) def test_union_compare_other(self): self.assertNotEqual(Union, object) @@ -917,7 +917,7 @@ class GenericTests(BaseTestCase): self.assertEqual(Union[T, U][int, Union[int, str]], Union[int, str]) class Base: ... class Derived(Base): ... - self.assertEqual(Union[T, Base][Derived], Base) + self.assertEqual(Union[T, Base][Union[Base, Derived]], Union[Base, Derived]) with self.assertRaises(TypeError): Union[T, int][1] diff --git a/Lib/typing.py b/Lib/typing.py index b10615c07f..3e82c6b1bb 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -206,8 +206,8 @@ def _check_generic(cls, parameters): def _remove_dups_flatten(parameters): - """An internal helper for Union creation and substitution: flatten Union's - among parameters, then remove duplicates and strict subclasses. + """An internal helper for Union creation and substitution: flatten Unions + among parameters, then remove duplicates. """ # Flatten out Union[Union[...], ...]. params = [] @@ -228,20 +228,7 @@ def _remove_dups_flatten(parameters): all_params.remove(t) params = new_params assert not all_params, all_params - # Weed out subclasses. - # E.g. Union[int, Employee, Manager] == Union[int, Employee]. - # If object is present it will be sole survivor among proper classes. - # Never discard type variables. - # (In particular, Union[str, AnyStr] != AnyStr.) - all_params = set(params) - for t1 in params: - if not isinstance(t1, type): - continue - if any((isinstance(t2, type) or - isinstance(t2, _GenericAlias) and t2._special) and issubclass(t1, t2) - for t2 in all_params - {t1}): - all_params.remove(t1) - return tuple(t for t in params if t in all_params) + return tuple(params) _cleanups = [] @@ -440,19 +427,6 @@ Union = _SpecialForm('Union', doc= Union[int, str] == Union[str, int] - - When two arguments have a subclass relationship, the least - derived argument is kept, e.g.:: - - class Employee: pass - class Manager(Employee): pass - Union[int, Employee, Manager] == Union[int, Employee] - Union[Manager, int, Employee] == Union[int, Employee] - Union[Employee, Manager] == Employee - - - Similar for object:: - - Union[int, object] == object - - You cannot subclass or instantiate a union. - You can use Optional[X] as a shorthand for Union[X, None]. """) diff --git a/Misc/NEWS.d/next/Library/2018-05-17-22-53-08.bpo-28556.C6Hnd1.rst b/Misc/NEWS.d/next/Library/2018-05-17-22-53-08.bpo-28556.C6Hnd1.rst new file mode 100644 index 0000000000..35e13bde18 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-05-17-22-53-08.bpo-28556.C6Hnd1.rst @@ -0,0 +1,3 @@ +Do not simplify arguments to `typing.Union`. Now `Union[Manager, Employee]` +is not simplified to `Employee` at runtime. Such simplification previously +caused several bugs and limited possibilities for introspection. -- 2.40.0