Skip to content
64 changes: 64 additions & 0 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -8267,6 +8267,65 @@ 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:
Comment thread
sobolevn marked this conversation as resolved.
Outdated
self.assertEqual(get_args(str | Annotated[int, {}] | Annotated[int, set()] | int),
Comment thread
sobolevn marked this conversation as resolved.
(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(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"]
LI = Annotated[List[int], "my decoration"]
Expand All @@ -8287,6 +8346,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()])
Comment thread
sobolevn marked this conversation as resolved.
Outdated

def test_instantiate(self):
class C:
Expand Down
48 changes: 37 additions & 11 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
sobolevn marked this conversation as resolved.
Outdated
if t not in new_annotated:
new_annotated.append(t)
return new_params + new_annotated


def _remove_dups_flatten(parameters):
Expand Down Expand Up @@ -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__)
Comment thread
AlexWaygood marked this conversation as resolved.
Outdated

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 / mutable metadata.
Comment thread
sobolevn marked this conversation as resolved.
Outdated