From c13aa098608c3bd813913b1b9244b429f10fdfc4 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Mon, 20 Nov 2023 16:16:15 +0300 Subject: [PATCH 01/11] gh-112281: Allow `Union` with unhashable `Annotated` metadata --- Lib/test/test_typing.py | 60 +++++++++++++++++++ Lib/typing.py | 48 +++++++++++---- ...-11-20-16-15-44.gh-issue-112281.gH4EVk.rst | 2 + 3 files changed, 99 insertions(+), 11 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-11-20-16-15-44.gh-issue-112281.gH4EVk.rst diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 2b5f34b4b92e0c..832a9f125321c4 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -8267,6 +8267,61 @@ def test_flatten(self): self.assertEqual(A.__metadata__, (4, 5)) self.assertEqual(A.__origin__, int) + def test_deduplicate(self): + # Regular: + self.assertEqual(get_args(Annotated[int, 1] | int), + (Annotated[int, 1], int)) + self.assertEqual(get_args(Union[Annotated[int, 1], int]), + (Annotated[int, 1], int)) + self.assertEqual(get_args(Annotated[int, 1] | Annotated[int, 2] | int), + (Annotated[int, 1], Annotated[int, 2], int)) + self.assertEqual(get_args(Union[Annotated[int, 1], Annotated[int, 2], int]), + (Annotated[int, 1], Annotated[int, 2], int)) + self.assertEqual(get_args(Annotated[int, 1] | Annotated[str, 1] | int), + (Annotated[int, 1], Annotated[str, 1], int)) + self.assertEqual(get_args(Union[Annotated[int, 1], Annotated[str, 1], int]), + (Annotated[int, 1], Annotated[str, 1], int)) + + # Duplicates: + self.assertEqual(Annotated[int, 1] | Annotated[int, 1] | int, + Annotated[int, 1] | int) + self.assertEqual(Union[Annotated[int, 1], Annotated[int, 1], int], + Union[Annotated[int, 1], int]) + + # Unhashable metdata: + self.assertEqual(get_args(str | Annotated[int, {}] | Annotated[int, set()] | int), + (str, int, Annotated[int, {}], Annotated[int, set()])) + self.assertEqual(get_args(Union[str, Annotated[int, {}], Annotated[int, set()], int]), + (str, int, Annotated[int, {}], Annotated[int, set()])) + self.assertEqual(get_args(str | Annotated[int, {}] | Annotated[str, {}] | int), + (str, int, Annotated[int, {}], Annotated[str, {}])) + self.assertEqual(get_args(Union[str, Annotated[int, {}], Annotated[str, {}], int]), + (str, int, Annotated[int, {}], Annotated[str, {}])) + + self.assertEqual(get_args(Annotated[int, 1] | str | Annotated[str, {}] | int), + (str, int, Annotated[int, 1], Annotated[str, {}])) + self.assertEqual(get_args(Union[Annotated[int, 1], str, Annotated[str, {}], int]), + (str, int, Annotated[int, 1], Annotated[str, {}])) + + import dataclasses + @dataclasses.dataclass + class ValueRange: + lo: int + hi: int + v = ValueRange(1, 2) + self.assertEqual(get_args(Annotated[int, v] | None), + (type(None), Annotated[int, v])) + self.assertEqual(get_args(Union[Annotated[int, v], None]), + (type(None), Annotated[int, v])) + self.assertEqual(get_args(Optional[Annotated[int, v]]), + (type(None), Annotated[int, v])) + + # Unhashable metadata duplicated: + self.assertEqual(Annotated[int, {}] | Annotated[int, {}] | int, + Annotated[int, {}] | int) + self.assertEqual(Union[Annotated[int, {}], Annotated[int, {}], int], + Union[Annotated[int, {}], int]) + def test_specialize(self): L = Annotated[List[T], "my decoration"] LI = Annotated[List[int], "my decoration"] @@ -8287,6 +8342,11 @@ def test_hash_eq(self): {Annotated[int, 4, 5], Annotated[int, 4, 5], Annotated[T, 4, 5]}, {Annotated[int, 4, 5], Annotated[T, 4, 5]} ) + # Unhashable `metadata` raises `TypeError`: + self.assertRaises(TypeError, hash, Annotated[int, []]) + class A: + __hash__ = None + self.assertRaises(TypeError, hash, Annotated[int, A()]) def test_instantiate(self): class C: diff --git a/Lib/typing.py b/Lib/typing.py index a96c7083eb785e..ba27ac1e55b798 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -310,16 +310,39 @@ def _unpack_args(args): 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 + try: + all_params = set(params) + except TypeError: + pass # might happen when some parts of type is unhashable + else: + if len(all_params) == len(params): # fast case + return params + + regular_params = [] + annotated_params = [] + for param in params: + if isinstance(param, _AnnotatedAlias): + annotated_params.append(param) + else: + regular_params.append(param) + + unique_params = set(regular_params) + new_params = [] + for t in regular_params: + if t in unique_params: + new_params.append(t) + unique_params.remove(t) + assert not unique_params, unique_params + if not annotated_params: + return new_params + + new_annotated = [] + for t in annotated_params: + # This is slow, but this is the only safe way to compare + # two `Annotated[]` instances, since `metadata` might be unhashable. + if t not in new_annotated: + new_annotated.append(t) + return new_params + new_annotated def _remove_dups_flatten(parameters): @@ -1533,7 +1556,10 @@ def copy_with(self, params): def __eq__(self, other): if not isinstance(other, (_UnionGenericAlias, types.UnionType)): return NotImplemented - return set(self.__args__) == set(other.__args__) + try: # fast path + return set(self.__args__) == set(other.__args__) + except TypeError: # not hashable, slow path + return _deduplicate(self.__args__) == _deduplicate(other.__args__) def __hash__(self): return hash(frozenset(self.__args__)) diff --git a/Misc/NEWS.d/next/Library/2023-11-20-16-15-44.gh-issue-112281.gH4EVk.rst b/Misc/NEWS.d/next/Library/2023-11-20-16-15-44.gh-issue-112281.gH4EVk.rst new file mode 100644 index 00000000000000..15fd8f30dd151e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-11-20-16-15-44.gh-issue-112281.gH4EVk.rst @@ -0,0 +1,2 @@ +Allow creating :ref:`union of types` for +:class:`typing.Annotated` with unhashable / mutable metadata. From 0ff7f847f6f29c5041f9677e1981b0bacb76fb6c Mon Sep 17 00:00:00 2001 From: sobolevn Date: Mon, 20 Nov 2023 16:29:32 +0300 Subject: [PATCH 02/11] Add one more test case --- Lib/test/test_typing.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 832a9f125321c4..2cb98f879543be 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -8319,8 +8319,12 @@ class ValueRange: # Unhashable metadata duplicated: self.assertEqual(Annotated[int, {}] | Annotated[int, {}] | int, Annotated[int, {}] | int) + self.assertEqual(Annotated[int, {}] | Annotated[int, {}] | int, + int | Annotated[int, {}]) self.assertEqual(Union[Annotated[int, {}], Annotated[int, {}], int], Union[Annotated[int, {}], int]) + self.assertEqual(Union[Annotated[int, {}], Annotated[int, {}], int], + Union[int, Annotated[int, {}]]) def test_specialize(self): L = Annotated[List[T], "my decoration"] From d348b746e0a099a4fce9c1f3a149273fe06081ea Mon Sep 17 00:00:00 2001 From: sobolevn Date: Mon, 20 Nov 2023 17:42:50 +0300 Subject: [PATCH 03/11] Fix ordering issues, use consistent hash check --- Lib/test/test_typing.py | 21 ++++++++++--- Lib/typing.py | 70 +++++++++++++++++++++++++++++------------ 2 files changed, 67 insertions(+), 24 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 2cb98f879543be..4910d4eb4ebb30 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -8267,7 +8267,7 @@ def test_flatten(self): self.assertEqual(A.__metadata__, (4, 5)) self.assertEqual(A.__origin__, int) - def test_deduplicate(self): + def test_deduplicate_from_union(self): # Regular: self.assertEqual(get_args(Annotated[int, 1] | int), (Annotated[int, 1], int)) @@ -8288,7 +8288,7 @@ def test_deduplicate(self): self.assertEqual(Union[Annotated[int, 1], Annotated[int, 1], int], Union[Annotated[int, 1], int]) - # Unhashable metdata: + # Unhashable metadata: self.assertEqual(get_args(str | Annotated[int, {}] | Annotated[int, set()] | int), (str, int, Annotated[int, {}], Annotated[int, set()])) self.assertEqual(get_args(Union[str, Annotated[int, {}], Annotated[int, set()], int]), @@ -8299,9 +8299,9 @@ def test_deduplicate(self): (str, int, Annotated[int, {}], Annotated[str, {}])) self.assertEqual(get_args(Annotated[int, 1] | str | Annotated[str, {}] | int), - (str, int, Annotated[int, 1], Annotated[str, {}])) + (Annotated[int, 1], str, int, Annotated[str, {}])) self.assertEqual(get_args(Union[Annotated[int, 1], str, Annotated[str, {}], int]), - (str, int, Annotated[int, 1], Annotated[str, {}])) + (Annotated[int, 1], str, int, Annotated[str, {}])) import dataclasses @dataclasses.dataclass @@ -8326,6 +8326,19 @@ class ValueRange: self.assertEqual(Union[Annotated[int, {}], Annotated[int, {}], int], Union[int, Annotated[int, {}]]) + def test_order_in_union(self): + import operator, functools + + expr1 = Annotated[int, 1] | str | Annotated[str, {}] | int + for args in itertools.permutations(get_args(expr1)): + with self.subTest(args=args): + self.assertEqual(expr1, functools.reduce(operator.or_, args)) + + expr2 = Union[Annotated[int, 1], str, Annotated[str, {}], int] + for args in itertools.permutations(get_args(expr2)): + with self.subTest(args=args): + self.assertEqual(expr2, Union[args]) + def test_specialize(self): L = Annotated[List[T], "my decoration"] LI = Annotated[List[int], "my decoration"] diff --git a/Lib/typing.py b/Lib/typing.py index ba27ac1e55b798..4e773d89e85d16 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -308,42 +308,67 @@ def _unpack_args(args): newargs.append(arg) return newargs -def _deduplicate(params): +def _deduplicate(params, *, unhashable_fallback=True): # Weed out strict duplicates, preserving the first of each occurrence. try: all_params = set(params) except TypeError: - pass # might happen when some parts of type is unhashable - else: - if len(all_params) == len(params): # fast case - return params + if not unhashable_fallback: + raise + + # Happens for cases like `Annotated[dict, {'x': IntValidator()}]` + hasable, unhashable = _deduplicate_unhashable(params) + return hasable + unhashable + if len(all_params) == len(params): # fast case + return params + new_params = [] + for t in params: + if t in all_params: + new_params.append(t) + all_params.remove(t) + assert not all_params, all_params + return new_params + +def _deduplicate_unhashable(params): regular_params = [] - annotated_params = [] + unhashable_params = [] for param in params: - if isinstance(param, _AnnotatedAlias): - annotated_params.append(param) + try: + hash(param) + except TypeError: + unhashable_params.append(param) else: regular_params.append(param) unique_params = set(regular_params) - new_params = [] + new_hashable = [] for t in regular_params: if t in unique_params: - new_params.append(t) + new_hashable.append(t) unique_params.remove(t) assert not unique_params, unique_params - if not annotated_params: - return new_params - new_annotated = [] - for t in annotated_params: + new_unhashable = [] + for t in unhashable_params: # This is slow, but this is the only safe way to compare # two `Annotated[]` instances, since `metadata` might be unhashable. - if t not in new_annotated: - new_annotated.append(t) - return new_params + new_annotated - + if t not in new_unhashable: + new_unhashable.append(t) + return new_hashable, new_unhashable + +def _compare_args_orderless(first_args, second_args): + first_hashable, first_unhashable = _deduplicate_unhashable(first_args) + second_hashable, second_unhashable = _deduplicate_unhashable(second_args) + if set(first_hashable) != set(second_hashable): + return False + 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. @@ -774,7 +799,12 @@ def open_helper(file: str, mode: MODE) -> str: parameters = _flatten_literal_params(parameters) try: - parameters = tuple(p for p, _ in _deduplicate(list(_value_and_type_iter(parameters)))) + parameters = tuple( + p for p, _ in _deduplicate( + list(_value_and_type_iter(parameters)), + unhashable_fallback=False, + ) + ) except TypeError: # unhashable parameters pass @@ -1559,7 +1589,7 @@ def __eq__(self, other): try: # fast path return set(self.__args__) == set(other.__args__) except TypeError: # not hashable, slow path - return _deduplicate(self.__args__) == _deduplicate(other.__args__) + return _compare_args_orderless(self.__args__, other.__args__) def __hash__(self): return hash(frozenset(self.__args__)) From 8dff1c4f1d9b69bf3aaaf528f0c2c8285c353acc Mon Sep 17 00:00:00 2001 From: sobolevn Date: Mon, 20 Nov 2023 17:48:12 +0300 Subject: [PATCH 04/11] More tests --- Lib/test/test_types.py | 12 ++++++++++++ Lib/test/test_typing.py | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index da32c4ea6477ce..2ae3648abbfbbc 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -711,6 +711,18 @@ def test_hash(self): self.assertEqual(hash(int | str), hash(str | int)) self.assertEqual(hash(int | str), hash(typing.Union[int, str])) + def test_union_of_unhashable(self): + class UnhashableMeta(type): + __hash__ = None + + class A(metaclass=UnhashableMeta): ... + class B(metaclass=UnhashableMeta): ... + + self.assertEqual((A | B).__args__, (A, B)) + self.assertRaises(TypeError, hash, A | B) + self.assertRaises(TypeError, hash, int | B) + self.assertRaises(TypeError, hash, A | int) + def test_instancecheck_and_subclasscheck(self): for x in (int | str, typing.Union[int, str]): with self.subTest(x=x): diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 4910d4eb4ebb30..2960e2020d3006 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -1769,6 +1769,18 @@ def test_union_union(self): v = Union[u, Employee] self.assertEqual(v, Union[int, float, Employee]) + def test_union_of_unhashable(self): + class UnhashableMeta(type): + __hash__ = None + + class A(metaclass=UnhashableMeta): ... + class B(metaclass=UnhashableMeta): ... + + self.assertEqual(Union[A, B].__args__, (A, B)) + self.assertRaises(TypeError, hash, Union[A, B]) + self.assertRaises(TypeError, hash, Union[int, B]) + self.assertRaises(TypeError, hash, Union[A, int]) + def test_repr(self): self.assertEqual(repr(Union), 'typing.Union') u = Union[Employee, int] From 968dd185c938289da9ccf9c105362234ef87f2ac Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 2 Dec 2023 13:28:07 +0300 Subject: [PATCH 05/11] Address review --- Lib/test/test_typing.py | 29 ++++++++-------- Lib/typing.py | 33 +++---------------- ...-11-20-16-15-44.gh-issue-112281.gH4EVk.rst | 2 +- 3 files changed, 19 insertions(+), 45 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 2960e2020d3006..804a3144f90dcf 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2,10 +2,11 @@ import collections import collections.abc from collections import defaultdict -from functools import lru_cache, wraps +from functools import lru_cache, wraps, reduce import gc import inspect import itertools +import operator import pickle import re import sys @@ -5376,10 +5377,8 @@ def some(self): self.assertFalse(hasattr(WithOverride.some, "__override__")) def test_multiple_decorators(self): - import functools - def with_wraps(f): # similar to `lru_cache` definition - @functools.wraps(f) + @wraps(f) def wrapper(*args, **kwargs): return f(*args, **kwargs) return wrapper @@ -8302,18 +8301,18 @@ def test_deduplicate_from_union(self): # Unhashable metadata: self.assertEqual(get_args(str | Annotated[int, {}] | Annotated[int, set()] | int), - (str, int, Annotated[int, {}], Annotated[int, set()])) + (str, Annotated[int, {}], Annotated[int, set()], int)) self.assertEqual(get_args(Union[str, Annotated[int, {}], Annotated[int, set()], int]), - (str, int, Annotated[int, {}], Annotated[int, set()])) + (str, Annotated[int, {}], Annotated[int, set()], int)) self.assertEqual(get_args(str | Annotated[int, {}] | Annotated[str, {}] | int), - (str, int, Annotated[int, {}], Annotated[str, {}])) + (str, Annotated[int, {}], Annotated[str, {}], int)) self.assertEqual(get_args(Union[str, Annotated[int, {}], Annotated[str, {}], int]), - (str, int, Annotated[int, {}], Annotated[str, {}])) + (str, Annotated[int, {}], Annotated[str, {}], int)) self.assertEqual(get_args(Annotated[int, 1] | str | Annotated[str, {}] | int), - (Annotated[int, 1], str, int, Annotated[str, {}])) + (Annotated[int, 1], str, Annotated[str, {}], int)) self.assertEqual(get_args(Union[Annotated[int, 1], str, Annotated[str, {}], int]), - (Annotated[int, 1], str, int, Annotated[str, {}])) + (Annotated[int, 1], str, Annotated[str, {}], int)) import dataclasses @dataclasses.dataclass @@ -8322,11 +8321,11 @@ class ValueRange: hi: int v = ValueRange(1, 2) self.assertEqual(get_args(Annotated[int, v] | None), - (type(None), Annotated[int, v])) + (Annotated[int, v], type(None))) self.assertEqual(get_args(Union[Annotated[int, v], None]), - (type(None), Annotated[int, v])) + (Annotated[int, v], type(None))) self.assertEqual(get_args(Optional[Annotated[int, v]]), - (type(None), Annotated[int, v])) + (Annotated[int, v], type(None))) # Unhashable metadata duplicated: self.assertEqual(Annotated[int, {}] | Annotated[int, {}] | int, @@ -8339,12 +8338,10 @@ class ValueRange: Union[int, Annotated[int, {}]]) def test_order_in_union(self): - import operator, functools - expr1 = Annotated[int, 1] | str | Annotated[str, {}] | int for args in itertools.permutations(get_args(expr1)): with self.subTest(args=args): - self.assertEqual(expr1, functools.reduce(operator.or_, args)) + self.assertEqual(expr1, reduce(operator.or_, args)) expr2 = Union[Annotated[int, 1], str, Annotated[str, {}], int] for args in itertools.permutations(get_args(expr2)): diff --git a/Lib/typing.py b/Lib/typing.py index 4e773d89e85d16..0203f2c76d5dd7 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -317,8 +317,7 @@ def _deduplicate(params, *, unhashable_fallback=True): raise # Happens for cases like `Annotated[dict, {'x': IntValidator()}]` - hasable, unhashable = _deduplicate_unhashable(params) - return hasable + unhashable + return _deduplicate_unhashable(params) if len(all_params) == len(params): # fast case return params @@ -330,38 +329,16 @@ def _deduplicate(params, *, unhashable_fallback=True): assert not all_params, all_params return new_params -def _deduplicate_unhashable(params): - regular_params = [] - unhashable_params = [] - for param in params: - try: - hash(param) - except TypeError: - unhashable_params.append(param) - else: - regular_params.append(param) - - unique_params = set(regular_params) - new_hashable = [] - for t in regular_params: - if t in unique_params: - new_hashable.append(t) - unique_params.remove(t) - assert not unique_params, unique_params - +def _deduplicate_unhashable(unhashable_params): new_unhashable = [] for t in unhashable_params: - # This is slow, but this is the only safe way to compare - # two `Annotated[]` instances, since `metadata` might be unhashable. if t not in new_unhashable: new_unhashable.append(t) - return new_hashable, new_unhashable + return new_unhashable def _compare_args_orderless(first_args, second_args): - first_hashable, first_unhashable = _deduplicate_unhashable(first_args) - second_hashable, second_unhashable = _deduplicate_unhashable(second_args) - if set(first_hashable) != set(second_hashable): - return False + first_unhashable = _deduplicate_unhashable(first_args) + second_unhashable = _deduplicate_unhashable(second_args) t = list(second_unhashable) try: for elem in first_unhashable: diff --git a/Misc/NEWS.d/next/Library/2023-11-20-16-15-44.gh-issue-112281.gH4EVk.rst b/Misc/NEWS.d/next/Library/2023-11-20-16-15-44.gh-issue-112281.gH4EVk.rst index 15fd8f30dd151e..01f6689bb471cd 100644 --- a/Misc/NEWS.d/next/Library/2023-11-20-16-15-44.gh-issue-112281.gH4EVk.rst +++ b/Misc/NEWS.d/next/Library/2023-11-20-16-15-44.gh-issue-112281.gH4EVk.rst @@ -1,2 +1,2 @@ Allow creating :ref:`union of types` for -:class:`typing.Annotated` with unhashable / mutable metadata. +:class:`typing.Annotated` with unhashable metadata. From 9f8b665cf29eae5b228781ced1b61e6b3b7d1ee1 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 2 Dec 2023 17:10:08 +0300 Subject: [PATCH 06/11] Address review --- Lib/typing.py | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 0203f2c76d5dd7..c4aa49f00a645b 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -308,26 +308,16 @@ def _unpack_args(args): newargs.append(arg) return newargs -def _deduplicate(params, *, unhashable_fallback=True): +def _deduplicate(params, *, unhashable_fallback=False): # Weed out strict duplicates, preserving the first of each occurrence. try: - all_params = set(params) + deduped = dict.fromkeys(params) except TypeError: if not unhashable_fallback: raise - # Happens for cases like `Annotated[dict, {'x': IntValidator()}]` return _deduplicate_unhashable(params) - - if len(all_params) == len(params): # fast case - return params - new_params = [] - for t in params: - if t in all_params: - new_params.append(t) - all_params.remove(t) - assert not all_params, all_params - return new_params + return deduped def _deduplicate_unhashable(unhashable_params): new_unhashable = [] @@ -360,7 +350,7 @@ def _remove_dups_flatten(parameters): else: params.append(p) - return tuple(_deduplicate(params)) + return tuple(_deduplicate(params, unhashable_fallback=True)) def _flatten_literal_params(parameters): @@ -777,10 +767,7 @@ def open_helper(file: str, mode: MODE) -> str: try: parameters = tuple( - p for p, _ in _deduplicate( - list(_value_and_type_iter(parameters)), - unhashable_fallback=False, - ) + p for p, _ in _deduplicate(list(_value_and_type_iter(parameters))) ) except TypeError: # unhashable parameters pass From a9c1700417f529e570afafd948b134902129dd88 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 2 Dec 2023 17:22:02 +0300 Subject: [PATCH 07/11] Address review --- Lib/typing.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index c4aa49f00a645b..42e4fd2f19e33b 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -766,9 +766,7 @@ def open_helper(file: str, mode: MODE) -> str: parameters = _flatten_literal_params(parameters) try: - parameters = tuple( - p for p, _ in _deduplicate(list(_value_and_type_iter(parameters))) - ) + parameters = tuple(p for p, _ in _deduplicate(list(_value_and_type_iter(parameters)))) except TypeError: # unhashable parameters pass From 4622bc8e7c01b85923f87fe9f9e0b0a3eb9c3c67 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 2 Dec 2023 17:26:42 +0300 Subject: [PATCH 08/11] Address review --- Lib/typing.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 42e4fd2f19e33b..e5b702acc436c7 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -311,13 +311,12 @@ def _unpack_args(args): def _deduplicate(params, *, unhashable_fallback=False): # Weed out strict duplicates, preserving the first of each occurrence. try: - deduped = dict.fromkeys(params) + return dict.fromkeys(params) except TypeError: if not unhashable_fallback: raise # Happens for cases like `Annotated[dict, {'x': IntValidator()}]` return _deduplicate_unhashable(params) - return deduped def _deduplicate_unhashable(unhashable_params): new_unhashable = [] From cadb2dfff898dcf2f84530bb64c14d404b3d364e Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Sat, 2 Dec 2023 17:47:05 +0300 Subject: [PATCH 09/11] Apply suggestions from code review Co-authored-by: Alex Waygood --- Lib/test/test_types.py | 14 +++++++++++--- Lib/test/test_typing.py | 7 +++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index 2ae3648abbfbbc..1ebe4589305755 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -719,9 +719,17 @@ class A(metaclass=UnhashableMeta): ... class B(metaclass=UnhashableMeta): ... self.assertEqual((A | B).__args__, (A, B)) - self.assertRaises(TypeError, hash, A | B) - self.assertRaises(TypeError, hash, int | B) - self.assertRaises(TypeError, hash, A | int) + union1 = A | B + with self.assertRaises(TypeError): + hash(union1) + + union2 = int | B + with self.assertRaises(TypeError): + hash(union2) + + union3 = A | int + with self.assertRaises(TypeError): + hash(union3) def test_instancecheck_and_subclasscheck(self): for x in (int | str, typing.Union[int, str]): diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 804a3144f90dcf..d92d94ce84ff8e 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -8369,10 +8369,13 @@ def test_hash_eq(self): {Annotated[int, 4, 5], Annotated[T, 4, 5]} ) # Unhashable `metadata` raises `TypeError`: - self.assertRaises(TypeError, hash, Annotated[int, []]) + with self.assertRaises(TypeError): + hash(Annotated[int, []]) + class A: __hash__ = None - self.assertRaises(TypeError, hash, Annotated[int, A()]) + with self.assertRaises(TypeError): + hash(Annotated[int, A()])) def test_instantiate(self): class C: From 7293ad770b4c5bf110cebfe59d6cc1db0865037d Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Sat, 2 Dec 2023 17:51:41 +0300 Subject: [PATCH 10/11] Apply suggestions from code review Co-authored-by: Alex Waygood --- Lib/test/test_typing.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index d92d94ce84ff8e..4d735a74c019bb 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -1778,9 +1778,17 @@ class A(metaclass=UnhashableMeta): ... class B(metaclass=UnhashableMeta): ... self.assertEqual(Union[A, B].__args__, (A, B)) - self.assertRaises(TypeError, hash, Union[A, B]) - self.assertRaises(TypeError, hash, Union[int, B]) - self.assertRaises(TypeError, hash, Union[A, int]) + union1 = Union[A, B] + with self.assertRaises(TypeError): + hash(union1) + + union2 = Union[int, B] + with self.assertRaises(TypeError): + hash(union2) + + union3 = Union[A, int] + with self.assertRaises(TypeError): + hash(union3) def test_repr(self): self.assertEqual(repr(Union), 'typing.Union') @@ -8369,13 +8377,15 @@ def test_hash_eq(self): {Annotated[int, 4, 5], Annotated[T, 4, 5]} ) # Unhashable `metadata` raises `TypeError`: + a1 = Annotated[int, []] with self.assertRaises(TypeError): - hash(Annotated[int, []]) + hash(a1) class A: __hash__ = None + a2 = Annotated[int, A()] with self.assertRaises(TypeError): - hash(Annotated[int, A()])) + hash(a2) def test_instantiate(self): class C: From 449f1145e47ad5b0a9deb88ebc76829675c03975 Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Sat, 2 Dec 2023 18:05:56 +0300 Subject: [PATCH 11/11] Update Lib/test/test_typing.py Co-authored-by: Alex Waygood --- Lib/test/test_typing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 4d735a74c019bb..0f602fb6b4cc85 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -8329,11 +8329,11 @@ class ValueRange: hi: int v = ValueRange(1, 2) self.assertEqual(get_args(Annotated[int, v] | None), - (Annotated[int, v], type(None))) + (Annotated[int, v], types.NoneType)) self.assertEqual(get_args(Union[Annotated[int, v], None]), - (Annotated[int, v], type(None))) + (Annotated[int, v], types.NoneType)) self.assertEqual(get_args(Optional[Annotated[int, v]]), - (Annotated[int, v], type(None))) + (Annotated[int, v], types.NoneType)) # Unhashable metadata duplicated: self.assertEqual(Annotated[int, {}] | Annotated[int, {}] | int,