8000 Fix various things with `Literal` (#145) · python/typing_extensions@8bff0a3 · GitHub
[go: up one dir, main page]

Skip to content

Commit 8bff0a3

Browse files
authored
Fix various things with Literal (#145)
1 parent 501a00e commit 8bff0a3

File tree

4 files changed

+106
-7
lines changed

4 files changed

+106
-7
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@
33
- Add `typing_extensions.Buffer`, a marker class for buffer types, as proposed
44
by PEP 688. Equivalent to `collections.abc.Buffer` in Python 3.12. Patch by
55
Jelle Zijlstra.
6+
- Backport two CPython PRs fixing various issues with `typing.Literal`:
7+
https://github.com/python/cpython/pull/23294 and
8+
https://github.com/python/cpython/pull/23383. Both CPython PRs were
9+
originally by Yurii Karabas, and both were backported to Python >=3.9.1, but
10+
no earlier. Patch by Alex Waygood.
11+
12+
A side effect of one of the changes is that equality comparisons of `Literal`
13+
objects will now raise a `TypeError` if one of the `Literal` objects being
14+
compared has a mutable parameter. (Using mutable parameters with `Literal` is
15+
not supported by PEP 586 or by any major static type checkers.)
616
- Backport [CPython PR 26067](https://github.com/python/cpython/pull/26067)
717
(originally by Yurii Karabas), ensuring that `isinstance()` calls on
818
protocols raise `TypeError` when the protocol is not decorated with

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ Certain objects were changed after they were added to `typing`, and
143143
- `TypeVar` gains two additional parameters, `default=` and `infer_variance=`,
144144
in the draft PEPs [695](https://peps.python.org/pep-0695/) and [696](https://peps.python.org/pep-0696/), which are being considered for inclusion
145145
in Python 3.12.
146+
- `Literal` does not flatten or deduplicate parameters on Python <3.9.1. The
147+
`typing_extensions` version flattens and deduplicates parameters on all
148+
Python versions.
146149

147150
There are a few types whose interface was modified between different
148151
versions of typing. For example, `typing.Sequence` was modified to

src/test_typing_extensions.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,8 @@ def test_literals_inside_other_types(self):
615615
List[Literal[("foo", "bar", "baz")]]
616616

617617
def test_repr(self):
618-
if hasattr(typing, 'Literal'):
618+
# we backport various bugfixes that were added in 3.9.1
619+
if sys.version_info >= (3, 9, 1):
619620
mod_name = 'typing'
620621
else:
621622
mod_name = 'typing_extensions'
@@ -624,6 +625,7 @@ def test_repr(self):
624625
self.assertEqual(repr(Literal[int]), mod_name + ".Literal[int]")
625626
self.assertEqual(repr(Literal), mod_name + ".Literal")
626627
self.assertEqual(repr(Literal[None]), mod_name + ".Literal[None]")
628+
self.assertEqual(repr(Literal[1, 2, 3, 3]), mod_name + ".Literal[1, 2, 3]")
627629

628630
def test_cannot_init(self):
629631
with self.assertRaises(TypeError):
@@ -655,6 +657,39 @@ def test_no_multiple_subscripts(self):
655657
with self.assertRaises(TypeError):
656658
Literal[1][1]
657659

660+
def test_equal(self):
661+
self.assertNotEqual(Literal[0], Literal[False])
662+
self.assertNotEqual(Literal[True], Literal[1])
663+
self.assertNotEqual(Literal[1], Literal[2])
664+
self.assertNotEqual(Literal[1, True], Literal[1])
665+
self.assertEqual(Literal[1], Literal[1])
666+
self.assertEqual(Literal[1, 2], Literal[2, 1])
667+
self.assertEqual(Literal[1, 2, 3], Literal[1, 2, 3, 3])
668+
669+
def test_hash(self):
670+
self.assertEqual(hash(Literal[1]), hash(Literal[1]))
671+
self.assertEqual(hash(Literal[1, 2]), hash(Literal[2, 1]))
672+
self.assertEqual(hash(Literal[1, 2, 3]), hash(Literal[1, 2, 3, 3]))
673+
674+
def test_args(self):
675+
self.assertEqual(Literal[1, 2, 3].__args__, (1, 2, 3))
676+
self.assertEqual(Literal[1, 2, 3, 3].__args__, (1, 2, 3))
677+
self.assertEqual(Literal[1, Literal[2], Literal[3, 4]].__args__, (1, 2, 3, 4))
678+
# Mutable arguments will not be deduplicated
679+
self.assertEqual(Literal[[], []].__args__, ([], []))
680+
681+
def test_flatten(self):
682+
l1 = Literal[Literal[1], Literal[2], Literal[3]]
683+
l2 = Literal[Literal[1, 2], 3]
684+
l3 = Literal[Literal[1, 2, 3]]
685+
for lit in l1, l2, l3:
686+
self.assertEqual(lit, Literal[1, 2, 3])
687+
self.assertEqual(lit.__args__, (1, 2, 3))
688+
689+
def test_caching_of_Literal_respects_type(self):
690+
self.assertIs(type(Literal[1].__args__[0]), int)
691+
self.assertIs(type(Literal[True].__args__[0]), bool)
692+
658693

659694
class MethodHolder:
660695
@classmethod
@@ -3566,6 +3601,8 @@ def test_typing_extensions_defers_when_possible(self):
35663601
'get_type_hints',
35673602
'is_typeddict',
35683603
}
3604+
if sys.version_info < (3, 9, 1):
3605+
exclude |= {"Literal"}
35693606
if sys.version_info < (3, 10):
35703607
exclude |= {'get_args', 'get_origin'}
35713608
if sys.version_info < (3, 11):

src/typing_extensions.py

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -261,21 +261,70 @@ def IntVar(name):
261261
return typing.TypeVar(name)
262262

263263

264-
# 3.8+:
265-
if hasattr(typing, 'Literal'):
264+
# Various Literal bugs were fixed in 3.9.1, but not backported earlier than that
265+
if sys.version_info >= (3, 9, 1):
266266
Literal = typing.Literal
267-
# 3.7:
268267
else:
268+
def _flatten_literal_params(parameters):
269+
"""An internal helper for Literal creation: flatten Literals among parameters"""
270+
params = []
271+
for p in parameters:
272+
if isinstance(p, _LiteralGenericAlias):
273+
params.extend(p.__args__)
274+
else:
275+
params.append(p)
276+
return tuple(params)
277+
278+
def _value_and_type_iter(params):
279+
for p in params:
280+
yield p, type(p)
281+
282+
class _LiteralGenericAlias(typing._GenericAlias, _root=True):
283+
def __eq__(self, other):
284+
if not isinstance(other, _LiteralGenericAlias):
285+
return NotImplemented
286+
these_args_deduped = set(_value_and_type_iter(self.__args__))
287+
other_args_deduped = set(_value_and_type_iter(other.__args__))
288+
return these_args_deduped == other_args_deduped
289+
290+
def __hash__(self):
291+
return hash(frozenset(_value_and_type_iter(self.__args__)))
292+
269293
class _LiteralForm(typing._SpecialForm, _root=True):
294+
def __init__(self, doc: str):
295+
self._name = 'Literal'
296+
self._doc = self.__doc__ = doc
270297

271298
def __repr__(self):
272299
return 'typing_extensions.' + self._name
273300

274301
def __getitem__(self, parameters):
275-
return typing._GenericAlias(self, parameters)
302+
if not isinstance(parameters, tuple):
303+
parameters = (parameters,)
304+
305+
parameters = _flatten_literal_params(parameters)
276306

277-
Literal = _LiteralForm('Literal',
278-
doc="""A type that can be used to indicate to type checkers
307+
val_type_pairs = list(_value_and_type_iter(parameters))
308+
try:
309+
deduped_pairs = set(val_type_pairs)
310+
except TypeError:
311+
# unhashable parameters
312+
pass
313+
else:
314+
# similar logic to typing._deduplicate on Python 3.9+
315+
if len(deduped_pairs) < len(val_type_pairs):
316+
new_parameters = []
317+
for pair in val_type_pairs:
318+
if pair in deduped_pairs:
319+
new_parameters.append(pair[0])
320+
deduped_pairs.remove(pair)
321+
assert not deduped_pairs, deduped_pairs
322+
parameters = tuple(new_parameters)
323+
324+
return _LiteralGenericAlias(self, parameters)
325+
326+
Literal = _LiteralForm(doc="""\
327+
A type that can be used to indicate to type checkers
279328
that the corresponding value has a value literally equivalent
280329
to the provided parameter. For example:
281330

0 commit comments

Comments
 (0)
0