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.
Files changed (50) hide show
  1. prospector/autodetect.py +10 -9
  2. prospector/blender.py +18 -12
  3. prospector/config/__init__.py +66 -49
  4. prospector/config/configuration.py +17 -14
  5. prospector/config/datatype.py +1 -1
  6. prospector/encoding.py +2 -2
  7. prospector/exceptions.py +1 -6
  8. prospector/finder.py +9 -8
  9. prospector/formatters/__init__.py +1 -1
  10. prospector/formatters/base.py +17 -11
  11. prospector/formatters/base_summary.py +43 -0
  12. prospector/formatters/emacs.py +5 -5
  13. prospector/formatters/grouped.py +9 -7
  14. prospector/formatters/json.py +3 -2
  15. prospector/formatters/pylint.py +41 -9
  16. prospector/formatters/text.py +10 -58
  17. prospector/formatters/vscode.py +17 -7
  18. prospector/formatters/xunit.py +6 -7
  19. prospector/formatters/yaml.py +4 -2
  20. prospector/message.py +32 -14
  21. prospector/pathutils.py +3 -10
  22. prospector/postfilter.py +9 -8
  23. prospector/profiles/exceptions.py +14 -11
  24. prospector/profiles/profile.py +70 -59
  25. prospector/run.py +20 -18
  26. prospector/suppression.py +19 -13
  27. prospector/tools/__init__.py +19 -13
  28. prospector/tools/bandit/__init__.py +27 -15
  29. prospector/tools/base.py +11 -3
  30. prospector/tools/dodgy/__init__.py +7 -3
  31. prospector/tools/mccabe/__init__.py +13 -6
  32. prospector/tools/mypy/__init__.py +44 -76
  33. prospector/tools/profile_validator/__init__.py +24 -15
  34. prospector/tools/pycodestyle/__init__.py +22 -15
  35. prospector/tools/pydocstyle/__init__.py +12 -6
  36. prospector/tools/pyflakes/__init__.py +35 -19
  37. prospector/tools/pylint/__init__.py +57 -31
  38. prospector/tools/pylint/collector.py +3 -5
  39. prospector/tools/pylint/linter.py +19 -14
  40. prospector/tools/pyright/__init__.py +18 -7
  41. prospector/tools/pyroma/__init__.py +10 -6
  42. prospector/tools/ruff/__init__.py +84 -0
  43. prospector/tools/utils.py +25 -19
  44. prospector/tools/vulture/__init__.py +25 -15
  45. {prospector-1.10.3.dist-info → prospector-1.13.2.dist-info}/METADATA +10 -11
  46. prospector-1.13.2.dist-info/RECORD +71 -0
  47. prospector-1.10.3.dist-info/RECORD +0 -69
  48. {prospector-1.10.3.dist-info → prospector-1.13.2.dist-info}/LICENSE +0 -0
  49. {prospector-1.10.3.dist-info → prospector-1.13.2.dist-info}/WHEEL +0 -0
  50. {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 "{}\n on line {} : char {}".format(
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 %s - it is not valid YAML" % self.filepath
30
+ def __repr__(self) -> str:
31
+ return f"Could not parse profile found at {self.filepath} - it is not valid YAML"
@@ -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, Dict, List, Optional, Tuple, Union
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: Dict[str, Any], inherit_order: List[str]):
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: Dict[str, Any] = {"disable": [], "enable": [], "run": None, "options": {}}
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 is_tool_enabled(self, name):
61
- enabled = getattr(self, name).get("run")
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: List[Path],
104
+ profile_path: list[Path],
100
105
  allow_shorthand: bool = True,
101
- forced_inherits: Optional[List[str]] = None,
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]}.yaml"]
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 = dict(base.items())
192
- out.update(dict(priority.items()))
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 = dict(base.items())
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_%s" % strictness), True
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(profile_dict, already_inherits, shorthands_found):
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(name, profile_path, data, inherit_list, allow_shorthand=False):
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: List[Path],
347
+ profile_path: list[Path],
337
348
  allow_shorthand: bool = True,
338
- forced_inherits: List[str] = None,
339
- ) -> Tuple[Dict[str, Any], List[str]]:
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, list(inherit_order), shorthands_found
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"] = "%0.2f" % delta.total_seconds()
125
+ summary["time_taken"] = f"{delta.total_seconds():0.2f}"
125
126
 
126
127
  external_config = []
127
- for tool, configured_by in self.config.configured_by.items():
128
+ for tool_name, configured_by in self.config.configured_by.items():
128
129
  if configured_by is not None:
129
- external_config.append((tool, configured_by))
130
+ external_config.append((tool_name, configured_by))
130
131
  if len(external_config) > 0:
131
- summary["external_config"] = ", ".join(["%s: %s" % info for info in external_config])
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, " "not directories.\n\n")
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 List
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(messages: List[Message]):
67
- ignore_files = set()
68
- ignore_messages: dict = defaultdict(lambda: defaultdict(list))
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(filepaths: List[Path], messages):
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 filepath, line in pylint_ignore_messages.items():
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[filepath][line_number].add(("pylint", code))
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[filepath][line_number].add(equivalent)
125
+ messages_to_ignore[pylint_filepath][line_number].add(equivalent)
120
126
 
121
127
  return paths_to_ignore, lines_to_ignore, messages_to_ignore
@@ -1,16 +1,22 @@
1
- import importlib
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
- def _tool_not_available(name, install_option_name):
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(name, package_name=None, tool_class_name=None, install_option_name=None):
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
- def _profile_validator_tool(*args, **kwargs):
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": _profile_validator_tool,
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