Skip to content

Commit 8e3995d

Browse files
authored
New rule: prefer x: Final = 42 over x: Final[Literal[42]] (#469)
1 parent 1f311d2 commit 8e3995d

3 files changed

Lines changed: 48 additions & 3 deletions

File tree

ERRORCODES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ The following warnings are currently emitted by default:
7777
| 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. | Style
7878
| Y062 | `Literal[]` slices shouldn't contain duplicates, e.g. `Literal[True, True]` is not allowed. | Redundant code
7979
| Y063 | Use [PEP 570 syntax](https://peps.python.org/pep-0570/) (e.g. `def foo(x: int, /) -> None: ...`) to denote positional-only arguments, rather than [the older Python 3.7-compatible syntax described in PEP 484](https://peps.python.org/pep-0484/#positional-only-arguments) (`def foo(__x: int) -> None: ...`, etc.). | Style
80+
| Y064 | Use simpler syntax to define final literal types. For example, use `x: Final = 42` instead of `x: Final[Literal[42]]`. | Style
8081

8182
## Warnings disabled by default
8283

pyi.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,7 @@ class PyiVisitor(ast.NodeVisitor):
10411041
long_strings_allowed: NestingCounter
10421042
in_function: NestingCounter
10431043
visiting_arg: NestingCounter
1044+
Y061_suppressed: NestingCounter
10441045

10451046
# This is only relevant for visiting classes
10461047
enclosing_class_ctx: EnclosingClassContext | None = None
@@ -1058,6 +1059,7 @@ def __init__(self, filename: str) -> None:
10581059
self.long_strings_allowed = NestingCounter()
10591060
self.in_function = NestingCounter()
10601061
self.visiting_arg = NestingCounter()
1062+
self.Y061_suppressed = NestingCounter()
10611063

10621064
def __repr__(self) -> str:
10631065
return f"{self.__class__.__name__}(filename={self.filename!r})"
@@ -1318,7 +1320,14 @@ def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
13181320
)
13191321

13201322
self.visit(node_target)
1321-
self.visit(node_annotation)
1323+
1324+
Y064_encountered = self._check_for_Y064_violations(node)
1325+
if Y064_encountered:
1326+
with self.Y061_suppressed.enabled():
1327+
self.visit(node_annotation)
1328+
else:
1329+
self.visit(node_annotation)
1330+
13221331
if node_value is not None:
13231332
if is_typealias:
13241333
self.visit(node_value)
@@ -1354,6 +1363,35 @@ def visit_TypeAlias(self, node: ast.TypeAlias) -> None:
13541363
self.generic_visit(node)
13551364
self._check_typealias(node=node, alias_name=node.name.id)
13561365

1366+
def _check_for_Y064_violations(self, node: ast.AnnAssign) -> bool:
1367+
annotation = node.annotation
1368+
1369+
if node.value or not isinstance(annotation, ast.Subscript):
1370+
return False
1371+
1372+
value = annotation.value
1373+
slice_ = annotation.slice
1374+
1375+
if (
1376+
_is_Final(value)
1377+
and isinstance(slice_, ast.Subscript)
1378+
and _is_Literal(slice_.value)
1379+
and isinstance(slice_.slice, ast.Constant)
1380+
):
1381+
final = ast.Name(id="Final", ctx=ast.Load())
1382+
suggestion = ast.AnnAssign(
1383+
target=node.target,
1384+
annotation=final,
1385+
value=slice_.slice,
1386+
simple=node.simple,
1387+
)
1388+
self.error(
1389+
node,
1390+
Y064.format(suggestion=unparse(suggestion), original=unparse(node)),
1391+
)
1392+
return True
1393+
return False
1394+
13571395
def _check_union_members(
13581396
self, members: Sequence[ast.expr], is_pep_604_union: bool
13591397
) -> None:
@@ -1513,7 +1551,7 @@ def _visit_typing_Literal(self, node: ast.Subscript) -> None:
15131551
Y062_encountered = True
15141552
self.error(member_list[1], Y062.format(unparse(member_list[1])))
15151553

1516-
if not Y062_encountered:
1554+
if not Y062_encountered and not self.Y061_suppressed.active:
15171555
if analysis.contains_only_none:
15181556
self.error(node.slice, Y061.format(suggestion="None"))
15191557
elif analysis.none_members:
@@ -2366,6 +2404,7 @@ def parse_options(options: argparse.Namespace) -> None:
23662404
Y061 = 'Y061 None inside "Literal[]" expression. Replace with "{suggestion}"'
23672405
Y062 = 'Y062 Duplicate "Literal[]" member "{}"'
23682406
Y063 = "Y063 Use PEP-570 syntax to indicate positional-only arguments"
2407+
Y064 = 'Y064 Use "{suggestion}" instead of "{original}"'
23692408
Y090 = (
23702409
'Y090 "{original}" means '
23712410
'"a tuple of length 1, in which the sole element is of type {typ!r}". '

tests/literals.pyi

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Literal
1+
from typing import Final, Literal
22

33
Literal[None] # Y061 None inside "Literal[]" expression. Replace with "None"
44
Literal[True, None] # Y061 None inside "Literal[]" expression. Replace with "Literal[True] | None"
@@ -21,3 +21,8 @@ Literal[1, None, "foo", None] # Y061 None inside "Literal[]" expression. Replac
2121
# and there are no None members in the Literal[] slice,
2222
# only emit Y062:
2323
Literal[None, True, None, True] # Y062 Duplicate "Literal[]" member "True"
24+
25+
x: Final[Literal[True]] # Y064 Use "x: Final = True" instead of "x: Final[Literal[True]]"
26+
# If Y061 and Y064 both apply, only emit Y064
27+
y: Final[Literal[None]] # Y064 Use "y: Final = None" instead of "y: Final[Literal[None]]"
28+
z: Final[Literal[True, False]]

0 commit comments

Comments
 (0)