Skip to content

Commit fa7bab6

Browse files
authored
Add a check for sys.version_info version order (#481)
1 parent 7117446 commit fa7bab6

File tree

4 files changed

+67
-0
lines changed

4 files changed

+67
-0
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Change Log
22

3+
## Unreleased
4+
5+
New error codes:
6+
* Y066: When using if/else with `sys.version_info`,
7+
put the code for new Python versions first.
8+
39
## 24.4.0
410

511
Bugfixes:

ERRORCODES.md

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ The following warnings are currently emitted by default:
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
8080
| Y064 | Use simpler syntax to define final literal types. For example, use `x: Final = 42` instead of `x: Final[Literal[42]]`. | Style
8181
| Y065 | Don't use bare `Incomplete` in argument and return annotations. Instead, leave them unannotated. Omitting an annotation entirely from a function will cause some type checkers to view the parameter or return type as "untyped"; this may result in stricter type-checking on code that makes use of the stubbed function. | Style
82+
| Y066 | When using if/else with `sys.version_info`, put the code for new Python versions first. | Style
8283

8384
## Warnings disabled by default
8485

pyi.py

+34
Original file line numberDiff line numberDiff line change
@@ -1586,6 +1586,8 @@ def _visit_slice_tuple(self, node: ast.Tuple, parent: str | None) -> None:
15861586
self.visit(node)
15871587

15881588
def visit_If(self, node: ast.If) -> None:
1589+
self._check_for_Y066_violations(node)
1590+
15891591
test = node.test
15901592
# No types can appear in if conditions, so avoid confusing additional errors.
15911593
with self.string_literals_allowed.enabled():
@@ -1621,6 +1623,34 @@ def _check_if_expression(self, node: ast.expr) -> None:
16211623
else:
16221624
self.error(node, Y002)
16231625

1626+
def _check_for_Y066_violations(self, node: ast.If) -> None:
1627+
def is_version_info(attr: ast.expr) -> bool:
1628+
return (
1629+
isinstance(attr, ast.Attribute)
1630+
and _is_name(attr.value, "sys")
1631+
and attr.attr == "version_info"
1632+
)
1633+
1634+
def if_chain_ends_with_else(if_chain: ast.If) -> bool:
1635+
orelse = if_chain.orelse
1636+
if len(orelse) == 1 and isinstance(orelse[0], ast.If):
1637+
return if_chain_ends_with_else(orelse[0])
1638+
return bool(orelse)
1639+
1640+
test = node.test
1641+
if not isinstance(test, ast.Compare):
1642+
return
1643+
1644+
left = test.left
1645+
op = test.ops[0]
1646+
if (
1647+
is_version_info(left)
1648+
and isinstance(op, ast.Lt) # sys.version_info < ...
1649+
and if_chain_ends_with_else(node)
1650+
):
1651+
new_syntax = "if " + unparse(test).replace("<", ">=", 1)
1652+
self.error(node, Y066.format(new_syntax=new_syntax))
1653+
16241654
def _check_subscript_version_check(self, node: ast.Compare) -> None:
16251655
# unless this is on, comparisons against a single integer aren't allowed
16261656
must_be_single = False
@@ -2415,6 +2445,10 @@ def parse_options(options: argparse.Namespace) -> None:
24152445
Y063 = "Y063 Use PEP-570 syntax to indicate positional-only arguments"
24162446
Y064 = 'Y064 Use "{suggestion}" instead of "{original}"'
24172447
Y065 = 'Y065 Leave {what} unannotated rather than using "Incomplete"'
2448+
Y066 = (
2449+
"Y066 When using if/else with sys.version_info, "
2450+
'put the code for new Python versions first, e.g. "{new_syntax}"'
2451+
)
24182452
Y090 = (
24192453
'Y090 "{original}" means '
24202454
'"a tuple of length 1, in which the sole element is of type {typ!r}". '

tests/sysversioninfo.pyi

+26
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,29 @@ if sys.version_info <= (3, 0): ... # Y006 Use only < and >= for version compari
2929
if sys.version_info < (3, 5): ...
3030
if sys.version_info >= (3, 5): ...
3131
if (2, 7) <= sys.version_info < (3, 5): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info
32+
33+
if sys.version_info >= (3, 10):
34+
def foo1(x, *, bar=True, baz=False): ...
35+
elif sys.version_info >= (3, 9):
36+
def foo1(x, *, bar=True): ...
37+
else:
38+
def foo1(x): ...
39+
40+
if sys.version_info < (3, 9):
41+
def foo2(x): ...
42+
elif sys.version_info < (3, 10):
43+
def foo2(x, *, bar=True): ...
44+
45+
if sys.version_info < (3, 10): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 10)"
46+
def foo3(x): ...
47+
else:
48+
def foo3(x, *, bar=True): ...
49+
50+
if sys.version_info < (3, 8): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 8)"
51+
def foo4(x): ...
52+
elif sys.version_info < (3, 9): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 9)"
53+
def foo4(x, *, bar=True): ...
54+
elif sys.version_info < (3, 10): # Y066 When using if/else with sys.version_info, put the code for new Python versions first, e.g. "if sys.version_info >= (3, 10)"
55+
def foo4(x, *, bar=True, baz=False): ...
56+
else:
57+
def foo4(x, *, bar=True, baz=False, qux=1): ...

0 commit comments

Comments
 (0)