Skip to content

Commit f6e4c9c

Browse files
authored
Check for PEP 604 usage in CI (#5903)
Since this is a common review issue and our stubs have all been converted Co-authored-by: hauntsaninja <>
1 parent 9d02cd2 commit f6e4c9c

File tree

2 files changed

+83
-0
lines changed

2 files changed

+83
-0
lines changed

.github/workflows/tests.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ jobs:
1717
- run: pip install toml
1818
- run: ./tests/check_consistent.py
1919

20+
pep-604:
21+
name: Check for PEP 604 usage
22+
runs-on: ubuntu-latest
23+
steps:
24+
- uses: actions/checkout@v2
25+
- uses: actions/setup-python@v2
26+
- run: ./tests/check_pep_604.py
27+
2028
flake8:
2129
name: Lint with flake8
2230
runs-on: ubuntu-latest

tests/check_pep_604.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/usr/bin/env python3
2+
3+
import ast
4+
import sys
5+
from pathlib import Path
6+
7+
8+
def check_pep_604(tree: ast.AST, path: Path) -> list[str]:
9+
errors = []
10+
11+
class UnionFinder(ast.NodeVisitor):
12+
def visit_Subscript(self, node: ast.Subscript) -> None:
13+
if (
14+
isinstance(node.value, ast.Name)
15+
and node.value.id == "Union"
16+
and isinstance(node.slice, ast.Tuple)
17+
):
18+
new_syntax = " | ".join(ast.unparse(x) for x in node.slice.elts)
19+
errors.append(
20+
(f"{path}:{node.lineno}: Use PEP 604 syntax for Union, e.g. `{new_syntax}`")
21+
)
22+
if (
23+
isinstance(node.value, ast.Name)
24+
and node.value.id == "Optional"
25+
):
26+
new_syntax = f"{ast.unparse(node.slice)} | None"
27+
errors.append(
28+
(f"{path}:{node.lineno}: Use PEP 604 syntax for Optional, e.g. `{new_syntax}`")
29+
)
30+
31+
# This doesn't check type aliases (or type var bounds, etc), since those are not
32+
# currently supported
33+
class AnnotationFinder(ast.NodeVisitor):
34+
def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
35+
UnionFinder().visit(node.annotation)
36+
37+
def visit_arg(self, node: ast.arg) -> None:
38+
if node.annotation is not None:
39+
UnionFinder().visit(node.annotation)
40+
41+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
42+
if node.returns is not None:
43+
UnionFinder().visit(node.returns)
44+
self.generic_visit(node)
45+
46+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
47+
if node.returns is not None:
48+
UnionFinder().visit(node.returns)
49+
self.generic_visit(node)
50+
51+
AnnotationFinder().visit(tree)
52+
return errors
53+
54+
55+
def main() -> None:
56+
errors = []
57+
for path in Path(".").glob("**/*.pyi"):
58+
if "@python2" in path.parts:
59+
continue
60+
if "stubs/protobuf/google/protobuf" in str(path): # TODO: fix protobuf stubs
61+
continue
62+
if "stubs/dateparser/" in str(path): # TODO: fix dateparser
63+
continue
64+
65+
with open(path) as f:
66+
tree = ast.parse(f.read())
67+
errors.extend(check_pep_604(tree, path))
68+
69+
if errors:
70+
print("\n".join(errors))
71+
sys.exit(1)
72+
73+
74+
if __name__ == "__main__":
75+
main()

0 commit comments

Comments
 (0)