Skip to content

Commit

Permalink
Detect multiple Literal members in a union (#112)
Browse files Browse the repository at this point in the history
Detect multiple `Literal` values in a union

Co-authored-by: Akuli <[email protected]>
  • Loading branch information
AlexWaygood and Akuli authored Jan 20, 2022
1 parent 43fea85 commit d6a3c96
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ unreleased
* introduce Y027 (Python 2-incompatible extension of Y022)
* all errors are now enabled by default
* introduce Y029 (never define ``__repr__`` or ``__str__``)
* introduce Y030 (use ``Literal['foo', 'bar']`` instead of ``Literal['foo'] | Literal['bar']``)

20.10.0
~~~~~~~
Expand Down
3 changes: 3 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ currently emitted:
* Y029: It is almost always redundant to define ``__str__`` or ``__repr__`` in
a stub file, as the signatures are almost always identical to
``object.__str__``and ``object.__repr__``.
* Y030: Union expressions should never have more than one ``Literal`` member,
as ``Literal[1] | Literal[2]`` is semantically identical to
``Literal[1, 2]``.

Many error codes enforce modern conventions, and some cannot yet be used in
all cases:
Expand Down
50 changes: 47 additions & 3 deletions pyi.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,9 +421,52 @@ def _check_union_members(self, members: Sequence[ast.expr]) -> None:
for member in members:
members_by_dump.setdefault(ast.dump(member), []).append(member)

for members in members_by_dump.values():
if len(members) >= 2:
self.error(members[1], Y016.format(unparse(members[1])))
dupes_in_union = False
for member_list in members_by_dump.values():
if len(member_list) >= 2:
self.error(member_list[1], Y016.format(unparse(member_list[1])))
dupes_in_union = True

if not dupes_in_union:
self._check_for_multiple_literals(members)

def _check_for_multiple_literals(self, members: Sequence[ast.expr]) -> None:
literals_in_union, non_literals_in_union = [], []

for member in members:
if (
isinstance(member, ast.Subscript)
and isinstance(member.value, ast.Name)
and member.value.id == "Literal"
):
literals_in_union.append(member.slice)
else:
non_literals_in_union.append(member)

if len(literals_in_union) < 2:
return

new_literal_members: list[ast.expr] = []

for literal in literals_in_union:
if sys.version_info >= (3, 9):
contents = literal
else:
contents = literal.value

if isinstance(contents, ast.Tuple):
new_literal_members.extend(contents.elts)
else:
new_literal_members.append(contents)

new_literal_slice = unparse(ast.Tuple(new_literal_members)).strip("()")

if non_literals_in_union:
suggestion = f'Combine them into one, e.g. "Literal[{new_literal_slice}]".'
else:
suggestion = f'Use a single Literal, e.g. "Literal[{new_literal_slice}]".'

self.error(members[0], Y030.format(suggestion=suggestion))

def visit_BinOp(self, node: ast.BinOp) -> None:
if not isinstance(node.op, ast.BitOr):
Expand Down Expand Up @@ -905,3 +948,4 @@ def parse_options(cls, optmanager, options, extra_args):
Y027 = 'Y027 Use {good_cls_name} instead of "{bad_cls_alias}"'
Y028 = "Y028 Use class-based syntax for NamedTuples"
Y029 = "Y029 Defining __repr__ or __str__ in a stub is almost always redundant"
Y030 = "Y030 Multiple Literal members in a union. {suggestion}"
8 changes: 8 additions & 0 deletions tests/union_duplicates.pyi
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Union
from typing_extensions import Literal, TypeAlias

def f1_pipe(x: int | str) -> None:
...
Expand All @@ -21,3 +22,10 @@ def f4_union(x: Union[int, None, int]) -> None: # Y016 Duplicate union member "
...
def f5_union(x: Union[int, int, None]) -> None: # Y016 Duplicate union member "int"
...


just_literals_subscript_union: Union[Literal[1], Literal[2]] # Y030 Multiple Literal members in a union. Use a single Literal, e.g. "Literal[1, 2]".
mixed_subscript_union: Union[str, Literal['foo'], Literal['bar']] # Y030 Multiple Literal members in a union. Combine them into one, e.g. "Literal['foo', 'bar']".
just_literals_pipe_union: TypeAlias = Literal[True] | Literal['idk'] # Y030 Multiple Literal members in a union. Use a single Literal, e.g. "Literal[True, 'idk']".
mixed_pipe_union: TypeAlias = Union[Literal[966], int, Literal['baz']] # Y030 Multiple Literal members in a union. Combine them into one, e.g. "Literal[966, 'baz']".
many_literal_members_but_needs_combining: TypeAlias = int | Literal['a', 'b'] | Literal['baz'] # Y030 Multiple Literal members in a union. Combine them into one, e.g. "Literal['a', 'b', 'baz']".

0 comments on commit d6a3c96

Please sign in to comment.