From 03637e2cb248f7ebf7ac45f6ecce439ffe3f6c61 Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Sun, 15 Nov 2020 15:23:48 +0200 Subject: [PATCH 1/8] bpo-42345: Literal equality no longer depends on order of arguments --- Lib/test/test_typing.py | 9 +++++++ Lib/typing.py | 25 ++++++++++++++++++- Misc/ACKS | 1 + .../2020-11-15-15-23-34.bpo-42345.hiIR7x.rst | 2 ++ 4 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2020-11-15-15-23-34.bpo-42345.hiIR7x.rst diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 2ab8be49b2875a..00df87de0be058 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -559,6 +559,15 @@ def test_no_multiple_subscripts(self): with self.assertRaises(TypeError): Literal[1][1] + def test_equal(self): + self.assertEqual(Literal[1, 2], Literal[2, 1]) + self.assertNotEqual(Literal[1, True], Literal[1]) + + def test_flatten(self): + self.assertEqual(Literal[Literal[1], Literal[2], Literal[3]], Literal[1, 2, 3]) + self.assertEqual(Literal[Literal[1, 2], 3], Literal[1, 2, 3]) + self.assertEqual(Literal[Literal[1, 2, 3]], Literal[1, 2, 3]) + XK = TypeVar('XK', str, bytes) XV = TypeVar('XV') diff --git a/Lib/typing.py b/Lib/typing.py index 3fa97a4a15f954..4a7098345f3d4f 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -228,6 +228,17 @@ def _remove_dups_flatten(parameters): return tuple(params) +def _flatten_literal_params(parameters): + """An internal helper for Literal creation: flatten Literals among parameters""" + params = [] + for p in parameters: + if isinstance(p, _LiteralGenericAlias): + params.extend(p.__args__) + else: + params.append(p) + return tuple(params) + + _cleanups = [] @@ -460,7 +471,11 @@ def open_helper(file: str, mode: MODE) -> str: """ # There is no '_type_check' call because arguments to Literal[...] are # values, not types. - return _GenericAlias(self, parameters) + if not isinstance(parameters, tuple): + parameters = (parameters,) + + parameters = _flatten_literal_params(parameters) + return _LiteralGenericAlias(self, parameters) @_SpecialForm @@ -930,6 +945,14 @@ def __subclasscheck__(self, cls): return True +class _LiteralGenericAlias(_GenericAlias, _root=True): + + def __eq__(self, other): + if not isinstance(other, _LiteralGenericAlias): + return NotImplemented + + return len(self.__args__) == len(other.__args__) and set(self.__args__) == set(other.__args__) + class Generic: """Abstract base class for generic types. diff --git a/Misc/ACKS b/Misc/ACKS index 35a87ae6b965da..1d106144d467f9 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -861,6 +861,7 @@ Jan Kanis Rafe Kaplan Jacob Kaplan-Moss Allison Kaptur +Yurii Karabas Janne Karila Per Øyvind Karlsen Anton Kasyanov diff --git a/Misc/NEWS.d/next/Library/2020-11-15-15-23-34.bpo-42345.hiIR7x.rst b/Misc/NEWS.d/next/Library/2020-11-15-15-23-34.bpo-42345.hiIR7x.rst new file mode 100644 index 00000000000000..8405d5ea38d3e1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-11-15-15-23-34.bpo-42345.hiIR7x.rst @@ -0,0 +1,2 @@ +Literal equality no longer depends on order of arguments. Patch provided by +Yurii Karabas. From 4fbf5900c8d946942890d167eeb7f107841df07b Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Sun, 15 Nov 2020 17:21:08 +0200 Subject: [PATCH 2/8] Use value and type for Literal equality check --- Lib/test/test_typing.py | 3 ++- Lib/typing.py | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 00df87de0be058..aacab5cc7d4d3a 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -560,8 +560,9 @@ def test_no_multiple_subscripts(self): Literal[1][1] def test_equal(self): + self.assertEqual(Literal[1], Literal[1]) self.assertEqual(Literal[1, 2], Literal[2, 1]) - self.assertNotEqual(Literal[1, True], Literal[1]) + self.assertEqual(Literal[1, 2, 3], Literal[1, 2, 3, 3]) def test_flatten(self): self.assertEqual(Literal[Literal[1], Literal[2], Literal[3]], Literal[1, 2, 3]) diff --git a/Lib/typing.py b/Lib/typing.py index 4a7098345f3d4f..994c2a05ae74ff 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -945,13 +945,20 @@ def __subclasscheck__(self, cls): return True +def _value_and_type_iter(parameters): + return ((p, type(p)) for p in parameters) + + class _LiteralGenericAlias(_GenericAlias, _root=True): def __eq__(self, other): if not isinstance(other, _LiteralGenericAlias): return NotImplemented - return len(self.__args__) == len(other.__args__) and set(self.__args__) == set(other.__args__) + return set(_value_and_type_iter(self.__args__)) == set(_value_and_type_iter(other.__args__)) + + def __hash__(self): + return hash(tuple(_value_and_type_iter(self.__args__))) class Generic: From ea06129e629c947bebee7924a4542b09fb21e611 Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Sun, 15 Nov 2020 18:14:37 +0200 Subject: [PATCH 3/8] Return Literal equality test --- Lib/test/test_typing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index aacab5cc7d4d3a..d0618f03eac4a6 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -560,6 +560,7 @@ def test_no_multiple_subscripts(self): Literal[1][1] def test_equal(self): + self.assertNotEqual(Literal[1, True], Literal[1]) self.assertEqual(Literal[1], Literal[1]) self.assertEqual(Literal[1, 2], Literal[2, 1]) self.assertEqual(Literal[1, 2, 3], Literal[1, 2, 3, 3]) From b8a597a7279d54957158473fa5efa4d81884c586 Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Sun, 15 Nov 2020 18:47:40 +0200 Subject: [PATCH 4/8] Use typed lru_cache for Literal type --- Lib/test/test_typing.py | 3 +++ Lib/typing.py | 36 ++++++++++++++++++++++++------------ 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index d0618f03eac4a6..c5ae0139775b1e 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -560,6 +560,9 @@ def test_no_multiple_subscripts(self): Literal[1][1] def test_equal(self): + self.assertNotEqual(Literal[0], Literal[False]) + self.assertNotEqual(Literal[True], Literal[1]) + self.assertNotEqual(Literal[1], Literal[2]) self.assertNotEqual(Literal[1, True], Literal[1]) self.assertEqual(Literal[1], Literal[1]) self.assertEqual(Literal[1, 2], Literal[2, 1]) diff --git a/Lib/typing.py b/Lib/typing.py index 994c2a05ae74ff..30adf59de0dcde 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -242,22 +242,27 @@ def _flatten_literal_params(parameters): _cleanups = [] -def _tp_cache(func): +def _tp_cache(typed=False): """Internal wrapper caching __getitem__ of generic types with a fallback to original function for non-hashable arguments. """ - cached = functools.lru_cache()(func) - _cleanups.append(cached.cache_clear) + if callable(typed): + return _tp_cache()(typed) - @functools.wraps(func) - def inner(*args, **kwds): - try: - return cached(*args, **kwds) - except TypeError: - pass # All real errors (not unhashable args) are raised below. - return func(*args, **kwds) - return inner + def decorator(func): + cached = functools.lru_cache(typed=typed)(func) + _cleanups.append(cached.cache_clear) + + @functools.wraps(func) + def inner(*args, **kwds): + try: + return cached(*args, **kwds) + except TypeError: + pass # All real errors (not unhashable args) are raised below. + return func(*args, **kwds) + return inner + return decorator def _eval_type(t, globalns, localns, recursive_guard=frozenset()): """Evaluate all forward references in the given type t. @@ -330,6 +335,13 @@ def __subclasscheck__(self, cls): def __getitem__(self, parameters): return self._getitem(self, parameters) + +class _LiteralSpecialForm(_SpecialForm, _root=True): + @_tp_cache(typed=True) + def __getitem__(self, parameters): + return self._getitem(self, parameters) + + @_SpecialForm def Any(self, parameters): """Special type indicating an unconstrained type. @@ -447,7 +459,7 @@ def Optional(self, parameters): arg = _type_check(parameters, f"{self} requires a single type.") return Union[arg, type(None)] -@_SpecialForm +@_LiteralSpecialForm def Literal(self, parameters): """Special typing form to define literal types (a.k.a. value types). From 19b095203c84d750a7ef52b977ee1110a1adedbb Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Sun, 15 Nov 2020 19:57:58 +0200 Subject: [PATCH 5/8] Update Misc/NEWS --- .../next/Library/2020-11-15-15-23-34.bpo-42345.hiIR7x.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2020-11-15-15-23-34.bpo-42345.hiIR7x.rst b/Misc/NEWS.d/next/Library/2020-11-15-15-23-34.bpo-42345.hiIR7x.rst index 8405d5ea38d3e1..423b5a35abd52f 100644 --- a/Misc/NEWS.d/next/Library/2020-11-15-15-23-34.bpo-42345.hiIR7x.rst +++ b/Misc/NEWS.d/next/Library/2020-11-15-15-23-34.bpo-42345.hiIR7x.rst @@ -1,2 +1,2 @@ -Literal equality no longer depends on order of arguments. Patch provided by -Yurii Karabas. +Fix ``typing.Literal`` equals method to ignore the order of arguments. +Patch provided by Yurii Karabas. From 9d2187a907f6d3e023ca946dde59bf0a1ceafb20 Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Mon, 16 Nov 2020 10:53:25 +0200 Subject: [PATCH 6/8] Update signature of _tp_cache function --- Lib/typing.py | 6 +++--- .../next/Library/2020-11-15-15-23-34.bpo-42345.hiIR7x.rst | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 30adf59de0dcde..fb278de476cf7b 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -242,12 +242,12 @@ def _flatten_literal_params(parameters): _cleanups = [] -def _tp_cache(typed=False): +def _tp_cache(func=None, /, *, typed=False): """Internal wrapper caching __getitem__ of generic types with a fallback to original function for non-hashable arguments. """ - if callable(typed): - return _tp_cache()(typed) + if func is not None: + return _tp_cache(typed=typed)(func) def decorator(func): cached = functools.lru_cache(typed=typed)(func) diff --git a/Misc/NEWS.d/next/Library/2020-11-15-15-23-34.bpo-42345.hiIR7x.rst b/Misc/NEWS.d/next/Library/2020-11-15-15-23-34.bpo-42345.hiIR7x.rst index 423b5a35abd52f..41eabbf1649822 100644 --- a/Misc/NEWS.d/next/Library/2020-11-15-15-23-34.bpo-42345.hiIR7x.rst +++ b/Misc/NEWS.d/next/Library/2020-11-15-15-23-34.bpo-42345.hiIR7x.rst @@ -1,2 +1,3 @@ Fix ``typing.Literal`` equals method to ignore the order of arguments. -Patch provided by Yurii Karabas. +Fix issue related to ``typing.Literal`` caching by adding ``typed`` +parameter to ``typing._tp_cache`` function. Patch provided by Yurii Karabas. From 942dece7b6503bfda8fbdf4d4db04ce5c095a796 Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Mon, 16 Nov 2020 11:40:15 +0200 Subject: [PATCH 7/8] Add Literal arguments deduplication --- Lib/test/test_typing.py | 1 + Lib/typing.py | 33 ++++++++++++------- .../2020-11-15-15-23-34.bpo-42345.hiIR7x.rst | 3 +- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index c5ae0139775b1e..fdf25ba16421f5 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -528,6 +528,7 @@ def test_repr(self): self.assertEqual(repr(Literal[int]), "typing.Literal[int]") self.assertEqual(repr(Literal), "typing.Literal") self.assertEqual(repr(Literal[None]), "typing.Literal[None]") + self.assertEqual(repr(Literal[1, 2, 3, 3]), "typing.Literal[1, 2, 3]") def test_cannot_init(self): with self.assertRaises(TypeError): diff --git a/Lib/typing.py b/Lib/typing.py index fb278de476cf7b..a432502cd45405 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -202,6 +202,20 @@ def _check_generic(cls, parameters, elen): f" actual {alen}, expected {elen}") +def _deduplicate(params): + # Weed out strict duplicates, preserving the first of each occurrence. + all_params = set(params) + if len(all_params) < len(params): + new_params = [] + for t in params: + if t in all_params: + new_params.append(t) + all_params.remove(t) + params = new_params + assert not all_params, all_params + return params + + def _remove_dups_flatten(parameters): """An internal helper for Union creation and substitution: flatten Unions among parameters, then remove duplicates. @@ -215,17 +229,8 @@ def _remove_dups_flatten(parameters): params.extend(p[1:]) else: params.append(p) - # Weed out strict duplicates, preserving the first of each occurrence. - all_params = set(params) - if len(all_params) < len(params): - new_params = [] - for t in params: - if t in all_params: - new_params.append(t) - all_params.remove(t) - params = new_params - assert not all_params, all_params - return tuple(params) + + return tuple(_deduplicate(params)) def _flatten_literal_params(parameters): @@ -487,6 +492,12 @@ def open_helper(file: str, mode: MODE) -> str: parameters = (parameters,) parameters = _flatten_literal_params(parameters) + + try: + parameters = tuple(p for p, _ in _deduplicate(list(_value_and_type_iter(parameters)))) + except TypeError: # unhashable parameters + pass + return _LiteralGenericAlias(self, parameters) diff --git a/Misc/NEWS.d/next/Library/2020-11-15-15-23-34.bpo-42345.hiIR7x.rst b/Misc/NEWS.d/next/Library/2020-11-15-15-23-34.bpo-42345.hiIR7x.rst index 41eabbf1649822..4ad635ab8b87c8 100644 --- a/Misc/NEWS.d/next/Library/2020-11-15-15-23-34.bpo-42345.hiIR7x.rst +++ b/Misc/NEWS.d/next/Library/2020-11-15-15-23-34.bpo-42345.hiIR7x.rst @@ -1,3 +1,4 @@ Fix ``typing.Literal`` equals method to ignore the order of arguments. Fix issue related to ``typing.Literal`` caching by adding ``typed`` -parameter to ``typing._tp_cache`` function. Patch provided by Yurii Karabas. +parameter to ``typing._tp_cache`` function. Add deduplication of +``typing.Literal`` arguments. Patch provided by Yurii Karabas. From f653d680e2fbf94e254f32a8a7e24cb0f95369a9 Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Tue, 17 Nov 2020 02:02:30 +0200 Subject: [PATCH 8/8] Update News. Add tests for Literal.__args__ --- Lib/test/test_typing.py | 16 +++++++++++++--- Lib/typing.py | 6 +++--- .../2020-11-15-15-23-34.bpo-42345.hiIR7x.rst | 6 ++---- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index fdf25ba16421f5..7deba0d71b7c4f 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -569,10 +569,20 @@ def test_equal(self): self.assertEqual(Literal[1, 2], Literal[2, 1]) self.assertEqual(Literal[1, 2, 3], Literal[1, 2, 3, 3]) + def test_args(self): + self.assertEqual(Literal[1, 2, 3].__args__, (1, 2, 3)) + self.assertEqual(Literal[1, 2, 3, 3].__args__, (1, 2, 3)) + self.assertEqual(Literal[1, Literal[2], Literal[3, 4]].__args__, (1, 2, 3, 4)) + # Mutable arguments will not be deduplicated + self.assertEqual(Literal[[], []].__args__, ([], [])) + def test_flatten(self): - self.assertEqual(Literal[Literal[1], Literal[2], Literal[3]], Literal[1, 2, 3]) - self.assertEqual(Literal[Literal[1, 2], 3], Literal[1, 2, 3]) - self.assertEqual(Literal[Literal[1, 2, 3]], Literal[1, 2, 3]) + l1 = Literal[Literal[1], Literal[2], Literal[3]] + l2 = Literal[Literal[1, 2], 3] + l3 = Literal[Literal[1, 2, 3]] + for l in l1, l2, l3: + self.assertEqual(l, Literal[1, 2, 3]) + self.assertEqual(l.__args__, (1, 2, 3)) XK = TypeVar('XK', str, bytes) diff --git a/Lib/typing.py b/Lib/typing.py index a432502cd45405..d310b3dd5820dc 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -251,9 +251,6 @@ def _tp_cache(func=None, /, *, typed=False): """Internal wrapper caching __getitem__ of generic types with a fallback to original function for non-hashable arguments. """ - if func is not None: - return _tp_cache(typed=typed)(func) - def decorator(func): cached = functools.lru_cache(typed=typed)(func) _cleanups.append(cached.cache_clear) @@ -267,6 +264,9 @@ def inner(*args, **kwds): return func(*args, **kwds) return inner + if func is not None: + return decorator(func) + return decorator def _eval_type(t, globalns, localns, recursive_guard=frozenset()): diff --git a/Misc/NEWS.d/next/Library/2020-11-15-15-23-34.bpo-42345.hiIR7x.rst b/Misc/NEWS.d/next/Library/2020-11-15-15-23-34.bpo-42345.hiIR7x.rst index 4ad635ab8b87c8..6339182c3ae727 100644 --- a/Misc/NEWS.d/next/Library/2020-11-15-15-23-34.bpo-42345.hiIR7x.rst +++ b/Misc/NEWS.d/next/Library/2020-11-15-15-23-34.bpo-42345.hiIR7x.rst @@ -1,4 +1,2 @@ -Fix ``typing.Literal`` equals method to ignore the order of arguments. -Fix issue related to ``typing.Literal`` caching by adding ``typed`` -parameter to ``typing._tp_cache`` function. Add deduplication of -``typing.Literal`` arguments. Patch provided by Yurii Karabas. +Fix various issues with ``typing.Literal`` parameter handling (flatten, +deduplicate, use type to cache key). Patch provided by Yurii Karabas.