GeezerVison is showing github.com — gh-112281: Allow `Union` with unhashable `Annotated` metadata by sobolevn · Pull Request #112283 · python/cpython
gh-112281: Allow Union with unhashable Annotated metadata
#112283
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and
privacy statement . We’ll occasionally send you account related emails.
Already on GitHub?
Sign in
to your account
Uh oh!
There was an error while loading. Please reload this page.
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
c13aa09
gh-112281: Allow `Union` with unhashable `Annotated` metadata
sobolevn Nov 20, 2023
0ff7f84
Add one more test case
sobolevn Nov 20, 2023
d348b74
Fix ordering issues, use consistent hash check
sobolevn Nov 20, 2023
8dff1c4
More tests
sobolevn Nov 20, 2023
968dd18
Address review
sobolevn Dec 2, 2023
9f8b665
Address review
sobolevn Dec 2, 2023
a9c1700
Address review
sobolevn Dec 2, 2023
4622bc8
Address review
sobolevn Dec 2, 2023
cadb2df
Apply suggestions from code review
sobolevn Dec 2, 2023
7293ad7
Apply suggestions from code review
sobolevn Dec 2, 2023
449f114
Update Lib/test/test_typing.py
sobolevn Dec 2, 2023
da80e22
Merge branch 'main' into issue-112281
sobolevn Feb 7, 2024
98cf0ff
Merge branch 'main' into issue-112281
AlexWaygood Mar 1, 2024
File filter
Filter by extension
.py
(3)
.rst
(1)
All 2 file types selected
Viewed files
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Hide whitespace
Diff view
Hide whitespace
There are no files selected for viewing
Original file line number
Diff line number
Diff line change
Expand Up
@@ -713,6 +713,26 @@ 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))
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]):
with self.subTest(x=x):
Expand Down
Original file line number
Diff line number
Diff line change
Expand Up
@@ -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
Expand Down
Expand Up
@@ -1769,6 +1770,26 @@ 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))
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')
u = Union[Employee, int]
Expand Down
Expand Up
@@ -5506,10 +5527,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
Expand Down
Expand Up
@@ -8524,6 +8543,76 @@ def test_flatten(self):
self.assertEqual(A.__metadata__, (4, 5))
self.assertEqual(A.__origin__, int)
def test_deduplicate_from_union(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 metadata:
self.assertEqual(get_args(str | Annotated[int, {}] | Annotated[int, set()] | int),
Comment thread
sobolevn marked this conversation as resolved.
Resolved
Uh oh!
There was an error while loading. Please reload this page.
(str, Annotated[int, {}], Annotated[int, set()], int))
self.assertEqual(get_args(Union[str, Annotated[int, {}], Annotated[int, set()], int]),
(str, Annotated[int, {}], Annotated[int, set()], int))
self.assertEqual(get_args(str | Annotated[int, {}] | Annotated[str, {}] | int),
(str, Annotated[int, {}], Annotated[str, {}], int))
self.assertEqual(get_args(Union[str, Annotated[int, {}], Annotated[str, {}], int]),
(str, Annotated[int, {}], Annotated[str, {}], int))
self.assertEqual(get_args(Annotated[int, 1] | str | Annotated[str, {}] | int),
(Annotated[int, 1], str, Annotated[str, {}], int))
self.assertEqual(get_args(Union[Annotated[int, 1], str, Annotated[str, {}], int]),
(Annotated[int, 1], str, Annotated[str, {}], int))
import dataclasses
@dataclasses.dataclass
class ValueRange:
lo: int
hi: int
v = ValueRange(1, 2)
self.assertEqual(get_args(Annotated[int, v] | None),
(Annotated[int, v], types.NoneType))
self.assertEqual(get_args(Union[Annotated[int, v], None]),
(Annotated[int, v], types.NoneType))
self.assertEqual(get_args(Optional[Annotated[int, v]]),
(Annotated[int, v], types.NoneType))
# 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_order_in_union(self):
expr1 = Annotated[int, 1] | str | Annotated[str, {}] | int
for args in itertools.permutations(get_args(expr1)):
with self.subTest(args=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)):
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"]
Expand All
@@ -8544,6 +8633,16 @@ 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`:
a1 = Annotated[int, []]
with self.assertRaises(TypeError):
hash(a1)
class A:
__hash__ = None
a2 = Annotated[int, A()]
with self.assertRaises(TypeError):
hash(a2)
def test_instantiate(self):
class C:
Expand Down
Original file line number
Diff line number
Diff line change
Expand Up
@@ -308,19 +308,33 @@ def _unpack_args(args):
newargs.append(arg)
return newargs
def _deduplicate(params):
def _deduplicate(params, *, unhashable_fallback=False ):
# 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:
return dict.fromkeys(params)
except TypeError:
if not unhashable_fallback:
raise
# Happens for cases like `Annotated[dict, {'x': IntValidator()}]`
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.
Expand All
@@ -335,7 +349,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):
Expand Down
Expand Up
@@ -1555,7 +1569,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 _compare_args_orderless(self.__args__, other.__args__)
def __hash__(self):
return hash(frozenset(self.__args__))
Expand Down
Original file line number
Diff line number
Diff line change
@@ -0,0 +1,2 @@
Allow creating :ref:`union of types<types-union>` for
:class:`typing.Annotated` with unhashable metadata.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.