prospector 1.10.3__py3-none-any.whl → 1.13.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- prospector/autodetect.py +10 -9
- prospector/blender.py +18 -12
- prospector/config/__init__.py +66 -49
- prospector/config/configuration.py +17 -14
- prospector/config/datatype.py +1 -1
- prospector/encoding.py +2 -2
- prospector/exceptions.py +1 -6
- prospector/finder.py +9 -8
- prospector/formatters/__init__.py +1 -1
- prospector/formatters/base.py +17 -11
- prospector/formatters/base_summary.py +43 -0
- prospector/formatters/emacs.py +5 -5
- prospector/formatters/grouped.py +9 -7
- prospector/formatters/json.py +3 -2
- prospector/formatters/pylint.py +41 -9
- prospector/formatters/text.py +10 -58
- prospector/formatters/vscode.py +17 -7
- prospector/formatters/xunit.py +6 -7
- prospector/formatters/yaml.py +4 -2
- prospector/message.py +32 -14
- prospector/pathutils.py +3 -10
- prospector/postfilter.py +9 -8
- prospector/profiles/exceptions.py +14 -11
- prospector/profiles/profile.py +70 -59
- prospector/run.py +20 -18
- prospector/suppression.py +19 -13
- prospector/tools/__init__.py +19 -13
- prospector/tools/bandit/__init__.py +27 -15
- prospector/tools/base.py +11 -3
- prospector/tools/dodgy/__init__.py +7 -3
- prospector/tools/mccabe/__init__.py +13 -6
- prospector/tools/mypy/__init__.py +44 -76
- prospector/tools/profile_validator/__init__.py +24 -15
- prospector/tools/pycodestyle/__init__.py +22 -15
- prospector/tools/pydocstyle/__init__.py +12 -6
- prospector/tools/pyflakes/__init__.py +35 -19
- prospector/tools/pylint/__init__.py +57 -31
- prospector/tools/pylint/collector.py +3 -5
- prospector/tools/pylint/linter.py +19 -14
- prospector/tools/pyright/__init__.py +18 -7
- prospector/tools/pyroma/__init__.py +10 -6
- prospector/tools/ruff/__init__.py +84 -0
- prospector/tools/utils.py +25 -19
- prospector/tools/vulture/__init__.py +25 -15
- {prospector-1.10.3.dist-info → prospector-1.13.2.dist-info}/METADATA +10 -11
- prospector-1.13.2.dist-info/RECORD +71 -0
- prospector-1.10.3.dist-info/RECORD +0 -69
- {prospector-1.10.3.dist-info → prospector-1.13.2.dist-info}/LICENSE +0 -0
- {prospector-1.10.3.dist-info → prospector-1.13.2.dist-info}/WHEEL +0 -0
- {prospector-1.10.3.dist-info → prospector-1.13.2.dist-info}/entry_points.txt +0 -0
|
@@ -1,28 +1,31 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
|
|
1
4
|
class ProfileNotFound(Exception):
|
|
2
|
-
def __init__(self, name, profile_path):
|
|
5
|
+
def __init__(self, name: str, profile_path: list[Path]) -> None:
|
|
3
6
|
super().__init__()
|
|
4
7
|
self.name = name
|
|
5
8
|
self.profile_path = profile_path
|
|
6
9
|
|
|
7
|
-
def __repr__(self):
|
|
10
|
+
def __repr__(self) -> str:
|
|
8
11
|
return "Could not find profile {}; searched in {}".format(
|
|
9
12
|
self.name,
|
|
10
|
-
":".join(self.profile_path),
|
|
13
|
+
":".join(map(str, self.profile_path)),
|
|
11
14
|
)
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
class CannotParseProfile(Exception):
|
|
15
|
-
def __init__(self, filepath, parse_error):
|
|
18
|
+
def __init__(self, filepath: str, parse_error: Exception) -> None:
|
|
16
19
|
super().__init__()
|
|
17
20
|
self.filepath = filepath
|
|
18
21
|
self.parse_error = parse_error
|
|
19
22
|
|
|
20
|
-
def get_parse_message(self):
|
|
21
|
-
return
|
|
22
|
-
self.parse_error.problem
|
|
23
|
-
self.parse_error.problem_mark.line
|
|
24
|
-
self.parse_error.problem_mark.column
|
|
23
|
+
def get_parse_message(self) -> str:
|
|
24
|
+
return (
|
|
25
|
+
f"{self.parse_error.problem}\n" # type: ignore[attr-defined]
|
|
26
|
+
f" on line {self.parse_error.problem_mark.line}: " # type: ignore[attr-defined]
|
|
27
|
+
f"char {self.parse_error.problem_mark.column}" # type: ignore[attr-defined]
|
|
25
28
|
)
|
|
26
29
|
|
|
27
|
-
def __repr__(self):
|
|
28
|
-
return "Could not parse profile found at
|
|
30
|
+
def __repr__(self) -> str:
|
|
31
|
+
return f"Could not parse profile found at {self.filepath} - it is not valid YAML"
|
prospector/profiles/profile.py
CHANGED
|
@@ -3,7 +3,7 @@ import json
|
|
|
3
3
|
import os
|
|
4
4
|
import pkgutil
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Any,
|
|
6
|
+
from typing import Any, Optional, Union
|
|
7
7
|
|
|
8
8
|
import yaml
|
|
9
9
|
|
|
@@ -14,7 +14,7 @@ BUILTIN_PROFILE_PATH = (Path(__file__).parent / "profiles").absolute()
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class ProspectorProfile:
|
|
17
|
-
def __init__(self, name: str, profile_dict:
|
|
17
|
+
def __init__(self, name: str, profile_dict: dict[str, Any], inherit_order: list[str]) -> None:
|
|
18
18
|
self.name = name
|
|
19
19
|
self.inherit_order = inherit_order
|
|
20
20
|
|
|
@@ -43,7 +43,7 @@ class ProspectorProfile:
|
|
|
43
43
|
tool_conf = profile_dict.get(tool, {})
|
|
44
44
|
|
|
45
45
|
# set the defaults for everything
|
|
46
|
-
conf:
|
|
46
|
+
conf: dict[str, Any] = {"disable": [], "enable": [], "run": None, "options": {}}
|
|
47
47
|
# use the "old" tool name
|
|
48
48
|
conf.update(tool_conf)
|
|
49
49
|
|
|
@@ -52,23 +52,28 @@ class ProspectorProfile:
|
|
|
52
52
|
|
|
53
53
|
setattr(self, tool, conf)
|
|
54
54
|
|
|
55
|
-
def get_disabled_messages(self, tool_name):
|
|
55
|
+
def get_disabled_messages(self, tool_name: str) -> list[str]:
|
|
56
56
|
disable = getattr(self, tool_name)["disable"]
|
|
57
57
|
enable = getattr(self, tool_name)["enable"]
|
|
58
58
|
return list(set(disable) - set(enable))
|
|
59
59
|
|
|
60
|
-
def
|
|
61
|
-
|
|
60
|
+
def get_enabled_messages(self, tool_name: str) -> list[str]:
|
|
61
|
+
disable = getattr(self, tool_name)["disable"]
|
|
62
|
+
enable = getattr(self, tool_name)["enable"]
|
|
63
|
+
return list(set(enable) - set(disable))
|
|
64
|
+
|
|
65
|
+
def is_tool_enabled(self, name: str) -> bool:
|
|
66
|
+
enabled: Optional[bool] = getattr(self, name).get("run")
|
|
62
67
|
if enabled is not None:
|
|
63
68
|
return enabled
|
|
64
69
|
# this is not explicitly enabled or disabled, so use the default
|
|
65
70
|
return name in DEFAULT_TOOLS
|
|
66
71
|
|
|
67
|
-
def list_profiles(self):
|
|
72
|
+
def list_profiles(self) -> list[str]:
|
|
68
73
|
# this profile is itself included
|
|
69
74
|
return [str(profile) for profile in self.inherit_order]
|
|
70
75
|
|
|
71
|
-
def as_dict(self):
|
|
76
|
+
def as_dict(self) -> dict[str, Any]:
|
|
72
77
|
out = {
|
|
73
78
|
"ignore-paths": self.ignore_paths,
|
|
74
79
|
"ignore-patterns": self.ignore_patterns,
|
|
@@ -87,19 +92,19 @@ class ProspectorProfile:
|
|
|
87
92
|
out[tool] = getattr(self, tool)
|
|
88
93
|
return out
|
|
89
94
|
|
|
90
|
-
def as_json(self):
|
|
95
|
+
def as_json(self) -> str:
|
|
91
96
|
return json.dumps(self.as_dict())
|
|
92
97
|
|
|
93
|
-
def as_yaml(self):
|
|
98
|
+
def as_yaml(self) -> str:
|
|
94
99
|
return yaml.safe_dump(self.as_dict())
|
|
95
100
|
|
|
96
101
|
@staticmethod
|
|
97
102
|
def load(
|
|
98
103
|
name_or_path: Union[str, Path],
|
|
99
|
-
profile_path:
|
|
104
|
+
profile_path: list[Path],
|
|
100
105
|
allow_shorthand: bool = True,
|
|
101
|
-
forced_inherits: Optional[
|
|
102
|
-
):
|
|
106
|
+
forced_inherits: Optional[list[str]] = None,
|
|
107
|
+
) -> "ProspectorProfile":
|
|
103
108
|
# First simply load all of the profiles and those that it explicitly inherits from
|
|
104
109
|
data, inherits = _load_and_merge(
|
|
105
110
|
name_or_path,
|
|
@@ -110,18 +115,18 @@ class ProspectorProfile:
|
|
|
110
115
|
return ProspectorProfile(str(name_or_path), data, inherits)
|
|
111
116
|
|
|
112
117
|
|
|
113
|
-
def _is_valid_extension(filename):
|
|
118
|
+
def _is_valid_extension(filename: Union[str, Path]) -> bool:
|
|
114
119
|
ext = os.path.splitext(filename)[1]
|
|
115
120
|
return ext in (".yml", ".yaml")
|
|
116
121
|
|
|
117
122
|
|
|
118
|
-
def _load_content_package(name):
|
|
123
|
+
def _load_content_package(name: str) -> Optional[dict[str, Any]]:
|
|
119
124
|
name_split = name.split(":", 1)
|
|
120
125
|
module_name = f"prospector_profile_{name_split[0]}"
|
|
121
126
|
file_names = (
|
|
122
127
|
["prospector.yaml", "prospector.yml"]
|
|
123
128
|
if len(name_split) == 1
|
|
124
|
-
else [f"{name_split[1]}.yaml", f"{name_split[1]}.
|
|
129
|
+
else [f"{name_split[1]}.yaml", f"{name_split[1]}.yml"]
|
|
125
130
|
)
|
|
126
131
|
|
|
127
132
|
data = None
|
|
@@ -138,10 +143,11 @@ def _load_content_package(name):
|
|
|
138
143
|
try:
|
|
139
144
|
return yaml.safe_load(data) or {}
|
|
140
145
|
except yaml.parser.ParserError as parse_error:
|
|
146
|
+
assert used_name is not None
|
|
141
147
|
raise CannotParseProfile(used_name, parse_error) from parse_error
|
|
142
148
|
|
|
143
149
|
|
|
144
|
-
def _load_content(name_or_path, profile_path):
|
|
150
|
+
def _load_content(name_or_path: Union[str, Path], profile_path: list[Path]) -> dict[str, Any]:
|
|
145
151
|
filename = None
|
|
146
152
|
optional = False
|
|
147
153
|
|
|
@@ -165,14 +171,14 @@ def _load_content(name_or_path, profile_path):
|
|
|
165
171
|
break
|
|
166
172
|
|
|
167
173
|
if filename is None:
|
|
168
|
-
result = _load_content_package(name_or_path)
|
|
174
|
+
result = _load_content_package(str(name_or_path))
|
|
169
175
|
if result is not None:
|
|
170
176
|
return result
|
|
171
177
|
|
|
172
178
|
if optional:
|
|
173
179
|
return {}
|
|
174
180
|
|
|
175
|
-
raise ProfileNotFound(name_or_path, profile_path)
|
|
181
|
+
raise ProfileNotFound(str(name_or_path), profile_path)
|
|
176
182
|
|
|
177
183
|
with codecs.open(filename) as fct:
|
|
178
184
|
try:
|
|
@@ -181,37 +187,34 @@ def _load_content(name_or_path, profile_path):
|
|
|
181
187
|
raise CannotParseProfile(filename, parse_error) from parse_error
|
|
182
188
|
|
|
183
189
|
|
|
184
|
-
def _ensure_list(value):
|
|
190
|
+
def _ensure_list(value: Any) -> list[Any]:
|
|
185
191
|
if isinstance(value, list):
|
|
186
192
|
return value
|
|
187
193
|
return [value]
|
|
188
194
|
|
|
189
195
|
|
|
190
|
-
def _simple_merge_dict(priority, base):
|
|
191
|
-
out =
|
|
192
|
-
|
|
196
|
+
def _simple_merge_dict(priority: dict[str, Any], base: dict[str, Any]) -> dict[str, Any]:
|
|
197
|
+
out = {**base, **priority}
|
|
198
|
+
keys = set(priority.keys()) | set(base.keys())
|
|
199
|
+
for key in keys:
|
|
200
|
+
if isinstance(base.get(key), dict) and isinstance(priority.get(key), dict):
|
|
201
|
+
out[key] = _simple_merge_dict(priority[key], base[key])
|
|
193
202
|
return out
|
|
194
203
|
|
|
195
204
|
|
|
196
|
-
def _merge_tool_config(priority, base):
|
|
197
|
-
out =
|
|
205
|
+
def _merge_tool_config(priority: dict[str, Any], base: dict[str, Any]) -> dict[str, Any]:
|
|
206
|
+
out = {**base, **priority}
|
|
198
207
|
|
|
199
208
|
# add options that are missing, but keep existing options from the priority dictionary
|
|
200
209
|
# TODO: write a unit test for this :-|
|
|
201
210
|
out["options"] = _simple_merge_dict(priority.get("options", {}), base.get("options", {}))
|
|
202
211
|
|
|
203
|
-
# copy in some basic pieces
|
|
204
|
-
for key in ("run", "load-plugins"):
|
|
205
|
-
value = priority.get(key, base.get(key))
|
|
206
|
-
if value is not None:
|
|
207
|
-
out[key] = value
|
|
208
|
-
|
|
209
212
|
# anything enabled in the 'priority' dict is removed
|
|
210
213
|
# from 'disabled' in the base dict and vice versa
|
|
211
|
-
base_disabled = base.get("disable") or []
|
|
212
|
-
base_enabled = base.get("enable") or []
|
|
213
|
-
pri_disabled = priority.get("disable") or []
|
|
214
|
-
pri_enabled = priority.get("enable") or []
|
|
214
|
+
base_disabled: list[Any] = base.get("disable") or []
|
|
215
|
+
base_enabled: list[Any] = base.get("enable") or []
|
|
216
|
+
pri_disabled: list[Any] = priority.get("disable") or []
|
|
217
|
+
pri_enabled: list[Any] = priority.get("enable") or []
|
|
215
218
|
|
|
216
219
|
out["disable"] = list(set(pri_disabled) | (set(base_disabled) - set(pri_enabled)))
|
|
217
220
|
out["enable"] = list(set(pri_enabled) | (set(base_enabled) - set(pri_disabled)))
|
|
@@ -219,7 +222,7 @@ def _merge_tool_config(priority, base):
|
|
|
219
222
|
return out
|
|
220
223
|
|
|
221
224
|
|
|
222
|
-
def _merge_profile_dict(priority: dict, base: dict) -> dict:
|
|
225
|
+
def _merge_profile_dict(priority: dict[str, Any], base: dict[str, Any]) -> dict[str, Any]:
|
|
223
226
|
# copy the base dict into our output
|
|
224
227
|
out = dict(base.items())
|
|
225
228
|
|
|
@@ -254,7 +257,7 @@ def _merge_profile_dict(priority: dict, base: dict) -> dict:
|
|
|
254
257
|
return out
|
|
255
258
|
|
|
256
259
|
|
|
257
|
-
def _determine_strictness(profile_dict, inherits):
|
|
260
|
+
def _determine_strictness(profile_dict: dict[str, Any], inherits: list[str]) -> tuple[Optional[str], bool]:
|
|
258
261
|
for profile in inherits:
|
|
259
262
|
if profile.startswith("strictness_"):
|
|
260
263
|
return None, False
|
|
@@ -262,10 +265,10 @@ def _determine_strictness(profile_dict, inherits):
|
|
|
262
265
|
strictness = profile_dict.get("strictness")
|
|
263
266
|
if strictness is None:
|
|
264
267
|
return None, False
|
|
265
|
-
return ("strictness_
|
|
268
|
+
return (f"strictness_{strictness}"), True
|
|
266
269
|
|
|
267
270
|
|
|
268
|
-
def _determine_pep8(profile_dict):
|
|
271
|
+
def _determine_pep8(profile_dict: dict[str, Any]) -> tuple[Optional[str], bool]:
|
|
269
272
|
pep8 = profile_dict.get("pep8")
|
|
270
273
|
if pep8 == "full":
|
|
271
274
|
return "full_pep8", True
|
|
@@ -276,28 +279,30 @@ def _determine_pep8(profile_dict):
|
|
|
276
279
|
return None, False
|
|
277
280
|
|
|
278
281
|
|
|
279
|
-
def _determine_doc_warnings(profile_dict):
|
|
282
|
+
def _determine_doc_warnings(profile_dict: dict[str, Any]) -> tuple[Optional[str], bool]:
|
|
280
283
|
doc_warnings = profile_dict.get("doc-warnings")
|
|
281
284
|
if doc_warnings is None:
|
|
282
285
|
return None, False
|
|
283
286
|
return ("doc_warnings" if doc_warnings else "no_doc_warnings"), True
|
|
284
287
|
|
|
285
288
|
|
|
286
|
-
def _determine_test_warnings(profile_dict):
|
|
289
|
+
def _determine_test_warnings(profile_dict: dict[str, Any]) -> tuple[Optional[str], bool]:
|
|
287
290
|
test_warnings = profile_dict.get("test-warnings")
|
|
288
291
|
if test_warnings is None:
|
|
289
292
|
return None, False
|
|
290
293
|
return (None if test_warnings else "no_test_warnings"), True
|
|
291
294
|
|
|
292
295
|
|
|
293
|
-
def _determine_member_warnings(profile_dict):
|
|
296
|
+
def _determine_member_warnings(profile_dict: dict[str, Any]) -> tuple[Optional[str], bool]:
|
|
294
297
|
member_warnings = profile_dict.get("member-warnings")
|
|
295
298
|
if member_warnings is None:
|
|
296
299
|
return None, False
|
|
297
300
|
return ("member_warnings" if member_warnings else "no_member_warnings"), True
|
|
298
301
|
|
|
299
302
|
|
|
300
|
-
def _determine_implicit_inherits(
|
|
303
|
+
def _determine_implicit_inherits(
|
|
304
|
+
profile_dict: dict[str, Any], already_inherits: list[str], shorthands_found: set[str]
|
|
305
|
+
) -> tuple[list[str], set[str]]:
|
|
301
306
|
# Note: the ordering is very important here - the earlier items
|
|
302
307
|
# in the list have precedence over the later items. The point of
|
|
303
308
|
# the doc/test/pep8 profiles is usually to restore items which were
|
|
@@ -324,7 +329,13 @@ def _determine_implicit_inherits(profile_dict, already_inherits, shorthands_foun
|
|
|
324
329
|
return inherits, shorthands_found
|
|
325
330
|
|
|
326
331
|
|
|
327
|
-
def _append_profiles(
|
|
332
|
+
def _append_profiles(
|
|
333
|
+
name: str,
|
|
334
|
+
profile_path: list[Path],
|
|
335
|
+
data: dict[Union[str, Path], Any],
|
|
336
|
+
inherit_list: list[str],
|
|
337
|
+
allow_shorthand: bool = False,
|
|
338
|
+
) -> tuple[dict[Union[str, Path], Any], list[str]]:
|
|
328
339
|
new_data, new_il, _ = _load_profile(name, profile_path, allow_shorthand=allow_shorthand)
|
|
329
340
|
data.update(new_data)
|
|
330
341
|
inherit_list += new_il
|
|
@@ -333,10 +344,10 @@ def _append_profiles(name, profile_path, data, inherit_list, allow_shorthand=Fal
|
|
|
333
344
|
|
|
334
345
|
def _load_and_merge(
|
|
335
346
|
name_or_path: Union[str, Path],
|
|
336
|
-
profile_path:
|
|
347
|
+
profile_path: list[Path],
|
|
337
348
|
allow_shorthand: bool = True,
|
|
338
|
-
forced_inherits:
|
|
339
|
-
) ->
|
|
349
|
+
forced_inherits: Optional[list[str]] = None,
|
|
350
|
+
) -> tuple[dict[str, Any], list[str]]:
|
|
340
351
|
# First simply load all of the profiles and those that it explicitly inherits from
|
|
341
352
|
data, inherit_list, shorthands_found = _load_profile(
|
|
342
353
|
str(name_or_path),
|
|
@@ -367,7 +378,7 @@ def _load_and_merge(
|
|
|
367
378
|
# top of the inheritance tree to the bottom). This means that the lower down
|
|
368
379
|
# values overwrite those from above, meaning that the initially provided profile
|
|
369
380
|
# has precedence.
|
|
370
|
-
merged: dict = {}
|
|
381
|
+
merged: dict[str, Any] = {}
|
|
371
382
|
for name in inherit_list[::-1]:
|
|
372
383
|
priority = data[name]
|
|
373
384
|
merged = _merge_profile_dict(priority, merged)
|
|
@@ -375,7 +386,7 @@ def _load_and_merge(
|
|
|
375
386
|
return merged, inherit_list
|
|
376
387
|
|
|
377
388
|
|
|
378
|
-
def _transform_legacy(profile_dict):
|
|
389
|
+
def _transform_legacy(profile_dict: dict[str, Any]) -> dict[str, Any]:
|
|
379
390
|
"""
|
|
380
391
|
After pep8 was renamed to pycodestyle, this pre-filter just moves profile
|
|
381
392
|
config blocks using the old name to use the new name, merging if both are
|
|
@@ -419,19 +430,19 @@ def _transform_legacy(profile_dict):
|
|
|
419
430
|
|
|
420
431
|
|
|
421
432
|
def _load_profile(
|
|
422
|
-
name_or_path,
|
|
423
|
-
profile_path,
|
|
424
|
-
shorthands_found=None,
|
|
425
|
-
already_loaded=None,
|
|
426
|
-
allow_shorthand=True,
|
|
427
|
-
forced_inherits=None,
|
|
428
|
-
):
|
|
433
|
+
name_or_path: Union[str, Path],
|
|
434
|
+
profile_path: list[Path],
|
|
435
|
+
shorthands_found: Optional[set[str]] = None,
|
|
436
|
+
already_loaded: Optional[list[Union[str, Path]]] = None,
|
|
437
|
+
allow_shorthand: bool = True,
|
|
438
|
+
forced_inherits: Optional[list[str]] = None,
|
|
439
|
+
) -> tuple[dict[Union[str, Path], Any], list[str], set[str]]:
|
|
429
440
|
# recursively get the contents of the basic profile and those it inherits from
|
|
430
441
|
base_contents = _load_content(name_or_path, profile_path)
|
|
431
442
|
|
|
432
443
|
base_contents = _transform_legacy(base_contents)
|
|
433
444
|
|
|
434
|
-
inherit_order = [name_or_path]
|
|
445
|
+
inherit_order = [str(name_or_path)]
|
|
435
446
|
shorthands_found = shorthands_found or set()
|
|
436
447
|
|
|
437
448
|
already_loaded = already_loaded or []
|
|
@@ -448,7 +459,7 @@ def _load_profile(
|
|
|
448
459
|
inherits += extra_inherits
|
|
449
460
|
shorthands_found |= extra_shorthands
|
|
450
461
|
|
|
451
|
-
contents_dict = {name_or_path: base_contents}
|
|
462
|
+
contents_dict: dict[Union[str, Path], Any] = {name_or_path: base_contents}
|
|
452
463
|
|
|
453
464
|
for inherit_profile in inherits:
|
|
454
465
|
if inherit_profile in already_loaded:
|
|
@@ -470,4 +481,4 @@ def _load_profile(
|
|
|
470
481
|
# note: a new list is returned here rather than simply using inherit_order to give astroid a
|
|
471
482
|
# clue about the type of the returned object, as otherwise it can recurse infinitely and crash,
|
|
472
483
|
# this meaning that prospector does not run on prospector cleanly!
|
|
473
|
-
return contents_dict,
|
|
484
|
+
return contents_dict, inherit_order, shorthands_found
|
prospector/run.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import argparse
|
|
1
2
|
import codecs
|
|
2
3
|
import os.path
|
|
3
4
|
import sys
|
|
4
5
|
import warnings
|
|
5
6
|
from datetime import datetime
|
|
6
7
|
from pathlib import Path
|
|
7
|
-
from typing import TextIO
|
|
8
|
+
from typing import Any, Optional, TextIO
|
|
8
9
|
|
|
9
10
|
from prospector import blender, postfilter, tools
|
|
10
11
|
from prospector.compat import is_relative_to
|
|
@@ -19,12 +20,12 @@ from prospector.tools.utils import CaptureOutput
|
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class Prospector:
|
|
22
|
-
def __init__(self, config: ProspectorConfig):
|
|
23
|
+
def __init__(self, config: ProspectorConfig) -> None:
|
|
23
24
|
self.config = config
|
|
24
|
-
self.summary = None
|
|
25
|
+
self.summary: Optional[dict[str, Any]] = None
|
|
25
26
|
self.messages = config.messages
|
|
26
27
|
|
|
27
|
-
def process_messages(self, found_files, messages):
|
|
28
|
+
def process_messages(self, found_files: FileFinder, messages: list[Message]) -> list[Message]:
|
|
28
29
|
if self.config.blending:
|
|
29
30
|
messages = blender.blend(messages)
|
|
30
31
|
|
|
@@ -38,10 +39,10 @@ class Prospector:
|
|
|
38
39
|
|
|
39
40
|
return postfilter.filter_messages(found_files.python_modules, messages)
|
|
40
41
|
|
|
41
|
-
def execute(self):
|
|
42
|
+
def execute(self) -> None:
|
|
42
43
|
deprecated_names = self.config.replace_deprecated_tool_names()
|
|
43
44
|
|
|
44
|
-
summary = {
|
|
45
|
+
summary: dict[str, Any] = {
|
|
45
46
|
"started": datetime.now(),
|
|
46
47
|
}
|
|
47
48
|
summary.update(self.config.get_summary_information())
|
|
@@ -67,7 +68,7 @@ class Prospector:
|
|
|
67
68
|
message=msg,
|
|
68
69
|
)
|
|
69
70
|
messages.append(message)
|
|
70
|
-
warnings.warn(msg, category=DeprecationWarning)
|
|
71
|
+
warnings.warn(msg, category=DeprecationWarning, stacklevel=0)
|
|
71
72
|
|
|
72
73
|
# Run the tools
|
|
73
74
|
for tool in self.config.get_tools(found_files):
|
|
@@ -121,28 +122,29 @@ class Prospector:
|
|
|
121
122
|
summary["completed"] = datetime.now()
|
|
122
123
|
|
|
123
124
|
delta = summary["completed"] - summary["started"]
|
|
124
|
-
summary["time_taken"] = "
|
|
125
|
+
summary["time_taken"] = f"{delta.total_seconds():0.2f}"
|
|
125
126
|
|
|
126
127
|
external_config = []
|
|
127
|
-
for
|
|
128
|
+
for tool_name, configured_by in self.config.configured_by.items():
|
|
128
129
|
if configured_by is not None:
|
|
129
|
-
external_config.append((
|
|
130
|
+
external_config.append((tool_name, configured_by))
|
|
130
131
|
if len(external_config) > 0:
|
|
131
|
-
summary["external_config"] = ", ".join(["
|
|
132
|
+
summary["external_config"] = ", ".join(["{}: {}".format(*info) for info in external_config])
|
|
132
133
|
|
|
133
134
|
self.summary = summary
|
|
134
135
|
self.messages = self.messages + messages
|
|
135
136
|
|
|
136
|
-
def get_summary(self):
|
|
137
|
+
def get_summary(self) -> Optional[dict[str, Any]]:
|
|
137
138
|
return self.summary
|
|
138
139
|
|
|
139
|
-
def get_messages(self):
|
|
140
|
+
def get_messages(self) -> list[Message]:
|
|
140
141
|
return self.messages
|
|
141
142
|
|
|
142
|
-
def print_messages(self):
|
|
143
|
+
def print_messages(self) -> None:
|
|
143
144
|
output_reports = self.config.get_output_report()
|
|
144
145
|
|
|
145
146
|
for report in output_reports:
|
|
147
|
+
assert self.summary is not None
|
|
146
148
|
output_format, output_files = report
|
|
147
149
|
self.summary["formatter"] = output_format
|
|
148
150
|
|
|
@@ -161,7 +163,7 @@ class Prospector:
|
|
|
161
163
|
with codecs.open(output_file, "w+") as target:
|
|
162
164
|
self.write_to(formatter, target)
|
|
163
165
|
|
|
164
|
-
def write_to(self, formatter: Formatter, target: TextIO):
|
|
166
|
+
def write_to(self, formatter: Formatter, target: TextIO) -> None:
|
|
165
167
|
# Produce the output
|
|
166
168
|
target.write(
|
|
167
169
|
formatter.render(
|
|
@@ -173,7 +175,7 @@ class Prospector:
|
|
|
173
175
|
target.write("\n")
|
|
174
176
|
|
|
175
177
|
|
|
176
|
-
def get_parser():
|
|
178
|
+
def get_parser() -> argparse.ArgumentParser:
|
|
177
179
|
"""
|
|
178
180
|
This is a helper method to return an argparse parser, to
|
|
179
181
|
be used with the Sphinx argparse plugin for documentation.
|
|
@@ -183,13 +185,13 @@ def get_parser():
|
|
|
183
185
|
return source.build_parser(manager.settings, None)
|
|
184
186
|
|
|
185
187
|
|
|
186
|
-
def main():
|
|
188
|
+
def main() -> None:
|
|
187
189
|
# Get our configuration
|
|
188
190
|
config = ProspectorConfig()
|
|
189
191
|
|
|
190
192
|
paths = config.paths
|
|
191
193
|
if len(paths) > 1 and not all(os.path.isfile(path) for path in paths):
|
|
192
|
-
sys.stderr.write("\nIn multi-path mode, all inputs must be files,
|
|
194
|
+
sys.stderr.write("\nIn multi-path mode, all inputs must be files, not directories.\n\n")
|
|
193
195
|
get_parser().print_usage()
|
|
194
196
|
sys.exit(2)
|
|
195
197
|
|
prospector/suppression.py
CHANGED
|
@@ -19,11 +19,12 @@ in the file:
|
|
|
19
19
|
This module's job is to attempt to collect all of these methods into
|
|
20
20
|
a single coherent list of error suppression locations.
|
|
21
21
|
"""
|
|
22
|
+
|
|
22
23
|
import re
|
|
23
24
|
import warnings
|
|
24
25
|
from collections import defaultdict
|
|
25
26
|
from pathlib import Path
|
|
26
|
-
from typing import
|
|
27
|
+
from typing import Optional
|
|
27
28
|
|
|
28
29
|
from prospector import encoding
|
|
29
30
|
from prospector.exceptions import FatalProspectorException
|
|
@@ -34,7 +35,7 @@ _PEP8_IGNORE_LINE = re.compile(r"#\s+noqa", re.IGNORECASE)
|
|
|
34
35
|
_PYLINT_SUPPRESSED_MESSAGE = re.compile(r"^Suppressed \'([a-z0-9-]+)\' \(from line \d+\)$")
|
|
35
36
|
|
|
36
37
|
|
|
37
|
-
def get_noqa_suppressions(file_contents):
|
|
38
|
+
def get_noqa_suppressions(file_contents: list[str]) -> tuple[bool, set[int]]:
|
|
38
39
|
"""
|
|
39
40
|
Finds all pep8/flake8 suppression messages
|
|
40
41
|
|
|
@@ -63,9 +64,11 @@ _PYLINT_EQUIVALENTS = {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
|
|
66
|
-
def _parse_pylint_informational(
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
def _parse_pylint_informational(
|
|
68
|
+
messages: list[Message],
|
|
69
|
+
) -> tuple[set[Optional[Path]], dict[Optional[Path], dict[int, list[str]]]]:
|
|
70
|
+
ignore_files: set[Optional[Path]] = set()
|
|
71
|
+
ignore_messages: dict[Optional[Path], dict[int, list[str]]] = defaultdict(lambda: defaultdict(list))
|
|
69
72
|
|
|
70
73
|
for message in messages:
|
|
71
74
|
if message.source == "pylint":
|
|
@@ -77,21 +80,24 @@ def _parse_pylint_informational(messages: List[Message]):
|
|
|
77
80
|
raise FatalProspectorException(f"Could not parsed suppressed message from {message.message}")
|
|
78
81
|
suppressed_code = match.group(1)
|
|
79
82
|
line_dict = ignore_messages[message.location.path]
|
|
83
|
+
assert message.location.line is not None
|
|
80
84
|
line_dict[message.location.line].append(suppressed_code)
|
|
81
85
|
elif message.code == "file-ignored":
|
|
82
86
|
ignore_files.add(message.location.path)
|
|
83
87
|
return ignore_files, ignore_messages
|
|
84
88
|
|
|
85
89
|
|
|
86
|
-
def get_suppressions(
|
|
90
|
+
def get_suppressions(
|
|
91
|
+
filepaths: list[Path], messages: list[Message]
|
|
92
|
+
) -> tuple[set[Optional[Path]], dict[Path, set[int]], dict[Optional[Path], dict[int, set[tuple[str, str]]]]]:
|
|
87
93
|
"""
|
|
88
94
|
Given every message which was emitted by the tools, and the
|
|
89
95
|
list of files to inspect, create a list of files to ignore,
|
|
90
96
|
and a map of filepath -> line-number -> codes to ignore
|
|
91
97
|
"""
|
|
92
|
-
paths_to_ignore = set()
|
|
93
|
-
lines_to_ignore: dict = defaultdict(set)
|
|
94
|
-
messages_to_ignore: dict = defaultdict(lambda: defaultdict(set))
|
|
98
|
+
paths_to_ignore: set[Optional[Path]] = set()
|
|
99
|
+
lines_to_ignore: dict[Path, set[int]] = defaultdict(set)
|
|
100
|
+
messages_to_ignore: dict[Optional[Path], dict[int, set[tuple[str, str]]]] = defaultdict(lambda: defaultdict(set))
|
|
95
101
|
|
|
96
102
|
# first deal with 'noqa' style messages
|
|
97
103
|
for filepath in filepaths:
|
|
@@ -99,7 +105,7 @@ def get_suppressions(filepaths: List[Path], messages):
|
|
|
99
105
|
file_contents = encoding.read_py_file(filepath).split("\n")
|
|
100
106
|
except encoding.CouldNotHandleEncoding as err:
|
|
101
107
|
# TODO: this output will break output formats such as JSON
|
|
102
|
-
warnings.warn(f"{err.path}: {err.__cause__}", ImportWarning)
|
|
108
|
+
warnings.warn(f"{err.path}: {err.__cause__}", ImportWarning, stacklevel=2)
|
|
103
109
|
continue
|
|
104
110
|
|
|
105
111
|
ignore_file, ignore_lines = get_noqa_suppressions(file_contents)
|
|
@@ -110,12 +116,12 @@ def get_suppressions(filepaths: List[Path], messages):
|
|
|
110
116
|
# now figure out which messages were suppressed by pylint
|
|
111
117
|
pylint_ignore_files, pylint_ignore_messages = _parse_pylint_informational(messages)
|
|
112
118
|
paths_to_ignore |= pylint_ignore_files
|
|
113
|
-
for
|
|
119
|
+
for pylint_filepath, line in pylint_ignore_messages.items():
|
|
114
120
|
for line_number, codes in line.items():
|
|
115
121
|
for code in codes:
|
|
116
|
-
messages_to_ignore[
|
|
122
|
+
messages_to_ignore[pylint_filepath][line_number].add(("pylint", code))
|
|
117
123
|
if code in _PYLINT_EQUIVALENTS:
|
|
118
124
|
for equivalent in _PYLINT_EQUIVALENTS[code]:
|
|
119
|
-
messages_to_ignore[
|
|
125
|
+
messages_to_ignore[pylint_filepath][line_number].add(equivalent)
|
|
120
126
|
|
|
121
127
|
return paths_to_ignore, lines_to_ignore, messages_to_ignore
|
prospector/tools/__init__.py
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
import
|
|
1
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
2
2
|
|
|
3
3
|
from prospector.exceptions import FatalProspectorException
|
|
4
|
+
from prospector.finder import FileFinder
|
|
5
|
+
from prospector.message import Message
|
|
4
6
|
from prospector.tools.base import ToolBase
|
|
5
7
|
from prospector.tools.dodgy import DodgyTool
|
|
6
8
|
from prospector.tools.mccabe import McCabeTool
|
|
9
|
+
from prospector.tools.profile_validator import ProfileValidationTool # pylint: disable=cyclic-import
|
|
7
10
|
from prospector.tools.pycodestyle import PycodestyleTool
|
|
8
11
|
from prospector.tools.pydocstyle import PydocstyleTool
|
|
9
12
|
from prospector.tools.pyflakes import PyFlakesTool
|
|
10
13
|
from prospector.tools.pylint import PylintTool
|
|
11
14
|
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from prospector.config import ProspectorConfig
|
|
12
17
|
|
|
13
|
-
|
|
18
|
+
|
|
19
|
+
def _tool_not_available(name: str, install_option_name: str) -> type[ToolBase]:
|
|
14
20
|
class NotAvailableTool(ToolBase):
|
|
15
21
|
"""
|
|
16
22
|
Dummy tool class to return when a particular dependency is not found (such as mypy, or bandit)
|
|
@@ -18,10 +24,10 @@ def _tool_not_available(name, install_option_name):
|
|
|
18
24
|
if the user tries to run prospector and specifies using the tool at which point an error is raised.
|
|
19
25
|
"""
|
|
20
26
|
|
|
21
|
-
def configure(self, prospector_config, found_files):
|
|
27
|
+
def configure(self, prospector_config: "ProspectorConfig", found_files: FileFinder) -> None:
|
|
22
28
|
pass
|
|
23
29
|
|
|
24
|
-
def run(self, _):
|
|
30
|
+
def run(self, _: Any) -> list[Message]:
|
|
25
31
|
raise FatalProspectorException(
|
|
26
32
|
f"\nCannot run tool {name} as support was not installed.\n"
|
|
27
33
|
f"Please install by running 'pip install prospector[{install_option_name}]'\n\n"
|
|
@@ -30,7 +36,12 @@ def _tool_not_available(name, install_option_name):
|
|
|
30
36
|
return NotAvailableTool
|
|
31
37
|
|
|
32
38
|
|
|
33
|
-
def _optional_tool(
|
|
39
|
+
def _optional_tool(
|
|
40
|
+
name: str,
|
|
41
|
+
package_name: Optional[str] = None,
|
|
42
|
+
tool_class_name: Optional[str] = None,
|
|
43
|
+
install_option_name: Optional[str] = None,
|
|
44
|
+
) -> type[ToolBase]:
|
|
34
45
|
package_name = "prospector.tools.%s" % (package_name or name)
|
|
35
46
|
tool_class_name = tool_class_name or f"{name.title()}Tool"
|
|
36
47
|
install_option_name = install_option_name or f"with_{name}"
|
|
@@ -45,25 +56,20 @@ def _optional_tool(name, package_name=None, tool_class_name=None, install_option
|
|
|
45
56
|
return tool_class
|
|
46
57
|
|
|
47
58
|
|
|
48
|
-
|
|
49
|
-
# bit of a hack to avoid a cyclic import...
|
|
50
|
-
mdl = importlib.import_module("prospector.tools.profile_validator")
|
|
51
|
-
return mdl.ProfileValidationTool(*args, **kwargs)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
TOOLS = {
|
|
59
|
+
TOOLS: dict[str, type[ToolBase]] = {
|
|
55
60
|
"dodgy": DodgyTool,
|
|
56
61
|
"mccabe": McCabeTool,
|
|
57
62
|
"pyflakes": PyFlakesTool,
|
|
58
63
|
"pycodestyle": PycodestyleTool,
|
|
59
64
|
"pylint": PylintTool,
|
|
60
65
|
"pydocstyle": PydocstyleTool,
|
|
61
|
-
"profile-validator":
|
|
66
|
+
"profile-validator": ProfileValidationTool,
|
|
62
67
|
"vulture": _optional_tool("vulture"),
|
|
63
68
|
"pyroma": _optional_tool("pyroma"),
|
|
64
69
|
"pyright": _optional_tool("pyright"),
|
|
65
70
|
"mypy": _optional_tool("mypy"),
|
|
66
71
|
"bandit": _optional_tool("bandit"),
|
|
72
|
+
"ruff": _optional_tool("ruff"),
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
|