diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e3dcded..650b8e61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ New error codes: * Introduce Y059: `Generic[]` should always be the last base class, if it is present in the bases of a class. * Introduce Y060, which flags redundant inheritance from `Generic[]`. +* Introduce Y061: Do not use `None` inside a `Literal[]` slice. + For example, use `Literal["foo"] | None` instead of `Literal["foo", None]`. Other changes: * The undocumented `pyi.__version__` and `pyi.PyiTreeChecker.version` diff --git a/ERRORCODES.md b/ERRORCODES.md index ea7138fa..042954b5 100644 --- a/ERRORCODES.md +++ b/ERRORCODES.md @@ -63,6 +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 looks at classes that have exactly two bases. +| 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. ## Warnings disabled by default diff --git a/pyi.py b/pyi.py index ea3e7674..eff7ccbd 100644 --- a/pyi.py +++ b/pyi.py @@ -1359,7 +1359,7 @@ def visit_Subscript(self, node: ast.Subscript) -> None: self.visit(subscripted_object) if subscripted_object_name == "Literal": with self.string_literals_allowed.enabled(): - self.visit(node.slice) + self._visit_typing_Literal(node) return if isinstance(node.slice, ast.Tuple): @@ -1369,6 +1369,25 @@ def visit_Subscript(self, node: ast.Subscript) -> None: if subscripted_object_name in {"tuple", "Tuple"}: 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]` + 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] + elts[i + 1 :] + 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` + self.visit(node.slice) + def _visit_slice_tuple(self, node: ast.Tuple, parent: str | None) -> None: if parent == "Union": self._check_union_members(node.elts, is_pep_604_union=False) @@ -2180,6 +2199,7 @@ def parse_options(options: argparse.Namespace) -> None: 'Y060 Redundant inheritance from "Generic[]"; ' "class would be inferred as generic anyway" ) +Y061 = 'Y061 None inside "Literal[]" expression. Replace with "{suggestion}"' 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 new file mode 100644 index 00000000..897ba6cb --- /dev/null +++ b/tests/literals.pyi @@ -0,0 +1,5 @@ +from typing import Literal + +Literal[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] | None"