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,15 +1,21 @@
1
+ from typing import TYPE_CHECKING, Any, Optional
2
+
1
3
  from pyflakes.api import checkPath
4
+ from pyflakes.messages import Message as FlakeMessage
2
5
  from pyflakes.reporter import Reporter
3
6
 
7
+ from prospector.finder import FileFinder
4
8
  from prospector.message import Location, Message
5
9
  from prospector.tools.base import ToolBase
6
10
 
11
+ if TYPE_CHECKING:
12
+ from prospector.config import ProspectorConfig
7
13
  __all__ = ("PyFlakesTool",)
8
14
 
9
15
 
10
16
  # Prospector uses the same pyflakes codes as flake8 defines,
11
17
  # see https://flake8.pycqa.org/en/latest/user/error-codes.html
12
- # and https://gitlab.com/pycqa/flake8/-/blob/e817c63a/src/flake8/plugins/pyflakes.py
18
+ # and https://github.com/PyCQA/flake8/blob/e817c63a/src/flake8/plugins/pyflakes.py
13
19
  _MESSAGE_CODES = {
14
20
  "UnusedImport": "F401",
15
21
  "ImportShadowedByLoopVar": "F402",
@@ -81,13 +87,22 @@ LEGACY_CODE_MAP = {
81
87
 
82
88
 
83
89
  class ProspectorReporter(Reporter):
84
- def __init__(self, ignore=None):
90
+ def __init__(self, ignore: Optional[list[str]] = None) -> None:
85
91
  super().__init__(None, None)
86
- self._messages = []
92
+ self._messages: list[Message] = []
87
93
  self.ignore = ignore or ()
88
94
 
89
- # pylint: disable=too-many-arguments
90
- def record_message(self, filename=None, line=None, character=None, code=None, message=None):
95
+ def record_message( # pylint: disable=too-many-arguments
96
+ self,
97
+ filename: str,
98
+ line: Optional[int] = None,
99
+ character: Optional[int] = None,
100
+ code: Optional[str] = None,
101
+ message: Optional[str] = None,
102
+ ) -> None:
103
+ assert message is not None
104
+ assert code is not None
105
+
91
106
  code = code or "F999"
92
107
  if code in self.ignore:
93
108
  return
@@ -99,15 +114,16 @@ class ProspectorReporter(Reporter):
99
114
  line=line,
100
115
  character=character,
101
116
  )
102
- message = Message(
103
- source="pyflakes",
104
- code=code,
105
- location=location,
106
- message=message,
117
+ self._messages.append(
118
+ Message(
119
+ source="pyflakes",
120
+ code=code,
121
+ location=location,
122
+ message=message,
123
+ )
107
124
  )
108
- self._messages.append(message)
109
125
 
110
- def unexpectedError(self, filename, msg): # noqa
126
+ def unexpectedError(self, filename: str, msg: str) -> None:
111
127
  self.record_message(
112
128
  filename=filename,
113
129
  code="F999",
@@ -115,7 +131,7 @@ class ProspectorReporter(Reporter):
115
131
  )
116
132
 
117
133
  # pylint: disable=too-many-arguments
118
- def syntaxError(self, filename, msg, lineno, offset, text): # noqa
134
+ def syntaxError(self, filename: str, msg: str, lineno: int, offset: int, text: str) -> None:
119
135
  self.record_message(
120
136
  filename=filename,
121
137
  line=lineno,
@@ -124,7 +140,7 @@ class ProspectorReporter(Reporter):
124
140
  message=msg,
125
141
  )
126
142
 
127
- def flake(self, message):
143
+ def flake(self, message: FlakeMessage) -> None:
128
144
  code = _MESSAGE_CODES.get(message.__class__.__name__, "F999")
129
145
 
130
146
  self.record_message(
@@ -135,21 +151,21 @@ class ProspectorReporter(Reporter):
135
151
  message=message.message % message.message_args,
136
152
  )
137
153
 
138
- def get_messages(self):
154
+ def get_messages(self) -> list[Message]:
139
155
  return self._messages
140
156
 
141
157
 
142
158
  class PyFlakesTool(ToolBase):
143
- def __init__(self, *args, **kwargs):
159
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
144
160
  super().__init__(*args, **kwargs)
145
- self.ignore_codes = ()
161
+ self.ignore_codes: list[str] = []
146
162
 
147
- def configure(self, prospector_config, _):
163
+ def configure(self, prospector_config: "ProspectorConfig", _: Any) -> None:
148
164
  ignores = prospector_config.get_disabled_messages("pyflakes")
149
165
  # convert old style to new
150
166
  self.ignore_codes = [LEGACY_CODE_MAP.get(code, code) for code in ignores]
151
167
 
152
- def run(self, found_files):
168
+ def run(self, found_files: FileFinder) -> list[Message]:
153
169
  reporter = ProspectorReporter(ignore=self.ignore_codes)
154
170
  for filepath in found_files.python_modules:
155
171
  checkPath(str(filepath.absolute()), reporter)
@@ -2,10 +2,11 @@ import os
2
2
  import re
3
3
  import sys
4
4
  from collections import defaultdict
5
+ from collections.abc import Iterable
5
6
  from pathlib import Path
6
- from typing import List
7
+ from typing import TYPE_CHECKING, Any, Optional, Union
7
8
 
8
- from pylint.config import find_pylintrc
9
+ from pylint.config import find_default_config_files
9
10
  from pylint.exceptions import UnknownMessageError
10
11
  from pylint.lint.run import _cpu_count
11
12
 
@@ -15,6 +16,9 @@ from prospector.tools.base import ToolBase
15
16
  from prospector.tools.pylint.collector import Collector
16
17
  from prospector.tools.pylint.linter import ProspectorLinter
17
18
 
19
+ if TYPE_CHECKING:
20
+ from prospector.config import ProspectorConfig
21
+
18
22
  _UNUSED_WILDCARD_IMPORT_RE = re.compile(r"^Unused import(\(s\))? (.*) from wildcard import")
19
23
 
20
24
 
@@ -27,12 +31,13 @@ class PylintTool(ToolBase):
27
31
  # be functions (they don't use the 'self' argument) but that would
28
32
  # make this module/class a bit ugly.
29
33
 
30
- def __init__(self):
31
- self._args = None
32
- self._collector = self._linter = None
33
- self._orig_sys_path = []
34
+ def __init__(self) -> None:
35
+ self._args: Any = None
36
+ self._collector: Optional[Collector] = None
37
+ self._linter: Optional[ProspectorLinter] = None
38
+ self._orig_sys_path: list[str] = []
34
39
 
35
- def _prospector_configure(self, prospector_config, linter: ProspectorLinter):
40
+ def _prospector_configure(self, prospector_config: "ProspectorConfig", linter: ProspectorLinter) -> list[Message]:
36
41
  errors = []
37
42
 
38
43
  if "django" in prospector_config.libraries:
@@ -43,14 +48,14 @@ class PylintTool(ToolBase):
43
48
  linter.load_plugin_modules(["pylint_flask"])
44
49
 
45
50
  profile_path = os.path.join(prospector_config.workdir, prospector_config.profile.name)
46
- for plugin in prospector_config.profile.pylint.get("load-plugins", []):
51
+ for plugin in prospector_config.profile.pylint.get("load-plugins", []): # type: ignore[attr-defined]
47
52
  try:
48
53
  linter.load_plugin_modules([plugin])
49
54
  except ImportError:
50
55
  errors.append(self._error_message(profile_path, f"Could not load plugin {plugin}"))
51
56
 
52
57
  for msg_id in prospector_config.get_disabled_messages("pylint"):
53
- try:
58
+ try: # noqa: SIM105
54
59
  linter.disable(msg_id)
55
60
  except UnknownMessageError:
56
61
  # If the msg_id doesn't exist in PyLint any more,
@@ -64,7 +69,9 @@ class PylintTool(ToolBase):
64
69
  continue
65
70
  for option in checker.options:
66
71
  if option[0] in options:
67
- checker.set_option(option[0], options[option[0]])
72
+ checker._arguments_manager.set_option( # pylint: disable=protected-access
73
+ option[0], options[option[0]]
74
+ )
68
75
 
69
76
  # The warnings about disabling warnings are useful for figuring out
70
77
  # with other tools to suppress messages from. For example, an unused
@@ -82,16 +89,17 @@ class PylintTool(ToolBase):
82
89
  if not hasattr(checker, "options"):
83
90
  continue
84
91
  for option in checker.options:
85
- if max_line_length is not None:
86
- if option[0] == "max-line-length":
87
- checker.set_option("max-line-length", max_line_length)
92
+ if max_line_length is not None and option[0] == "max-line-length":
93
+ checker._arguments_manager.set_option( # pylint: disable=protected-access
94
+ "max-line-length", max_line_length
95
+ )
88
96
  return errors
89
97
 
90
- def _error_message(self, filepath, message):
98
+ def _error_message(self, filepath: Union[str, Path], message: str) -> Message:
91
99
  location = Location(filepath, None, None, 0, 0)
92
100
  return Message("prospector", "config-problem", location, message)
93
101
 
94
- def _pylintrc_configure(self, pylintrc, linter):
102
+ def _pylintrc_configure(self, pylintrc: Union[str, Path], linter: ProspectorLinter) -> list[Message]:
95
103
  errors = []
96
104
  are_plugins_loaded = linter.config_from_file(pylintrc)
97
105
  if not are_plugins_loaded and hasattr(linter.config, "load_plugins"):
@@ -102,7 +110,9 @@ class PylintTool(ToolBase):
102
110
  errors.append(self._error_message(pylintrc, f"Could not load plugin {plugin}"))
103
111
  return errors
104
112
 
105
- def configure(self, prospector_config, found_files: FileFinder):
113
+ def configure(
114
+ self, prospector_config: "ProspectorConfig", found_files: FileFinder
115
+ ) -> Optional[tuple[Optional[Union[str, Path]], Optional[Iterable[Message]]]]:
106
116
  extra_sys_path = found_files.make_syspath()
107
117
  check_paths = self._get_pylint_check_paths(found_files)
108
118
 
@@ -127,13 +137,13 @@ class PylintTool(ToolBase):
127
137
  self._linter = linter
128
138
  return configured_by, config_messages
129
139
 
130
- def _set_path_finder(self, extra_sys_path: List[Path], pylint_options):
140
+ def _set_path_finder(self, extra_sys_path: list[Path], pylint_options: dict[str, Any]) -> None:
131
141
  # insert the target path into the system path to get correct behaviour
132
142
  self._orig_sys_path = sys.path
133
143
  if not pylint_options.get("use_pylint_default_path_finder"):
134
144
  sys.path = sys.path + [str(path.absolute()) for path in extra_sys_path]
135
145
 
136
- def _get_pylint_check_paths(self, found_files: FileFinder) -> List[Path]:
146
+ def _get_pylint_check_paths(self, found_files: FileFinder) -> list[Path]:
137
147
  # create a list of packages, but don't include packages which are
138
148
  # subpackages of others as checks will be duplicated
139
149
  check_paths = set()
@@ -165,19 +175,30 @@ class PylintTool(ToolBase):
165
175
  return sorted(check_paths)
166
176
 
167
177
  def _get_pylint_configuration(
168
- self, check_paths: List[Path], linter: ProspectorLinter, prospector_config, pylint_options
169
- ):
170
- self._args = linter.load_command_line_configuration(str(path) for path in check_paths)
178
+ self,
179
+ check_paths: list[Path],
180
+ linter: ProspectorLinter,
181
+ prospector_config: "ProspectorConfig",
182
+ pylint_options: dict[str, Any],
183
+ ) -> tuple[list[Message], Optional[Union[Path, str]]]:
184
+ self._args = check_paths
171
185
  linter.load_default_plugins()
172
186
 
173
- config_messages = self._prospector_configure(prospector_config, linter)
174
- configured_by = None
187
+ config_messages: list[Message] = self._prospector_configure(prospector_config, linter)
188
+ configured_by: Optional[Union[str, Path]] = None
175
189
 
176
190
  if prospector_config.use_external_config("pylint"):
177
- # try to find a .pylintrc
178
- pylintrc = pylint_options.get("config_file")
191
+ # Try to find a .pylintrc
192
+ pylintrc: Optional[Union[str, Path]] = pylint_options.get("config_file")
179
193
  external_config = prospector_config.external_config_location("pylint")
180
- pylintrc = pylintrc or external_config or find_pylintrc()
194
+
195
+ pylintrc = pylintrc or external_config
196
+
197
+ if pylintrc is None:
198
+ for p in find_default_config_files():
199
+ pylintrc = str(p)
200
+ break
201
+
181
202
  if pylintrc is None: # nothing explicitly configured
182
203
  for possible in (".pylintrc", "pylintrc", "pyproject.toml", "setup.cfg"):
183
204
  pylintrc_path = os.path.join(prospector_config.workdir, possible)
@@ -194,7 +215,7 @@ class PylintTool(ToolBase):
194
215
 
195
216
  return config_messages, configured_by
196
217
 
197
- def _combine_w0614(self, messages):
218
+ def _combine_w0614(self, messages: list[Message]) -> list[Message]:
198
219
  """
199
220
  For the "unused import from wildcard import" messages,
200
221
  we want to combine all warnings about the same line into
@@ -212,15 +233,17 @@ class PylintTool(ToolBase):
212
233
  for location, message_list in by_loc.items():
213
234
  names = []
214
235
  for msg in message_list:
215
- names.append(_UNUSED_WILDCARD_IMPORT_RE.match(msg.message).group(1))
236
+ match_ = _UNUSED_WILDCARD_IMPORT_RE.match(msg.message)
237
+ assert match_ is not None
238
+ names.append(match_.group(1))
216
239
 
217
- msgtxt = "Unused imports from wildcard import: %s" % ", ".join(names)
240
+ msgtxt = "Unused imports from wildcard import: {}".format(", ".join(names))
218
241
  combined_message = Message("pylint", "unused-wildcard-import", location, msgtxt)
219
242
  out.append(combined_message)
220
243
 
221
244
  return out
222
245
 
223
- def combine(self, messages):
246
+ def combine(self, messages: list[Message]) -> list[Message]:
224
247
  """
225
248
  Combine repeated messages.
226
249
 
@@ -234,7 +257,10 @@ class PylintTool(ToolBase):
234
257
  combined = self._combine_w0614(messages)
235
258
  return sorted(combined)
236
259
 
237
- def run(self, found_files) -> List[Message]:
260
+ def run(self, found_files: FileFinder) -> list[Message]:
261
+ assert self._collector is not None
262
+ assert self._linter is not None
263
+
238
264
  self._linter.check(self._args)
239
265
  sys.path = self._orig_sys_path
240
266
 
@@ -3,6 +3,7 @@ from typing import List
3
3
 
4
4
  from pylint.exceptions import UnknownMessageError
5
5
  from pylint.message import Message as PylintMessage
6
+ from pylint.message import MessageDefinitionStore
6
7
  from pylint.reporters import BaseReporter
7
8
 
8
9
  from prospector.message import Location, Message
@@ -11,10 +12,10 @@ from prospector.message import Location, Message
11
12
  class Collector(BaseReporter):
12
13
  name = "collector"
13
14
 
14
- def __init__(self, message_store):
15
+ def __init__(self, message_store: MessageDefinitionStore) -> None:
15
16
  BaseReporter.__init__(self, output=StringIO())
16
17
  self._message_store = message_store
17
- self._messages = []
18
+ self._messages: list[Message] = []
18
19
 
19
20
  def handle_message(self, msg: PylintMessage) -> None:
20
21
  loc = Location(msg.abspath, msg.module, msg.obj, msg.line, msg.column)
@@ -34,8 +35,5 @@ class Collector(BaseReporter):
34
35
  message = Message("pylint", msg_symbol, loc, msg.msg)
35
36
  self._messages.append(message)
36
37
 
37
- def _display(self, layout) -> None:
38
- pass
39
-
40
38
  def get_messages(self) -> List[Message]:
41
39
  return self._messages
@@ -1,34 +1,39 @@
1
+ from collections.abc import Iterable
1
2
  from pathlib import Path
3
+ from typing import Any, Optional, Union
2
4
 
3
5
  from packaging import version as packaging_version
4
6
  from pylint import version as pylint_version
7
+ from pylint.config.config_initialization import _config_initialization
5
8
  from pylint.lint import PyLinter
6
- from pylint.utils import _splitstrip
9
+
10
+ from prospector.finder import FileFinder
11
+
12
+
13
+ class UnrecognizedOptions(Exception):
14
+ """Raised when an unrecognized option is found in the Pylint configuration."""
7
15
 
8
16
 
9
17
  class ProspectorLinter(PyLinter):
10
- def __init__(self, found_files, *args, **kwargs):
18
+ def __init__(self, found_files: FileFinder, *args: Any, **kwargs: Any) -> None:
11
19
  self._files = found_files
12
20
  # set up the standard PyLint linter
13
21
  PyLinter.__init__(self, *args, **kwargs)
14
22
 
15
- def config_from_file(self, config_file=None):
16
- """Will return `True` if plugins have been loaded. For pylint>=1.5. Else `False`."""
17
- self.read_config_file(config_file)
18
- if self.cfgfile_parser.has_option("MASTER", "load-plugins"):
19
- plugins = _splitstrip(self.cfgfile_parser.get("MASTER", "load-plugins"))
20
- self.load_plugin_modules(plugins)
21
- self.load_config_file()
23
+ # Largely inspired by https://github.com/pylint-dev/pylint/blob/main/pylint/config/config_initialization.py#L26
24
+ def config_from_file(self, config_file: Optional[Union[str, Path]] = None) -> bool:
25
+ """Initialize the configuration from a file."""
26
+ _config_initialization(self, [], config_file=config_file)
22
27
  return True
23
28
 
24
- def _expand_files(self, modules):
25
- expanded = super()._expand_files(modules)
26
- filtered = {}
29
+ def _expand_files(self, files_or_modules: list[str]) -> Union[Iterable[Any], dict[str, Any]]:
30
+ expanded = super()._expand_files(files_or_modules)
31
+ filtered: dict[str, Any] = {}
27
32
  # PyLinter._expand_files returns dict since 2.15.7.
28
33
  if packaging_version.parse(pylint_version) > packaging_version.parse("2.15.6"):
29
- for module in expanded:
34
+ for module, expanded_module in expanded.items():
30
35
  if not self._files.is_excluded(Path(module)):
31
- filtered[module] = expanded[module]
36
+ filtered[module] = expanded_module
32
37
  return filtered
33
38
  else:
34
39
  for module in expanded:
@@ -1,14 +1,21 @@
1
1
  import json
2
- import subprocess
2
+ import subprocess # nosec
3
+ from collections.abc import Iterable
4
+ from typing import TYPE_CHECKING, Any, Optional
3
5
 
4
6
  import pyright
5
7
 
8
+ from prospector.finder import FileFinder
6
9
  from prospector.message import Location, Message
7
10
  from prospector.tools import ToolBase
11
+ from prospector.tools.exceptions import BadToolConfig
12
+
13
+ if TYPE_CHECKING:
14
+ from prospector.config import ProspectorConfig
15
+
8
16
 
9
17
  __all__ = ("PyrightTool",)
10
18
 
11
- from prospector.tools.exceptions import BadToolConfig
12
19
 
13
20
  VALID_OPTIONS = [
14
21
  "level",
@@ -21,7 +28,7 @@ VALID_OPTIONS = [
21
28
  ]
22
29
 
23
30
 
24
- def format_messages(json_encoded):
31
+ def format_messages(json_encoded: str) -> list[Message]:
25
32
  json_decoded = json.loads(json_encoded)
26
33
  diagnostics = json_decoded.get("generalDiagnostics", [])
27
34
  messages = []
@@ -42,15 +49,17 @@ def format_messages(json_encoded):
42
49
 
43
50
 
44
51
  class PyrightTool(ToolBase):
45
- def __init__(self, *args, **kwargs):
52
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
46
53
  super().__init__(*args, **kwargs)
47
54
  self.checker = pyright
48
55
  self.options = ["--outputjson"]
49
56
 
50
- def configure(self, prospector_config, _):
57
+ def configure( # pylint: disable=useless-return
58
+ self, prospector_config: "ProspectorConfig", _: Any
59
+ ) -> Optional[tuple[str, Optional[Iterable[Message]]]]:
51
60
  options = prospector_config.tool_options("pyright")
52
61
 
53
- for option_key in options.keys():
62
+ for option_key in options:
54
63
  if option_key not in VALID_OPTIONS:
55
64
  url = "https://github.com/PyCQA/prospector/blob/master/prospector/tools/pyright/__init__.py"
56
65
  raise BadToolConfig(
@@ -80,7 +89,9 @@ class PyrightTool(ToolBase):
80
89
  if venv_path:
81
90
  self.options.extend(["--venv-path", venv_path])
82
91
 
83
- def run(self, found_files):
92
+ return None
93
+
94
+ def run(self, found_files: FileFinder) -> list[Message]:
84
95
  paths = [str(path) for path in found_files.python_modules]
85
96
  paths.extend(self.options)
86
97
  result = self.checker.run(*paths, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
@@ -1,5 +1,6 @@
1
1
  import logging
2
- from typing import TYPE_CHECKING, List
2
+ from collections.abc import Iterable
3
+ from typing import TYPE_CHECKING, Any, Optional
3
4
 
4
5
  from prospector.finder import FileFinder
5
6
  from prospector.message import Location, Message
@@ -48,7 +49,7 @@ PYROMA_ALL_CODES = {
48
49
  PYROMA_CODES = {}
49
50
 
50
51
 
51
- def _copy_codes():
52
+ def _copy_codes() -> None:
52
53
  for name, code in PYROMA_ALL_CODES.items():
53
54
  if hasattr(ratings, name):
54
55
  PYROMA_CODES[getattr(ratings, name)] = code
@@ -60,14 +61,17 @@ PYROMA_TEST_CLASSES = [t.__class__ for t in ratings.ALL_TESTS]
60
61
 
61
62
 
62
63
  class PyromaTool(ToolBase):
63
- def __init__(self, *args, **kwargs):
64
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
64
65
  super().__init__(*args, **kwargs)
65
- self.ignore_codes = ()
66
+ self.ignore_codes: list[str] = []
66
67
 
67
- def configure(self, prospector_config: "ProspectorConfig", found_files: FileFinder):
68
+ def configure( # pylint: disable=useless-return
69
+ self, prospector_config: "ProspectorConfig", found_files: FileFinder
70
+ ) -> Optional[tuple[str, Optional[Iterable[Message]]]]:
68
71
  self.ignore_codes = prospector_config.get_disabled_messages("pyroma")
72
+ return None
69
73
 
70
- def run(self, found_files: FileFinder) -> List[Message]:
74
+ def run(self, found_files: FileFinder) -> list[Message]:
71
75
  messages = []
72
76
  for directory in found_files.directories:
73
77
  # just list directories which are not ignored, but find any `setup.py` ourselves
@@ -0,0 +1,84 @@
1
+ import json
2
+ import subprocess # nosec
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from ruff.__main__ import find_ruff_bin
6
+
7
+ from prospector.finder import FileFinder
8
+ from prospector.message import Location, Message
9
+ from prospector.tools.base import ToolBase
10
+
11
+ if TYPE_CHECKING:
12
+ from prospector.config import ProspectorConfig
13
+
14
+
15
+ class RuffTool(ToolBase):
16
+ def configure(self, prospector_config: "ProspectorConfig", _: Any) -> None:
17
+ self.ruff_bin = find_ruff_bin()
18
+ self.ruff_args = ["check", "--output-format=json"]
19
+
20
+ enabled = prospector_config.get_enabled_messages("ruff")
21
+ if enabled:
22
+ enabled_arg_value = ",".join(enabled)
23
+ self.ruff_args.append(f"--select={enabled_arg_value}")
24
+ disabled = prospector_config.get_disabled_messages("ruff")
25
+ if disabled:
26
+ disabled_arg_value = ",".join(disabled)
27
+ self.ruff_args.append(f"--ignore={disabled_arg_value}")
28
+
29
+ options = prospector_config.tool_options("ruff")
30
+ for key, value in options.items():
31
+ if value is True:
32
+ self.ruff_args.append(f"--{key}")
33
+ elif value is False:
34
+ pass
35
+ elif isinstance(value, list):
36
+ arg_value = ",".join(value)
37
+ self.ruff_args.append(f"--{key}={arg_value}")
38
+ # dict is like array but with a dict with true/false value to be able to merge profiles
39
+ elif isinstance(value, dict):
40
+ arg_value = ",".join(k for k, v in value.items() if v)
41
+ self.ruff_args.append(f"--{key}={arg_value}")
42
+ else:
43
+ self.ruff_args.append(f"--{key}={value}")
44
+
45
+ def run(self, found_files: FileFinder) -> list[Message]:
46
+ messages = []
47
+ completed_process = subprocess.run( # noqa: S603
48
+ [self.ruff_bin, *self.ruff_args, *found_files.python_modules], capture_output=True
49
+ )
50
+ if not completed_process.stdout:
51
+ messages.append(
52
+ Message(
53
+ "ruff",
54
+ "",
55
+ Location(None, None, None, None, None),
56
+ completed_process.stderr.decode(),
57
+ )
58
+ )
59
+ return messages
60
+ for message in json.loads(completed_process.stdout):
61
+ sub_message = {}
62
+ if message.get("url"):
63
+ sub_message["See"] = message["url"]
64
+ if message.get("fix") and message["fix"].get("applicability"):
65
+ sub_message["Fix applicability"] = message["fix"]["applicability"]
66
+ message_str = message.get("message", "")
67
+ if sub_message:
68
+ message_str += f" [{', '.join(f'{k}: {v}' for k, v in sub_message.items())}]"
69
+
70
+ messages.append(
71
+ Message(
72
+ "ruff",
73
+ message.get("code") or "unknown",
74
+ Location(
75
+ message.get("filename") or "unknown",
76
+ None,
77
+ None,
78
+ line=message.get("location", {}).get("row"),
79
+ character=message.get("location", {}).get("column"),
80
+ ),
81
+ message_str,
82
+ )
83
+ )
84
+ return messages
prospector/tools/utils.py CHANGED
@@ -1,47 +1,53 @@
1
1
  import sys
2
+ from io import TextIOWrapper
3
+ from typing import Optional
2
4
 
3
5
 
4
- class CaptureStream:
5
- def __init__(self):
6
+ class CaptureStream(TextIOWrapper):
7
+ def __init__(self) -> None:
6
8
  self.contents = ""
7
9
 
8
- def write(self, text):
10
+ def write(self, text: str, /) -> int:
9
11
  self.contents += text
12
+ return len(text)
10
13
 
11
- def close(self):
14
+ def close(self) -> None:
12
15
  pass
13
16
 
14
- def flush(self):
17
+ def flush(self) -> None:
15
18
  pass
16
19
 
17
20
 
18
21
  class CaptureOutput:
19
- def __init__(self, hide):
22
+ _prev_streams = None
23
+ stdout: Optional[CaptureStream] = None
24
+ stderr: Optional[CaptureStream] = None
25
+
26
+ def __init__(self, hide: bool) -> None:
20
27
  self.hide = hide
21
- self._prev_streams = None
22
- self.stdout, self.stderr = None, None
23
28
 
24
- def __enter__(self):
29
+ def __enter__(self) -> "CaptureOutput":
25
30
  if self.hide:
26
- self._prev_streams = [
31
+ self._prev_streams = (
27
32
  sys.stdout,
28
33
  sys.stderr,
29
34
  sys.__stdout__,
30
35
  sys.__stderr__,
31
- ]
36
+ )
32
37
  self.stdout = CaptureStream()
33
38
  self.stderr = CaptureStream()
34
- sys.stdout, sys.__stdout__ = self.stdout, self.stdout
35
- sys.stderr, sys.__stderr__ = self.stderr, self.stderr
39
+ sys.stdout, sys.__stdout__ = self.stdout, self.stdout # type: ignore[misc]
40
+ sys.stderr, sys.__stderr__ = self.stderr, self.stderr # type: ignore[misc]
36
41
  return self
37
42
 
38
- def get_hidden_stdout(self):
39
- return self.stdout.contents
43
+ def get_hidden_stdout(self) -> str:
44
+ return "" if self.stdout is None else self.stdout.contents
40
45
 
41
- def get_hidden_stderr(self):
42
- return self.stderr.contents
46
+ def get_hidden_stderr(self) -> str:
47
+ return "" if self.stderr is None else self.stderr.contents
43
48
 
44
- def __exit__(self, exc_type, exc_val, exc_tb):
49
+ def __exit__(self, exc_type: type, exc_val: Exception, exc_tb: type) -> None:
45
50
  if self.hide:
46
- sys.stdout, sys.stderr, sys.__stdout__, sys.__stderr__ = self._prev_streams
51
+ assert self._prev_streams is not None
52
+ sys.stdout, sys.stderr, sys.__stdout__, sys.__stderr__ = self._prev_streams # type: ignore[misc]
47
53
  del self._prev_streams