]>
Commit | Line | Data |
---|---|---|
53e6db90 DC |
1 | """Statistic collection logic for Flake8.""" |
2 | from __future__ import annotations | |
3 | ||
4 | from typing import Generator | |
5 | from typing import NamedTuple | |
6 | ||
7 | from flake8.violation import Violation | |
8 | ||
9 | ||
10 | class Statistics: | |
11 | """Manager of aggregated statistics for a run of Flake8.""" | |
12 | ||
13 | def __init__(self) -> None: | |
14 | """Initialize the underlying dictionary for our statistics.""" | |
15 | self._store: dict[Key, Statistic] = {} | |
16 | ||
17 | def error_codes(self) -> list[str]: | |
18 | """Return all unique error codes stored. | |
19 | ||
20 | :returns: | |
21 | Sorted list of error codes. | |
22 | """ | |
23 | return sorted({key.code for key in self._store}) | |
24 | ||
25 | def record(self, error: Violation) -> None: | |
26 | """Add the fact that the error was seen in the file. | |
27 | ||
28 | :param error: | |
29 | The Violation instance containing the information about the | |
30 | violation. | |
31 | """ | |
32 | key = Key.create_from(error) | |
33 | if key not in self._store: | |
34 | self._store[key] = Statistic.create_from(error) | |
35 | self._store[key].increment() | |
36 | ||
37 | def statistics_for( | |
38 | self, prefix: str, filename: str | None = None | |
39 | ) -> Generator[Statistic, None, None]: | |
40 | """Generate statistics for the prefix and filename. | |
41 | ||
42 | If you have a :class:`Statistics` object that has recorded errors, | |
43 | you can generate the statistics for a prefix (e.g., ``E``, ``E1``, | |
44 | ``W50``, ``W503``) with the optional filter of a filename as well. | |
45 | ||
46 | .. code-block:: python | |
47 | ||
48 | >>> stats = Statistics() | |
49 | >>> stats.statistics_for('E12', | |
50 | filename='src/flake8/statistics.py') | |
51 | <generator ...> | |
52 | >>> stats.statistics_for('W') | |
53 | <generator ...> | |
54 | ||
55 | :param prefix: | |
56 | The error class or specific error code to find statistics for. | |
57 | :param filename: | |
58 | (Optional) The filename to further filter results by. | |
59 | :returns: | |
60 | Generator of instances of :class:`Statistic` | |
61 | """ | |
62 | matching_errors = sorted( | |
63 | key for key in self._store if key.matches(prefix, filename) | |
64 | ) | |
65 | for error_code in matching_errors: | |
66 | yield self._store[error_code] | |
67 | ||
68 | ||
69 | class Key(NamedTuple): | |
70 | """Simple key structure for the Statistics dictionary. | |
71 | ||
72 | To make things clearer, easier to read, and more understandable, we use a | |
73 | namedtuple here for all Keys in the underlying dictionary for the | |
74 | Statistics object. | |
75 | """ | |
76 | ||
77 | filename: str | |
78 | code: str | |
79 | ||
80 | @classmethod | |
81 | def create_from(cls, error: Violation) -> Key: | |
82 | """Create a Key from :class:`flake8.violation.Violation`.""" | |
83 | return cls(filename=error.filename, code=error.code) | |
84 | ||
85 | def matches(self, prefix: str, filename: str | None) -> bool: | |
86 | """Determine if this key matches some constraints. | |
87 | ||
88 | :param prefix: | |
89 | The error code prefix that this key's error code should start with. | |
90 | :param filename: | |
91 | The filename that we potentially want to match on. This can be | |
92 | None to only match on error prefix. | |
93 | :returns: | |
94 | True if the Key's code starts with the prefix and either filename | |
95 | is None, or the Key's filename matches the value passed in. | |
96 | """ | |
97 | return self.code.startswith(prefix) and ( | |
98 | filename is None or self.filename == filename | |
99 | ) | |
100 | ||
101 | ||
102 | class Statistic: | |
103 | """Simple wrapper around the logic of each statistic. | |
104 | ||
105 | Instead of maintaining a simple but potentially hard to reason about | |
106 | tuple, we create a class which has attributes and a couple | |
107 | convenience methods on it. | |
108 | """ | |
109 | ||
110 | def __init__( | |
111 | self, error_code: str, filename: str, message: str, count: int | |
112 | ) -> None: | |
113 | """Initialize our Statistic.""" | |
114 | self.error_code = error_code | |
115 | self.filename = filename | |
116 | self.message = message | |
117 | self.count = count | |
118 | ||
119 | @classmethod | |
120 | def create_from(cls, error: Violation) -> Statistic: | |
121 | """Create a Statistic from a :class:`flake8.violation.Violation`.""" | |
122 | return cls( | |
123 | error_code=error.code, | |
124 | filename=error.filename, | |
125 | message=error.text, | |
126 | count=0, | |
127 | ) | |
128 | ||
129 | def increment(self) -> None: | |
130 | """Increment the number of times we've seen this error in this file.""" | |
131 | self.count += 1 |