]>
Commit | Line | Data |
---|---|---|
1 | """Plugin built-in to Flake8 to treat pyflakes as a plugin.""" | |
2 | from __future__ import annotations | |
3 | ||
4 | import argparse | |
5 | import ast | |
6 | import logging | |
7 | import os | |
8 | from typing import Any | |
9 | from typing import Generator | |
10 | ||
11 | import pyflakes.checker | |
12 | ||
13 | from flake8 import utils | |
14 | from flake8.options.manager import OptionManager | |
15 | ||
16 | LOG = logging.getLogger(__name__) | |
17 | ||
18 | FLAKE8_PYFLAKES_CODES = { | |
19 | "UnusedImport": "F401", | |
20 | "ImportShadowedByLoopVar": "F402", | |
21 | "ImportStarUsed": "F403", | |
22 | "LateFutureImport": "F404", | |
23 | "ImportStarUsage": "F405", | |
24 | "ImportStarNotPermitted": "F406", | |
25 | "FutureFeatureNotDefined": "F407", | |
26 | "PercentFormatInvalidFormat": "F501", | |
27 | "PercentFormatExpectedMapping": "F502", | |
28 | "PercentFormatExpectedSequence": "F503", | |
29 | "PercentFormatExtraNamedArguments": "F504", | |
30 | "PercentFormatMissingArgument": "F505", | |
31 | "PercentFormatMixedPositionalAndNamed": "F506", | |
32 | "PercentFormatPositionalCountMismatch": "F507", | |
33 | "PercentFormatStarRequiresSequence": "F508", | |
34 | "PercentFormatUnsupportedFormatCharacter": "F509", | |
35 | "StringDotFormatInvalidFormat": "F521", | |
36 | "StringDotFormatExtraNamedArguments": "F522", | |
37 | "StringDotFormatExtraPositionalArguments": "F523", | |
38 | "StringDotFormatMissingArgument": "F524", | |
39 | "StringDotFormatMixingAutomatic": "F525", | |
40 | "FStringMissingPlaceholders": "F541", | |
41 | "MultiValueRepeatedKeyLiteral": "F601", | |
42 | "MultiValueRepeatedKeyVariable": "F602", | |
43 | "TooManyExpressionsInStarredAssignment": "F621", | |
44 | "TwoStarredExpressions": "F622", | |
45 | "AssertTuple": "F631", | |
46 | "IsLiteral": "F632", | |
47 | "InvalidPrintSyntax": "F633", | |
48 | "IfTuple": "F634", | |
49 | "BreakOutsideLoop": "F701", | |
50 | "ContinueOutsideLoop": "F702", | |
51 | "YieldOutsideFunction": "F704", | |
52 | "ReturnOutsideFunction": "F706", | |
53 | "DefaultExceptNotLast": "F707", | |
54 | "DoctestSyntaxError": "F721", | |
55 | "ForwardAnnotationSyntaxError": "F722", | |
56 | "RedefinedWhileUnused": "F811", | |
57 | "UndefinedName": "F821", | |
58 | "UndefinedExport": "F822", | |
59 | "UndefinedLocal": "F823", | |
60 | "DuplicateArgument": "F831", | |
61 | "UnusedVariable": "F841", | |
62 | "UnusedAnnotation": "F842", | |
63 | "RaiseNotImplemented": "F901", | |
64 | } | |
65 | ||
66 | ||
67 | class FlakesChecker(pyflakes.checker.Checker): | |
68 | """Subclass the Pyflakes checker to conform with the flake8 API.""" | |
69 | ||
70 | with_doctest = False | |
71 | include_in_doctest: list[str] = [] | |
72 | exclude_from_doctest: list[str] = [] | |
73 | ||
74 | def __init__(self, tree: ast.AST, filename: str) -> None: | |
75 | """Initialize the PyFlakes plugin with an AST tree and filename.""" | |
76 | filename = utils.normalize_path(filename) | |
77 | with_doctest = self.with_doctest | |
78 | included_by = [ | |
79 | include | |
80 | for include in self.include_in_doctest | |
81 | if include != "" and filename.startswith(include) | |
82 | ] | |
83 | if included_by: | |
84 | with_doctest = True | |
85 | ||
86 | for exclude in self.exclude_from_doctest: | |
87 | if exclude != "" and filename.startswith(exclude): | |
88 | with_doctest = False | |
89 | overlapped_by = [ | |
90 | include | |
91 | for include in included_by | |
92 | if include.startswith(exclude) | |
93 | ] | |
94 | ||
95 | if overlapped_by: | |
96 | with_doctest = True | |
97 | ||
98 | super().__init__(tree, filename=filename, withDoctest=with_doctest) | |
99 | ||
100 | @classmethod | |
101 | def add_options(cls, parser: OptionManager) -> None: | |
102 | """Register options for PyFlakes on the Flake8 OptionManager.""" | |
103 | parser.add_option( | |
104 | "--builtins", | |
105 | parse_from_config=True, | |
106 | comma_separated_list=True, | |
107 | help="define more built-ins, comma separated", | |
108 | ) | |
109 | parser.add_option( | |
110 | "--doctests", | |
111 | default=False, | |
112 | action="store_true", | |
113 | parse_from_config=True, | |
114 | help="also check syntax of the doctests", | |
115 | ) | |
116 | parser.add_option( | |
117 | "--include-in-doctest", | |
118 | default="", | |
119 | dest="include_in_doctest", | |
120 | parse_from_config=True, | |
121 | comma_separated_list=True, | |
122 | normalize_paths=True, | |
123 | help="Run doctests only on these files", | |
124 | ) | |
125 | parser.add_option( | |
126 | "--exclude-from-doctest", | |
127 | default="", | |
128 | dest="exclude_from_doctest", | |
129 | parse_from_config=True, | |
130 | comma_separated_list=True, | |
131 | normalize_paths=True, | |
132 | help="Skip these files when running doctests", | |
133 | ) | |
134 | ||
135 | @classmethod | |
136 | def parse_options(cls, options: argparse.Namespace) -> None: | |
137 | """Parse option values from Flake8's OptionManager.""" | |
138 | if options.builtins: | |
139 | cls.builtIns = cls.builtIns.union(options.builtins) | |
140 | cls.with_doctest = options.doctests | |
141 | ||
142 | if options.include_in_doctest or options.exclude_from_doctest: | |
143 | LOG.warning( | |
144 | "--include-in-doctest / --exclude-from-doctest will be " | |
145 | "removed in a future version. see PyCQA/flake8#1747" | |
146 | ) | |
147 | ||
148 | included_files = [] | |
149 | for included_file in options.include_in_doctest: | |
150 | if included_file == "": | |
151 | continue | |
152 | if not included_file.startswith((os.sep, "./", "~/")): | |
153 | included_files.append(f"./{included_file}") | |
154 | else: | |
155 | included_files.append(included_file) | |
156 | cls.include_in_doctest = utils.normalize_paths(included_files) | |
157 | ||
158 | excluded_files = [] | |
159 | for excluded_file in options.exclude_from_doctest: | |
160 | if excluded_file == "": | |
161 | continue | |
162 | if not excluded_file.startswith((os.sep, "./", "~/")): | |
163 | excluded_files.append(f"./{excluded_file}") | |
164 | else: | |
165 | excluded_files.append(excluded_file) | |
166 | cls.exclude_from_doctest = utils.normalize_paths(excluded_files) | |
167 | ||
168 | inc_exc = set(cls.include_in_doctest).intersection( | |
169 | cls.exclude_from_doctest | |
170 | ) | |
171 | if inc_exc: | |
172 | raise ValueError( | |
173 | f"{inc_exc!r} was specified in both the " | |
174 | f"include-in-doctest and exclude-from-doctest " | |
175 | f"options. You are not allowed to specify it in " | |
176 | f"both for doctesting." | |
177 | ) | |
178 | ||
179 | def run(self) -> Generator[tuple[int, int, str, type[Any]], None, None]: | |
180 | """Run the plugin.""" | |
181 | for message in self.messages: | |
182 | col = getattr(message, "col", 0) | |
183 | yield ( | |
184 | message.lineno, | |
185 | col, | |
186 | "{} {}".format( | |
187 | FLAKE8_PYFLAKES_CODES.get(type(message).__name__, "F999"), | |
188 | message.message % message.message_args, | |
189 | ), | |
190 | message.__class__, | |
191 | ) |