Skip to content

Commit 2f5b26e

Browse files
tomasr8AlexWaygood
andauthored
Enforce consistent use of Literal and None (#435)
Co-authored-by: Alex Waygood <[email protected]>
1 parent 5ce0ecf commit 2f5b26e

File tree

4 files changed

+29
-1
lines changed

4 files changed

+29
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ New error codes:
99
* Introduce Y059: `Generic[]` should always be the last base class, if it is
1010
present in the bases of a class.
1111
* Introduce Y060, which flags redundant inheritance from `Generic[]`.
12+
* Introduce Y061: Do not use `None` inside a `Literal[]` slice.
13+
For example, use `Literal["foo"] | None` instead of `Literal["foo", None]`.
1214

1315
Other changes:
1416
* The undocumented `pyi.__version__` and `pyi.PyiTreeChecker.version`

ERRORCODES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ The following warnings are currently emitted by default:
6363
| 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.
6464
| 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.
6565
| Y060 | Redundant inheritance from `Generic[]`. For example, `class Foo(Iterable[_T], Generic[_T]): ...` can be written more simply as `class Foo(Iterable[_T]): ...`.<br><br>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.
66+
| 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.
6667

6768
## Warnings disabled by default
6869

pyi.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1359,7 +1359,7 @@ def visit_Subscript(self, node: ast.Subscript) -> None:
13591359
self.visit(subscripted_object)
13601360
if subscripted_object_name == "Literal":
13611361
with self.string_literals_allowed.enabled():
1362-
self.visit(node.slice)
1362+
self._visit_typing_Literal(node)
13631363
return
13641364

13651365
if isinstance(node.slice, ast.Tuple):
@@ -1369,6 +1369,25 @@ def visit_Subscript(self, node: ast.Subscript) -> None:
13691369
if subscripted_object_name in {"tuple", "Tuple"}:
13701370
self._Y090_error(node)
13711371

1372+
def _visit_typing_Literal(self, node: ast.Subscript) -> None:
1373+
if isinstance(node.slice, ast.Constant) and _is_None(node.slice):
1374+
# Special case for `Literal[None]`
1375+
self.error(node.slice, Y061.format(suggestion="None"))
1376+
elif isinstance(node.slice, ast.Tuple):
1377+
elts = node.slice.elts
1378+
for i, elt in enumerate(elts):
1379+
if _is_None(elt):
1380+
elts_without_none = elts[:i] + elts[i + 1 :]
1381+
if len(elts_without_none) == 1:
1382+
new_literal_slice = unparse(elts_without_none[0])
1383+
else:
1384+
new_slice_node = ast.Tuple(elts=elts_without_none)
1385+
new_literal_slice = unparse(new_slice_node).strip("()")
1386+
suggestion = f"Literal[{new_literal_slice}] | None"
1387+
self.error(elt, Y061.format(suggestion=suggestion))
1388+
break # Only report the first `None`
1389+
self.visit(node.slice)
1390+
13721391
def _visit_slice_tuple(self, node: ast.Tuple, parent: str | None) -> None:
13731392
if parent == "Union":
13741393
self._check_union_members(node.elts, is_pep_604_union=False)
@@ -2180,6 +2199,7 @@ def parse_options(options: argparse.Namespace) -> None:
21802199
'Y060 Redundant inheritance from "Generic[]"; '
21812200
"class would be inferred as generic anyway"
21822201
)
2202+
Y061 = 'Y061 None inside "Literal[]" expression. Replace with "{suggestion}"'
21832203
Y090 = (
21842204
'Y090 "{original}" means '
21852205
'"a tuple of length 1, in which the sole element is of type {typ!r}". '

tests/literals.pyi

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from typing import Literal
2+
3+
Literal[None] # Y061 None inside "Literal[]" expression. Replace with "None"
4+
Literal[True, None] # Y061 None inside "Literal[]" expression. Replace with "Literal[True] | None"
5+
Literal[1, None, "foo", None] # Y061 None inside "Literal[]" expression. Replace with "Literal[1, 'foo', None] | None"

0 commit comments

Comments
 (0)