From 7693865cec727fd82323b9ec95014246308468c5 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sun, 12 Nov 2023 14:20:46 +0100 Subject: [PATCH 01/11] Add Y062: Forbid duplicate elements in `typing.Literal` --- pyi.py | 81 ++++++++++++++++++++++++++++++++++++---------- tests/literals.pyi | 7 ++++ 2 files changed, 71 insertions(+), 17 deletions(-) diff --git a/pyi.py b/pyi.py index 1164bf5d..86c223b7 100644 --- a/pyi.py +++ b/pyi.py @@ -671,6 +671,53 @@ def _analyse_union(members: Sequence[ast.expr]) -> UnionAnalysis: ) +class TypingLiteralAnalysis(NamedTuple): + members_by_dump: defaultdict[str, list[ast.expr]] + members_without_none: list[ast.expr] + none_members: list[ast.expr] + contains_only_none: bool + + +def _analyse_typing_Literal(node: ast.Subscript) -> TypingLiteralAnalysis: + """Return a tuple providing analysis of a `typing.Literal` slice. + + >>> source = 'Literal[True, None, True, None, False]' + >>> literal = _ast_node_for(source) + >>> analysis = _analyse_typing_Literal(literal) + >>> len(analysis.members_by_dump["Constant(value=True)"]) + 2 + >>> analysis.contains_only_none + False + >>> len(analysis.none_members) + 2 + >>> unparse(ast.Tuple(analysis.members_without_none)) + '(True, True, False)' + """ + + members_by_dump: defaultdict[str, list[ast.expr]] = defaultdict(list) + members_without_none: list[ast.expr] = [] + none_members: list[ast.expr] = [] + + if isinstance(node.slice, ast.Tuple): + members = node.slice.elts + else: + members = [node.slice] + + for member in members: + members_by_dump[ast.dump(member)].append(member) + if _is_None(member): + none_members.append(member) + else: + members_without_none.append(member) + + return TypingLiteralAnalysis( + members_by_dump=members_by_dump, + members_without_none=members_without_none, + none_members=none_members, + contains_only_none=bool(none_members and not members_without_none), + ) + + _ALLOWED_MATH_ATTRIBUTES_IN_DEFAULTS = frozenset( {"math.inf", "math.nan", "math.e", "math.pi", "math.tau"} ) @@ -1425,24 +1472,23 @@ def visit_Subscript(self, node: ast.Subscript) -> None: self._Y090_error(node) def _visit_typing_Literal(self, node: ast.Subscript) -> None: - if isinstance(node.slice, ast.Constant) and _is_None(node.slice): - # Special case for `Literal[None]` + analysis = _analyse_typing_Literal(node) + + if analysis.contains_only_none: self.error(node.slice, Y061.format(suggestion="None")) - elif isinstance(node.slice, ast.Tuple): - elts = node.slice.elts - for i, elt in enumerate(elts): - if _is_None(elt): - elts_without_none = elts[:i] + [ - elt for elt in elts[i + 1 :] if not _is_None(elt) - ] - if len(elts_without_none) == 1: - new_literal_slice = unparse(elts_without_none[0]) - else: - new_slice_node = ast.Tuple(elts=elts_without_none) - new_literal_slice = unparse(new_slice_node).strip("()") - suggestion = f"Literal[{new_literal_slice}] | None" - self.error(elt, Y061.format(suggestion=suggestion)) - break # Only report the first `None` + elif analysis.none_members: + if len(analysis.members_without_none) == 1: + new_literal_slice = unparse(analysis.members_without_none[0]) + else: + new_slice_node = ast.Tuple(elts=analysis.members_without_none) + new_literal_slice = unparse(new_slice_node).strip("()") + suggestion = f"Literal[{new_literal_slice}] | None" + self.error(analysis.none_members[0], Y061.format(suggestion=suggestion)) + + for member_list in analysis.members_by_dump.values(): + if len(member_list) > 1 and not _is_None(member_list[0]): + self.error(member_list[1], Y062.format(unparse(member_list[1]))) + self.visit(node.slice) def _visit_slice_tuple(self, node: ast.Tuple, parent: str | None) -> None: @@ -2258,6 +2304,7 @@ def parse_options(options: argparse.Namespace) -> None: "class would be inferred as generic anyway" ) Y061 = 'Y061 None inside "Literal[]" expression. Replace with "{suggestion}"' +Y062 = 'Y062 Duplicate "Literal[]" member "{}"' Y090 = ( 'Y090 "{original}" means ' '"a tuple of length 1, in which the sole element is of type {typ!r}". ' diff --git a/tests/literals.pyi b/tests/literals.pyi index a619881f..dfc3ac65 100644 --- a/tests/literals.pyi +++ b/tests/literals.pyi @@ -1,5 +1,12 @@ from typing import Literal Literal[None] # Y061 None inside "Literal[]" expression. Replace with "None" +Literal[None, None] # Y061 None inside "Literal[]" expression. Replace with "None" Literal[True, None] # Y061 None inside "Literal[]" expression. Replace with "Literal[True] | None" Literal[1, None, "foo", None] # Y061 None inside "Literal[]" expression. Replace with "Literal[1, 'foo'] | None" + +Literal[True, True] # Y062 Duplicate "Literal[]" member "True" +Literal[True, True, True] # Y062 Duplicate "Literal[]" member "True" +Literal[True, False, True, False] # Y062 Duplicate "Literal[]" member "True" # Y062 Duplicate "Literal[]" member "False" + +Literal[None, True, None, True] # Y061 None inside "Literal[]" expression. Replace with "Literal[True, True] | None" # Y062 Duplicate "Literal[]" member "True" From 587a466f7ceda991638c3c610da663c63666e181 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sun, 12 Nov 2023 16:21:29 +0100 Subject: [PATCH 02/11] Add changelog --- CHANGELOG.md | 5 +++++ ERRORCODES.md | 1 + 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aedaa20f..42720b5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## Unreleased + +New error codes: +* Y062: Disallow duplicate elements inside `Literal[]` slices. + ## 23.11.0 New error codes: diff --git a/ERRORCODES.md b/ERRORCODES.md index a9db5b7b..6ce79728 100644 --- a/ERRORCODES.md +++ b/ERRORCODES.md @@ -64,6 +64,7 @@ The following warnings are currently emitted by default: | Y059 | `Generic[]` should always be the last base class, if it is present in a class's bases tuple. At runtime, if `Generic[]` is not the final class in a the bases tuple, this [can cause the class creation to fail](https://github.com/python/cpython/issues/106102). In a stub file, however, this rule is enforced purely for stylistic consistency. | Y060 | Redundant inheritance from `Generic[]`. For example, `class Foo(Iterable[_T], Generic[_T]): ...` can be written more simply as `class Foo(Iterable[_T]): ...`.

To avoid false-positive errors, and to avoid complexity in the implementation, this check is deliberately conservative: it only flags classes where all subscripted bases have identical code inside their subscript slices. | Y061 | Do not use `None` inside a `Literal[]` slice. For example, use `Literal["foo"] \| None` instead of `Literal["foo", None]`. While both are legal according to [PEP 586](https://peps.python.org/pep-0586/), the former is preferred for stylistic consistency. +| Y062 | `Literal[]` slices shouldn't contain duplicates, e.g. `Literal[True, True]` is not allowed. ## Warnings disabled by default From 0e53dc01554e210dc0344334df6f0fd5eb13ff83 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 12 Nov 2023 15:23:13 +0000 Subject: [PATCH 03/11] [pre-commit.ci] auto fixes from pre-commit.com hooks --- pyi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyi.py b/pyi.py index 86c223b7..a39ee1a1 100644 --- a/pyi.py +++ b/pyi.py @@ -1488,7 +1488,7 @@ def _visit_typing_Literal(self, node: ast.Subscript) -> None: for member_list in analysis.members_by_dump.values(): if len(member_list) > 1 and not _is_None(member_list[0]): self.error(member_list[1], Y062.format(unparse(member_list[1]))) - + self.visit(node.slice) def _visit_slice_tuple(self, node: ast.Tuple, parent: str | None) -> None: From 10ea1d1df66d481ceedbb21a8b4f62ac7a37a9e0 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Mon, 13 Nov 2023 22:52:49 +0100 Subject: [PATCH 04/11] Remove broken doctest --- pyi.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/pyi.py b/pyi.py index a39ee1a1..750fa660 100644 --- a/pyi.py +++ b/pyi.py @@ -679,20 +679,7 @@ class TypingLiteralAnalysis(NamedTuple): def _analyse_typing_Literal(node: ast.Subscript) -> TypingLiteralAnalysis: - """Return a tuple providing analysis of a `typing.Literal` slice. - - >>> source = 'Literal[True, None, True, None, False]' - >>> literal = _ast_node_for(source) - >>> analysis = _analyse_typing_Literal(literal) - >>> len(analysis.members_by_dump["Constant(value=True)"]) - 2 - >>> analysis.contains_only_none - False - >>> len(analysis.none_members) - 2 - >>> unparse(ast.Tuple(analysis.members_without_none)) - '(True, True, False)' - """ + """Return a tuple providing analysis of a `typing.Literal` slice.""" members_by_dump: defaultdict[str, list[ast.expr]] = defaultdict(list) members_without_none: list[ast.expr] = [] From 26eb19d0f0511a21b03a990954a43715b623e10f Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Mon, 13 Nov 2023 23:25:43 +0100 Subject: [PATCH 05/11] Do not emit Y061 if Y062 is emitted as well --- pyi.py | 25 ++++++++++++++----------- tests/literals.pyi | 3 ++- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/pyi.py b/pyi.py index 750fa660..758d0ca3 100644 --- a/pyi.py +++ b/pyi.py @@ -1461,21 +1461,24 @@ def visit_Subscript(self, node: ast.Subscript) -> None: def _visit_typing_Literal(self, node: ast.Subscript) -> None: analysis = _analyse_typing_Literal(node) - if analysis.contains_only_none: - self.error(node.slice, Y061.format(suggestion="None")) - elif analysis.none_members: - if len(analysis.members_without_none) == 1: - new_literal_slice = unparse(analysis.members_without_none[0]) - else: - new_slice_node = ast.Tuple(elts=analysis.members_without_none) - new_literal_slice = unparse(new_slice_node).strip("()") - suggestion = f"Literal[{new_literal_slice}] | None" - self.error(analysis.none_members[0], Y061.format(suggestion=suggestion)) - + Y062_encountered = False for member_list in analysis.members_by_dump.values(): if len(member_list) > 1 and not _is_None(member_list[0]): + Y062_encountered = True self.error(member_list[1], Y062.format(unparse(member_list[1]))) + if not Y062_encountered: + if analysis.contains_only_none: + self.error(node.slice, Y061.format(suggestion="None")) + elif analysis.none_members: + if len(analysis.members_without_none) == 1: + new_literal_slice = unparse(analysis.members_without_none[0]) + else: + new_slice_node = ast.Tuple(elts=analysis.members_without_none) + new_literal_slice = unparse(new_slice_node).strip("()") + suggestion = f"Literal[{new_literal_slice}] | None" + self.error(analysis.none_members[0], Y061.format(suggestion=suggestion)) + self.visit(node.slice) def _visit_slice_tuple(self, node: ast.Tuple, parent: str | None) -> None: diff --git a/tests/literals.pyi b/tests/literals.pyi index dfc3ac65..199d3218 100644 --- a/tests/literals.pyi +++ b/tests/literals.pyi @@ -9,4 +9,5 @@ Literal[True, True] # Y062 Duplicate "Literal[]" member "True" Literal[True, True, True] # Y062 Duplicate "Literal[]" member "True" Literal[True, False, True, False] # Y062 Duplicate "Literal[]" member "True" # Y062 Duplicate "Literal[]" member "False" -Literal[None, True, None, True] # Y061 None inside "Literal[]" expression. Replace with "Literal[True, True] | None" # Y062 Duplicate "Literal[]" member "True" +# If both Y061 and Y062 would be emitted, only emit Y062 +Literal[None, True, None, True] # Y062 Duplicate "Literal[]" member "True" From 6fb5f33ed31429ea8bf652b1c4f3d3bd233e45b1 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Mon, 13 Nov 2023 23:26:51 +0100 Subject: [PATCH 06/11] Update error codes docs --- ERRORCODES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ERRORCODES.md b/ERRORCODES.md index 6ce79728..bb0a651b 100644 --- a/ERRORCODES.md +++ b/ERRORCODES.md @@ -63,7 +63,7 @@ The following warnings are currently emitted by default: | Y058 | Use `Iterator` rather than `Generator` as the return value for simple `__iter__` methods, and `AsyncIterator` rather than `AsyncGenerator` as the return value for simple `__aiter__` methods. Using `(Async)Iterator` for these methods is simpler and more elegant, and reflects the fact that the precise kind of iterator returned from an `__iter__` method is usually an implementation detail that could change at any time, and should not be relied upon. | Y059 | `Generic[]` should always be the last base class, if it is present in a class's bases tuple. At runtime, if `Generic[]` is not the final class in a the bases tuple, this [can cause the class creation to fail](https://github.com/python/cpython/issues/106102). In a stub file, however, this rule is enforced purely for stylistic consistency. | Y060 | Redundant inheritance from `Generic[]`. For example, `class Foo(Iterable[_T], Generic[_T]): ...` can be written more simply as `class Foo(Iterable[_T]): ...`.

To avoid false-positive errors, and to avoid complexity in the implementation, this check is deliberately conservative: it only flags classes where all subscripted bases have identical code inside their subscript slices. -| Y061 | Do not use `None` inside a `Literal[]` slice. For example, use `Literal["foo"] \| None` instead of `Literal["foo", None]`. While both are legal according to [PEP 586](https://peps.python.org/pep-0586/), the former is preferred for stylistic consistency. +| Y061 | Do not use `None` inside a `Literal[]` slice. For example, use `Literal["foo"] \| None` instead of `Literal["foo", None]`. While both are legal according to [PEP 586](https://peps.python.org/pep-0586/), the former is preferred for stylistic consistency. Note that this warning is not emitted if Y062 is emitted for the same `Literal[]` slice. For example, `Literal[None, None, True, True]` only emits Y062. | Y062 | `Literal[]` slices shouldn't contain duplicates, e.g. `Literal[True, True]` is not allowed. ## Warnings disabled by default From 0c5185f4134941f7b265d8256332daf4a239f021 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Tue, 14 Nov 2023 00:07:28 +0100 Subject: [PATCH 07/11] Make mypy happy --- pyi.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/pyi.py b/pyi.py index 758d0ca3..8e1a03ac 100644 --- a/pyi.py +++ b/pyi.py @@ -396,8 +396,16 @@ def _is_type_or_Type(node: ast.expr) -> bool: return cls_name in {"type", "Type"} -def _is_None(node: ast.expr) -> bool: - return isinstance(node, ast.Constant) and node.value is None +if sys.version_info >= (3, 9): + def _is_None(node: ast.expr) -> bool: + return isinstance(node, ast.Constant) and node.value is None +else: + def _is_None(node: Union[ast.expr, ast.slice]) -> bool: + if isinstance(node, ast.Constant): + return node.value is None + elif isinstance(node, ast.Index): + return node.value is None + return False class ExitArgAnalysis(NamedTuple): @@ -672,18 +680,19 @@ def _analyse_union(members: Sequence[ast.expr]) -> UnionAnalysis: class TypingLiteralAnalysis(NamedTuple): - members_by_dump: defaultdict[str, list[ast.expr]] - members_without_none: list[ast.expr] - none_members: list[ast.expr] + members_by_dump: defaultdict[str, list[_SliceContents]] + members_without_none: list[_SliceContents] + none_members: list[_SliceContents] contains_only_none: bool def _analyse_typing_Literal(node: ast.Subscript) -> TypingLiteralAnalysis: """Return a tuple providing analysis of a `typing.Literal` slice.""" - members_by_dump: defaultdict[str, list[ast.expr]] = defaultdict(list) - members_without_none: list[ast.expr] = [] - none_members: list[ast.expr] = [] + members: Sequence[_SliceContents] + members_by_dump: defaultdict[str, list[_SliceContents]] = defaultdict(list) + members_without_none: list[_SliceContents] = [] + none_members: list[_SliceContents] = [] if isinstance(node.slice, ast.Tuple): members = node.slice.elts From 5d206c0f28c20f3dc129291cd9b1fdee9118d690 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 23:08:16 +0000 Subject: [PATCH 08/11] [pre-commit.ci] auto fixes from pre-commit.com hooks --- pyi.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyi.py b/pyi.py index 8e1a03ac..a175bede 100644 --- a/pyi.py +++ b/pyi.py @@ -397,9 +397,12 @@ def _is_type_or_Type(node: ast.expr) -> bool: if sys.version_info >= (3, 9): + def _is_None(node: ast.expr) -> bool: return isinstance(node, ast.Constant) and node.value is None + else: + def _is_None(node: Union[ast.expr, ast.slice]) -> bool: if isinstance(node, ast.Constant): return node.value is None From 2332d6d1da19ab73404a240f49b7c530cc4b28a9 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Tue, 14 Nov 2023 21:13:27 +0100 Subject: [PATCH 09/11] Use 'type: ignore' for mypy --- pyi.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/pyi.py b/pyi.py index a175bede..bb19287a 100644 --- a/pyi.py +++ b/pyi.py @@ -396,19 +396,8 @@ def _is_type_or_Type(node: ast.expr) -> bool: return cls_name in {"type", "Type"} -if sys.version_info >= (3, 9): - - def _is_None(node: ast.expr) -> bool: - return isinstance(node, ast.Constant) and node.value is None - -else: - - def _is_None(node: Union[ast.expr, ast.slice]) -> bool: - if isinstance(node, ast.Constant): - return node.value is None - elif isinstance(node, ast.Index): - return node.value is None - return False +def _is_None(node: ast.expr) -> bool: + return isinstance(node, ast.Constant) and node.value is None class ExitArgAnalysis(NamedTuple): @@ -704,7 +693,7 @@ def _analyse_typing_Literal(node: ast.Subscript) -> TypingLiteralAnalysis: for member in members: members_by_dump[ast.dump(member)].append(member) - if _is_None(member): + if _is_None(member): # type: ignore[arg-type,unused-ignore] none_members.append(member) else: members_without_none.append(member) @@ -1475,7 +1464,7 @@ def _visit_typing_Literal(self, node: ast.Subscript) -> None: Y062_encountered = False for member_list in analysis.members_by_dump.values(): - if len(member_list) > 1 and not _is_None(member_list[0]): + if len(member_list) > 1 and not _is_None(member_list[0]): # type: ignore[arg-type,unused-ignore] Y062_encountered = True self.error(member_list[1], Y062.format(unparse(member_list[1]))) From 3dd4a85eda4589da1d37e8ba1c644123ca97d703 Mon Sep 17 00:00:00 2001 From: Tomas R Date: Wed, 15 Nov 2023 19:24:40 +0100 Subject: [PATCH 10/11] Apply suggestions from code review Co-authored-by: Alex Waygood --- CHANGELOG.md | 3 +++ pyi.py | 4 ++++ tests/literals.pyi | 16 +++++++++++++--- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42720b5a..275c55e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ New error codes: * Y062: Disallow duplicate elements inside `Literal[]` slices. +Other changes: +* Y061 is no longer emitted in situations where Y062 would also be emitted. + ## 23.11.0 New error codes: diff --git a/pyi.py b/pyi.py index bb19287a..971bde3d 100644 --- a/pyi.py +++ b/pyi.py @@ -693,6 +693,8 @@ def _analyse_typing_Literal(node: ast.Subscript) -> TypingLiteralAnalysis: for member in members: members_by_dump[ast.dump(member)].append(member) + # https://github.com/PyCQA/flake8-pyi/pull/449#discussion_r1391804472 + # TODO: Remove the `type: ignore` when we drop support for py38 if _is_None(member): # type: ignore[arg-type,unused-ignore] none_members.append(member) else: @@ -1464,6 +1466,8 @@ def _visit_typing_Literal(self, node: ast.Subscript) -> None: Y062_encountered = False for member_list in analysis.members_by_dump.values(): + # https://github.com/PyCQA/flake8-pyi/pull/449#discussion_r1391804472 + # TODO: Remove the `type: ignore` when we drop support for py38 if len(member_list) > 1 and not _is_None(member_list[0]): # type: ignore[arg-type,unused-ignore] Y062_encountered = True self.error(member_list[1], Y062.format(unparse(member_list[1]))) diff --git a/tests/literals.pyi b/tests/literals.pyi index 199d3218..57124c07 100644 --- a/tests/literals.pyi +++ b/tests/literals.pyi @@ -1,13 +1,23 @@ from typing import Literal Literal[None] # Y061 None inside "Literal[]" expression. Replace with "None" -Literal[None, None] # Y061 None inside "Literal[]" expression. Replace with "None" Literal[True, None] # Y061 None inside "Literal[]" expression. Replace with "Literal[True] | None" -Literal[1, None, "foo", None] # Y061 None inside "Literal[]" expression. Replace with "Literal[1, 'foo'] | None" Literal[True, True] # Y062 Duplicate "Literal[]" member "True" Literal[True, True, True] # Y062 Duplicate "Literal[]" member "True" Literal[True, False, True, False] # Y062 Duplicate "Literal[]" member "True" # Y062 Duplicate "Literal[]" member "False" -# If both Y061 and Y062 would be emitted, only emit Y062 +### +# The following rules here are slightly subtle, +# but make sense when it comes to giving the best suggestions to users of flake8-pyi. +### + +# If Y061 and Y062 both apply, but all the duplicate members are None, +# only emit Y061... +Literal[None, None] # Y061 None inside "Literal[]" expression. Replace with "None" +Literal[1, None, "foo", None] # Y061 None inside "Literal[]" expression. Replace with "Literal[1, 'foo'] | None" + +# ... but if Y061 and Y062 both apply +# and there are no None members in the Literal[] slice, +# only emit Y062: Literal[None, True, None, True] # Y062 Duplicate "Literal[]" member "True" From b7457998ff24eba5be0b9657f5b2aa8b6db4e577 Mon Sep 17 00:00:00 2001 From: Tomas R Date: Wed, 15 Nov 2023 19:26:43 +0100 Subject: [PATCH 11/11] Update ERRORCODES.md Co-authored-by: Alex Waygood --- ERRORCODES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ERRORCODES.md b/ERRORCODES.md index bb0a651b..1e0bd31d 100644 --- a/ERRORCODES.md +++ b/ERRORCODES.md @@ -63,7 +63,7 @@ The following warnings are currently emitted by default: | Y058 | Use `Iterator` rather than `Generator` as the return value for simple `__iter__` methods, and `AsyncIterator` rather than `AsyncGenerator` as the return value for simple `__aiter__` methods. Using `(Async)Iterator` for these methods is simpler and more elegant, and reflects the fact that the precise kind of iterator returned from an `__iter__` method is usually an implementation detail that could change at any time, and should not be relied upon. | Y059 | `Generic[]` should always be the last base class, if it is present in a class's bases tuple. At runtime, if `Generic[]` is not the final class in a the bases tuple, this [can cause the class creation to fail](https://github.com/python/cpython/issues/106102). In a stub file, however, this rule is enforced purely for stylistic consistency. | Y060 | Redundant inheritance from `Generic[]`. For example, `class Foo(Iterable[_T], Generic[_T]): ...` can be written more simply as `class Foo(Iterable[_T]): ...`.

To avoid false-positive errors, and to avoid complexity in the implementation, this check is deliberately conservative: it only flags classes where all subscripted bases have identical code inside their subscript slices. -| Y061 | Do not use `None` inside a `Literal[]` slice. For example, use `Literal["foo"] \| None` instead of `Literal["foo", None]`. While both are legal according to [PEP 586](https://peps.python.org/pep-0586/), the former is preferred for stylistic consistency. Note that this warning is not emitted if Y062 is emitted for the same `Literal[]` slice. For example, `Literal[None, None, True, True]` only emits Y062. +| Y061 | Do not use `None` inside a `Literal[]` slice. For example, use `Literal["foo"] \| None` instead of `Literal["foo", None]`. While both are legal according to [PEP 586](https://peps.python.org/pep-0586/), the former is preferred for stylistic consistency. Note that this warning is not emitted if Y062 is emitted for the same `Literal[]` slice. For example, `Literal[None, None, True, True]` only causes Y062 to be emitted. | Y062 | `Literal[]` slices shouldn't contain duplicates, e.g. `Literal[True, True]` is not allowed. ## Warnings disabled by default