]> crepu.dev Git - config.git/blame_incremental - djavu-asus/emacs/elpy/rpc-venv/lib/python3.11/site-packages/flake8/style_guide.py
Reorganización de directorios
[config.git] / djavu-asus / emacs / elpy / rpc-venv / lib / python3.11 / site-packages / flake8 / style_guide.py
... / ...
CommitLineData
1"""Implementation of the StyleGuide used by Flake8."""
2from __future__ import annotations
3
4import argparse
5import contextlib
6import copy
7import enum
8import functools
9import logging
10from typing import Generator
11from typing import Sequence
12
13from flake8 import defaults
14from flake8 import statistics
15from flake8 import utils
16from flake8.formatting import base as base_formatter
17from flake8.violation import Violation
18
19__all__ = ("StyleGuide",)
20
21LOG = logging.getLogger(__name__)
22
23
24class Selected(enum.Enum):
25 """Enum representing an explicitly or implicitly selected code."""
26
27 Explicitly = "explicitly selected"
28 Implicitly = "implicitly selected"
29
30
31class Ignored(enum.Enum):
32 """Enum representing an explicitly or implicitly ignored code."""
33
34 Explicitly = "explicitly ignored"
35 Implicitly = "implicitly ignored"
36
37
38class Decision(enum.Enum):
39 """Enum representing whether a code should be ignored or selected."""
40
41 Ignored = "ignored error"
42 Selected = "selected error"
43
44
45def _explicitly_chosen(
46 *,
47 option: list[str] | None,
48 extend: list[str] | None,
49) -> tuple[str, ...]:
50 ret = [*(option or []), *(extend or [])]
51 return tuple(sorted(ret, reverse=True))
52
53
54def _select_ignore(
55 *,
56 option: list[str] | None,
57 default: tuple[str, ...],
58 extended_default: list[str],
59 extend: list[str] | None,
60) -> tuple[str, ...]:
61 # option was explicitly set, ignore the default and extended default
62 if option is not None:
63 ret = [*option, *(extend or [])]
64 else:
65 ret = [*default, *extended_default, *(extend or [])]
66 return tuple(sorted(ret, reverse=True))
67
68
69class DecisionEngine:
70 """A class for managing the decision process around violations.
71
72 This contains the logic for whether a violation should be reported or
73 ignored.
74 """
75
76 def __init__(self, options: argparse.Namespace) -> None:
77 """Initialize the engine."""
78 self.cache: dict[str, Decision] = {}
79
80 self.selected_explicitly = _explicitly_chosen(
81 option=options.select,
82 extend=options.extend_select,
83 )
84 self.ignored_explicitly = _explicitly_chosen(
85 option=options.ignore,
86 extend=options.extend_ignore,
87 )
88
89 self.selected = _select_ignore(
90 option=options.select,
91 default=(),
92 extended_default=options.extended_default_select,
93 extend=options.extend_select,
94 )
95 self.ignored = _select_ignore(
96 option=options.ignore,
97 default=defaults.IGNORE,
98 extended_default=options.extended_default_ignore,
99 extend=options.extend_ignore,
100 )
101
102 def was_selected(self, code: str) -> Selected | Ignored:
103 """Determine if the code has been selected by the user.
104
105 :param code: The code for the check that has been run.
106 :returns:
107 Selected.Implicitly if the selected list is empty,
108 Selected.Explicitly if the selected list is not empty and a match
109 was found,
110 Ignored.Implicitly if the selected list is not empty but no match
111 was found.
112 """
113 if code.startswith(self.selected_explicitly):
114 return Selected.Explicitly
115 elif code.startswith(self.selected):
116 return Selected.Implicitly
117 else:
118 return Ignored.Implicitly
119
120 def was_ignored(self, code: str) -> Selected | Ignored:
121 """Determine if the code has been ignored by the user.
122
123 :param code:
124 The code for the check that has been run.
125 :returns:
126 Selected.Implicitly if the ignored list is empty,
127 Ignored.Explicitly if the ignored list is not empty and a match was
128 found,
129 Selected.Implicitly if the ignored list is not empty but no match
130 was found.
131 """
132 if code.startswith(self.ignored_explicitly):
133 return Ignored.Explicitly
134 elif code.startswith(self.ignored):
135 return Ignored.Implicitly
136 else:
137 return Selected.Implicitly
138
139 def make_decision(self, code: str) -> Decision:
140 """Decide if code should be ignored or selected."""
141 selected = self.was_selected(code)
142 ignored = self.was_ignored(code)
143 LOG.debug(
144 "The user configured %r to be %r, %r",
145 code,
146 selected,
147 ignored,
148 )
149
150 if isinstance(selected, Selected) and isinstance(ignored, Selected):
151 return Decision.Selected
152 elif isinstance(selected, Ignored) and isinstance(ignored, Ignored):
153 return Decision.Ignored
154 elif (
155 selected is Selected.Explicitly
156 and ignored is not Ignored.Explicitly
157 ):
158 return Decision.Selected
159 elif (
160 selected is not Selected.Explicitly
161 and ignored is Ignored.Explicitly
162 ):
163 return Decision.Ignored
164 elif selected is Ignored.Implicitly and ignored is Selected.Implicitly:
165 return Decision.Ignored
166 elif (
167 selected is Selected.Explicitly and ignored is Ignored.Explicitly
168 ) or (
169 selected is Selected.Implicitly and ignored is Ignored.Implicitly
170 ):
171 # we only get here if it was in both lists: longest prefix wins
172 select = next(s for s in self.selected if code.startswith(s))
173 ignore = next(s for s in self.ignored if code.startswith(s))
174 if len(select) > len(ignore):
175 return Decision.Selected
176 else:
177 return Decision.Ignored
178 else:
179 raise AssertionError(f"unreachable {code} {selected} {ignored}")
180
181 def decision_for(self, code: str) -> Decision:
182 """Return the decision for a specific code.
183
184 This method caches the decisions for codes to avoid retracing the same
185 logic over and over again. We only care about the select and ignore
186 rules as specified by the user in their configuration files and
187 command-line flags.
188
189 This method does not look at whether the specific line is being
190 ignored in the file itself.
191
192 :param code: The code for the check that has been run.
193 """
194 decision = self.cache.get(code)
195 if decision is None:
196 decision = self.make_decision(code)
197 self.cache[code] = decision
198 LOG.debug('"%s" will be "%s"', code, decision)
199 return decision
200
201
202class StyleGuideManager:
203 """Manage multiple style guides for a single run."""
204
205 def __init__(
206 self,
207 options: argparse.Namespace,
208 formatter: base_formatter.BaseFormatter,
209 decider: DecisionEngine | None = None,
210 ) -> None:
211 """Initialize our StyleGuide.
212
213 .. todo:: Add parameter documentation.
214 """
215 self.options = options
216 self.formatter = formatter
217 self.stats = statistics.Statistics()
218 self.decider = decider or DecisionEngine(options)
219 self.style_guides: list[StyleGuide] = []
220 self.default_style_guide = StyleGuide(
221 options, formatter, self.stats, decider=decider
222 )
223 self.style_guides = [
224 self.default_style_guide,
225 *self.populate_style_guides_with(options),
226 ]
227
228 self.style_guide_for = functools.lru_cache(maxsize=None)(
229 self._style_guide_for
230 )
231
232 def populate_style_guides_with(
233 self, options: argparse.Namespace
234 ) -> Generator[StyleGuide, None, None]:
235 """Generate style guides from the per-file-ignores option.
236
237 :param options:
238 The original options parsed from the CLI and config file.
239 :returns:
240 A copy of the default style guide with overridden values.
241 """
242 per_file = utils.parse_files_to_codes_mapping(options.per_file_ignores)
243 for filename, violations in per_file:
244 yield self.default_style_guide.copy(
245 filename=filename, extend_ignore_with=violations
246 )
247
248 def _style_guide_for(self, filename: str) -> StyleGuide:
249 """Find the StyleGuide for the filename in particular."""
250 return max(
251 (g for g in self.style_guides if g.applies_to(filename)),
252 key=lambda g: len(g.filename or ""),
253 )
254
255 @contextlib.contextmanager
256 def processing_file(
257 self, filename: str
258 ) -> Generator[StyleGuide, None, None]:
259 """Record the fact that we're processing the file's results."""
260 guide = self.style_guide_for(filename)
261 with guide.processing_file(filename):
262 yield guide
263
264 def handle_error(
265 self,
266 code: str,
267 filename: str,
268 line_number: int,
269 column_number: int,
270 text: str,
271 physical_line: str | None = None,
272 ) -> int:
273 """Handle an error reported by a check.
274
275 :param code:
276 The error code found, e.g., E123.
277 :param filename:
278 The file in which the error was found.
279 :param line_number:
280 The line number (where counting starts at 1) at which the error
281 occurs.
282 :param column_number:
283 The column number (where counting starts at 1) at which the error
284 occurs.
285 :param text:
286 The text of the error message.
287 :param physical_line:
288 The actual physical line causing the error.
289 :returns:
290 1 if the error was reported. 0 if it was ignored. This is to allow
291 for counting of the number of errors found that were not ignored.
292 """
293 guide = self.style_guide_for(filename)
294 return guide.handle_error(
295 code, filename, line_number, column_number, text, physical_line
296 )
297
298
299class StyleGuide:
300 """Manage a Flake8 user's style guide."""
301
302 def __init__(
303 self,
304 options: argparse.Namespace,
305 formatter: base_formatter.BaseFormatter,
306 stats: statistics.Statistics,
307 filename: str | None = None,
308 decider: DecisionEngine | None = None,
309 ):
310 """Initialize our StyleGuide.
311
312 .. todo:: Add parameter documentation.
313 """
314 self.options = options
315 self.formatter = formatter
316 self.stats = stats
317 self.decider = decider or DecisionEngine(options)
318 self.filename = filename
319 if self.filename:
320 self.filename = utils.normalize_path(self.filename)
321
322 def __repr__(self) -> str:
323 """Make it easier to debug which StyleGuide we're using."""
324 return f"<StyleGuide [{self.filename}]>"
325
326 def copy(
327 self,
328 filename: str | None = None,
329 extend_ignore_with: Sequence[str] | None = None,
330 ) -> StyleGuide:
331 """Create a copy of this style guide with different values."""
332 filename = filename or self.filename
333 options = copy.deepcopy(self.options)
334 options.extend_ignore = options.extend_ignore or []
335 options.extend_ignore.extend(extend_ignore_with or [])
336 return StyleGuide(
337 options, self.formatter, self.stats, filename=filename
338 )
339
340 @contextlib.contextmanager
341 def processing_file(
342 self, filename: str
343 ) -> Generator[StyleGuide, None, None]:
344 """Record the fact that we're processing the file's results."""
345 self.formatter.beginning(filename)
346 yield self
347 self.formatter.finished(filename)
348
349 def applies_to(self, filename: str) -> bool:
350 """Check if this StyleGuide applies to the file.
351
352 :param filename:
353 The name of the file with violations that we're potentially
354 applying this StyleGuide to.
355 :returns:
356 True if this applies, False otherwise
357 """
358 if self.filename is None:
359 return True
360 return utils.matches_filename(
361 filename,
362 patterns=[self.filename],
363 log_message=f'{self!r} does %(whether)smatch "%(path)s"',
364 logger=LOG,
365 )
366
367 def should_report_error(self, code: str) -> Decision:
368 """Determine if the error code should be reported or ignored.
369
370 This method only cares about the select and ignore rules as specified
371 by the user in their configuration files and command-line flags.
372
373 This method does not look at whether the specific line is being
374 ignored in the file itself.
375
376 :param code:
377 The code for the check that has been run.
378 """
379 return self.decider.decision_for(code)
380
381 def handle_error(
382 self,
383 code: str,
384 filename: str,
385 line_number: int,
386 column_number: int,
387 text: str,
388 physical_line: str | None = None,
389 ) -> int:
390 """Handle an error reported by a check.
391
392 :param code:
393 The error code found, e.g., E123.
394 :param filename:
395 The file in which the error was found.
396 :param line_number:
397 The line number (where counting starts at 1) at which the error
398 occurs.
399 :param column_number:
400 The column number (where counting starts at 1) at which the error
401 occurs.
402 :param text:
403 The text of the error message.
404 :param physical_line:
405 The actual physical line causing the error.
406 :returns:
407 1 if the error was reported. 0 if it was ignored. This is to allow
408 for counting of the number of errors found that were not ignored.
409 """
410 disable_noqa = self.options.disable_noqa
411 # NOTE(sigmavirus24): Apparently we're provided with 0-indexed column
412 # numbers so we have to offset that here.
413 if not column_number:
414 column_number = 0
415 error = Violation(
416 code,
417 filename,
418 line_number,
419 column_number + 1,
420 text,
421 physical_line,
422 )
423 error_is_selected = (
424 self.should_report_error(error.code) is Decision.Selected
425 )
426 is_not_inline_ignored = error.is_inline_ignored(disable_noqa) is False
427 if error_is_selected and is_not_inline_ignored:
428 self.formatter.handle(error)
429 self.stats.record(error)
430 return 1
431 return 0