|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | | -import argparse |
4 | 3 | import ast |
5 | 4 | import logging |
6 | 5 | import re |
7 | 6 | from dataclasses import dataclass |
8 | | -from typing import Any, ClassVar, Iterator, Literal |
| 7 | +from typing import ClassVar, Iterator |
9 | 8 |
|
10 | | -from flake8 import checker |
11 | 9 | from flake8.options.manager import OptionManager |
12 | | -from flake8.plugins.finder import LoadedPlugin |
13 | | -from flake8.plugins.pyflakes import FlakesChecker |
14 | | -from pyflakes.checker import ModuleScope |
15 | 10 |
|
16 | 11 | from . import errors, visitor |
17 | 12 |
|
18 | 13 | LOG = logging.getLogger("flake8.pyi") |
19 | 14 |
|
20 | 15 |
|
21 | | -class PyflakesPreProcessor(ast.NodeTransformer): |
22 | | - """Transform AST prior to passing it to pyflakes. |
23 | | -
|
24 | | - This reduces false positives on recursive class definitions. |
25 | | - """ |
26 | | - |
27 | | - def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef: |
28 | | - self.generic_visit(node) |
29 | | - node.bases = [ |
30 | | - # Remove the subscript to prevent F821 errors from being emitted |
31 | | - # for (valid) recursive definitions: Foo[Bar] --> Foo |
32 | | - base.value if isinstance(base, ast.Subscript) else base |
33 | | - for base in node.bases |
34 | | - ] |
35 | | - return node |
36 | | - |
37 | | - |
38 | | -class PyiAwareFlakesChecker(FlakesChecker): |
39 | | - def __init__(self, tree: ast.AST, *args: Any, **kwargs: Any) -> None: |
40 | | - super().__init__(PyflakesPreProcessor().visit(tree), *args, **kwargs) |
41 | | - |
42 | | - @property |
43 | | - def annotationsFutureEnabled(self) -> Literal[True]: |
44 | | - """Always allow forward references in `.pyi` files. |
45 | | -
|
46 | | - Pyflakes can already handle forward refs for annotations, |
47 | | - but only via `from __future__ import annotations`. |
48 | | - In a stub file, `from __future__ import annotations` is unnecessary, |
49 | | - so we pretend to pyflakes that it's always present when linting a `.pyi` file. |
50 | | - """ |
51 | | - return True |
52 | | - |
53 | | - @annotationsFutureEnabled.setter |
54 | | - def annotationsFutureEnabled(self, value: bool) -> None: |
55 | | - """Does nothing, as we always want this property to be `True`.""" |
56 | | - |
57 | | - def ASSIGN( |
58 | | - self, tree: ast.Assign, omit: str | tuple[str, ...] | None = None |
59 | | - ) -> None: |
60 | | - """Defer evaluation of assignments in the module scope. |
61 | | -
|
62 | | - This is a custom implementation of ASSIGN, originally derived from |
63 | | - handleChildren() in pyflakes 1.3.0. |
64 | | -
|
65 | | - This reduces false positives for: |
66 | | - - TypeVars bound or constrained to forward references |
67 | | - - Assignments to forward references that are not explicitly |
68 | | - demarcated as type aliases. |
69 | | - """ |
70 | | - if not isinstance(self.scope, ModuleScope): |
71 | | - super().ASSIGN(tree) |
72 | | - return |
73 | | - |
74 | | - for target in tree.targets: |
75 | | - self.handleNode(target, tree) |
76 | | - |
77 | | - self.deferFunction(lambda: self.handleNode(tree.value, tree)) |
78 | | - |
79 | | - def handleNodeDelete(self, node: ast.AST) -> None: |
80 | | - """Null implementation. |
81 | | -
|
82 | | - Lets users use `del` in stubs to denote private names. |
83 | | - """ |
84 | | - return |
85 | | - |
86 | | - |
87 | | -class PyiAwareFileChecker(checker.FileChecker): |
88 | | - def run_check(self, plugin: LoadedPlugin, **kwargs: Any) -> Any: |
89 | | - if plugin.obj is FlakesChecker: |
90 | | - if self.filename == "-": |
91 | | - filename = self.options.stdin_display_name |
92 | | - else: |
93 | | - filename = self.filename |
94 | | - |
95 | | - if filename.endswith(".pyi"): |
96 | | - LOG.info( |
97 | | - f"Replacing FlakesChecker with PyiAwareFlakesChecker while " |
98 | | - f"checking {filename!r}" |
99 | | - ) |
100 | | - plugin = plugin._replace(obj=PyiAwareFlakesChecker) |
101 | | - return super().run_check(plugin, **kwargs) |
102 | | - |
103 | | - |
104 | 16 | _TYPE_COMMENT_REGEX = re.compile(r"#\s*type:\s*(?!\s?ignore)([^#]+)(\s*#.*?)?$") |
105 | 17 |
|
106 | 18 |
|
@@ -136,18 +48,5 @@ def run(self) -> Iterator[errors.Error]: |
136 | 48 |
|
137 | 49 | @staticmethod |
138 | 50 | def add_options(parser: OptionManager) -> None: |
139 | | - """This is brittle, there's multiple levels of caching of defaults.""" |
140 | 51 | parser.parser.set_defaults(filename="*.py,*.pyi") |
141 | 52 | parser.extend_default_ignore(errors.DISABLED_BY_DEFAULT) |
142 | | - parser.add_option( |
143 | | - "--no-pyi-aware-file-checker", |
144 | | - default=False, |
145 | | - action="store_true", |
146 | | - parse_from_config=True, |
147 | | - help="don't patch flake8 with .pyi-aware file checker", |
148 | | - ) |
149 | | - |
150 | | - @staticmethod |
151 | | - def parse_options(options: argparse.Namespace) -> None: |
152 | | - if not options.no_pyi_aware_file_checker: |
153 | | - checker.FileChecker = PyiAwareFileChecker |
0 commit comments