]>
Commit | Line | Data |
---|---|---|
53e6db90 DC |
1 | """The base class and interface for all formatting plugins.""" |
2 | from __future__ import annotations | |
3 | ||
4 | import argparse | |
5 | import os | |
6 | import sys | |
7 | from typing import IO | |
8 | ||
9 | from flake8.formatting import _windows_color | |
10 | from flake8.statistics import Statistics | |
11 | from flake8.violation import Violation | |
12 | ||
13 | ||
14 | class BaseFormatter: | |
15 | """Class defining the formatter interface. | |
16 | ||
17 | .. attribute:: options | |
18 | ||
19 | The options parsed from both configuration files and the command-line. | |
20 | ||
21 | .. attribute:: filename | |
22 | ||
23 | If specified by the user, the path to store the results of the run. | |
24 | ||
25 | .. attribute:: output_fd | |
26 | ||
27 | Initialized when the :meth:`start` is called. This will be a file | |
28 | object opened for writing. | |
29 | ||
30 | .. attribute:: newline | |
31 | ||
32 | The string to add to the end of a line. This is only used when the | |
33 | output filename has been specified. | |
34 | """ | |
35 | ||
36 | def __init__(self, options: argparse.Namespace) -> None: | |
37 | """Initialize with the options parsed from config and cli. | |
38 | ||
39 | This also calls a hook, :meth:`after_init`, so subclasses do not need | |
40 | to call super to call this method. | |
41 | ||
42 | :param options: | |
43 | User specified configuration parsed from both configuration files | |
44 | and the command-line interface. | |
45 | """ | |
46 | self.options = options | |
47 | self.filename = options.output_file | |
48 | self.output_fd: IO[str] | None = None | |
49 | self.newline = "\n" | |
50 | self.color = options.color == "always" or ( | |
51 | options.color == "auto" | |
52 | and sys.stdout.isatty() | |
53 | and _windows_color.terminal_supports_color | |
54 | ) | |
55 | self.after_init() | |
56 | ||
57 | def after_init(self) -> None: | |
58 | """Initialize the formatter further.""" | |
59 | ||
60 | def beginning(self, filename: str) -> None: | |
61 | """Notify the formatter that we're starting to process a file. | |
62 | ||
63 | :param filename: | |
64 | The name of the file that Flake8 is beginning to report results | |
65 | from. | |
66 | """ | |
67 | ||
68 | def finished(self, filename: str) -> None: | |
69 | """Notify the formatter that we've finished processing a file. | |
70 | ||
71 | :param filename: | |
72 | The name of the file that Flake8 has finished reporting results | |
73 | from. | |
74 | """ | |
75 | ||
76 | def start(self) -> None: | |
77 | """Prepare the formatter to receive input. | |
78 | ||
79 | This defaults to initializing :attr:`output_fd` if :attr:`filename` | |
80 | """ | |
81 | if self.filename: | |
82 | dirname = os.path.dirname(os.path.abspath(self.filename)) | |
83 | os.makedirs(dirname, exist_ok=True) | |
84 | self.output_fd = open(self.filename, "a") | |
85 | ||
86 | def handle(self, error: Violation) -> None: | |
87 | """Handle an error reported by Flake8. | |
88 | ||
89 | This defaults to calling :meth:`format`, :meth:`show_source`, and | |
90 | then :meth:`write`. To extend how errors are handled, override this | |
91 | method. | |
92 | ||
93 | :param error: | |
94 | This will be an instance of | |
95 | :class:`~flake8.violation.Violation`. | |
96 | """ | |
97 | line = self.format(error) | |
98 | source = self.show_source(error) | |
99 | self.write(line, source) | |
100 | ||
101 | def format(self, error: Violation) -> str | None: | |
102 | """Format an error reported by Flake8. | |
103 | ||
104 | This method **must** be implemented by subclasses. | |
105 | ||
106 | :param error: | |
107 | This will be an instance of | |
108 | :class:`~flake8.violation.Violation`. | |
109 | :returns: | |
110 | The formatted error string. | |
111 | """ | |
112 | raise NotImplementedError( | |
113 | "Subclass of BaseFormatter did not implement" " format." | |
114 | ) | |
115 | ||
116 | def show_statistics(self, statistics: Statistics) -> None: | |
117 | """Format and print the statistics.""" | |
118 | for error_code in statistics.error_codes(): | |
119 | stats_for_error_code = statistics.statistics_for(error_code) | |
120 | statistic = next(stats_for_error_code) | |
121 | count = statistic.count | |
122 | count += sum(stat.count for stat in stats_for_error_code) | |
123 | self._write(f"{count:<5} {error_code} {statistic.message}") | |
124 | ||
125 | def show_benchmarks(self, benchmarks: list[tuple[str, float]]) -> None: | |
126 | """Format and print the benchmarks.""" | |
127 | # NOTE(sigmavirus24): The format strings are a little confusing, even | |
128 | # to me, so here's a quick explanation: | |
129 | # We specify the named value first followed by a ':' to indicate we're | |
130 | # formatting the value. | |
131 | # Next we use '<' to indicate we want the value left aligned. | |
132 | # Then '10' is the width of the area. | |
133 | # For floats, finally, we only want only want at most 3 digits after | |
134 | # the decimal point to be displayed. This is the precision and it | |
135 | # can not be specified for integers which is why we need two separate | |
136 | # format strings. | |
137 | float_format = "{value:<10.3} {statistic}".format | |
138 | int_format = "{value:<10} {statistic}".format | |
139 | for statistic, value in benchmarks: | |
140 | if isinstance(value, int): | |
141 | benchmark = int_format(statistic=statistic, value=value) | |
142 | else: | |
143 | benchmark = float_format(statistic=statistic, value=value) | |
144 | self._write(benchmark) | |
145 | ||
146 | def show_source(self, error: Violation) -> str | None: | |
147 | """Show the physical line generating the error. | |
148 | ||
149 | This also adds an indicator for the particular part of the line that | |
150 | is reported as generating the problem. | |
151 | ||
152 | :param error: | |
153 | This will be an instance of | |
154 | :class:`~flake8.violation.Violation`. | |
155 | :returns: | |
156 | The formatted error string if the user wants to show the source. | |
157 | If the user does not want to show the source, this will return | |
158 | ``None``. | |
159 | """ | |
160 | if not self.options.show_source or error.physical_line is None: | |
161 | return "" | |
162 | ||
163 | # Because column numbers are 1-indexed, we need to remove one to get | |
164 | # the proper number of space characters. | |
165 | indent = "".join( | |
166 | c if c.isspace() else " " | |
167 | for c in error.physical_line[: error.column_number - 1] | |
168 | ) | |
169 | # Physical lines have a newline at the end, no need to add an extra | |
170 | # one | |
171 | return f"{error.physical_line}{indent}^" | |
172 | ||
173 | def _write(self, output: str) -> None: | |
174 | """Handle logic of whether to use an output file or print().""" | |
175 | if self.output_fd is not None: | |
176 | self.output_fd.write(output + self.newline) | |
177 | if self.output_fd is None or self.options.tee: | |
178 | sys.stdout.buffer.write(output.encode() + self.newline.encode()) | |
179 | ||
180 | def write(self, line: str | None, source: str | None) -> None: | |
181 | """Write the line either to the output file or stdout. | |
182 | ||
183 | This handles deciding whether to write to a file or print to standard | |
184 | out for subclasses. Override this if you want behaviour that differs | |
185 | from the default. | |
186 | ||
187 | :param line: | |
188 | The formatted string to print or write. | |
189 | :param source: | |
190 | The source code that has been formatted and associated with the | |
191 | line of output. | |
192 | """ | |
193 | if line: | |
194 | self._write(line) | |
195 | if source: | |
196 | self._write(source) | |
197 | ||
198 | def stop(self) -> None: | |
199 | """Clean up after reporting is finished.""" | |
200 | if self.output_fd is not None: | |
201 | self.output_fd.close() | |
202 | self.output_fd = None |