diff --git a/prospector/blender.py b/prospector/blender.py index fb261912..6516d275 100644 --- a/prospector/blender.py +++ b/prospector/blender.py @@ -85,11 +85,10 @@ def blend(messages: list[Message], blend_combos: Optional[list[list[tuple[str, s blend_combos = blend_combos or BLEND_COMBOS # group messages by file and then line number - msgs_grouped: dict[Path, dict[int, list[Message]]] = defaultdict(lambda: defaultdict(list)) + msgs_grouped: dict[Optional[Path], dict[Optional[int], list[Message]]] = defaultdict(lambda: defaultdict(list)) for message in messages: - line = message.location.line - msgs_grouped[message.location.path][-1 if line is None else line].append( + msgs_grouped[message.location.path][message.location.line].append( message, ) diff --git a/prospector/formatters/base.py b/prospector/formatters/base.py index f1185a0f..f2d3db03 100644 --- a/prospector/formatters/base.py +++ b/prospector/formatters/base.py @@ -28,7 +28,8 @@ def render(self, summary: bool = True, messages: bool = True, profile: bool = Fa raise NotImplementedError def _make_path(self, location: Location) -> Path: - return location.relative_path(self.paths_relative_to) + path_ = location.relative_path(self.paths_relative_to) + return Path() if path_ is None else path_ def _message_to_dict(self, message: Message) -> dict[str, Any]: loc = { diff --git a/prospector/formatters/grouped.py b/prospector/formatters/grouped.py index 320c22a4..b9f0a27e 100644 --- a/prospector/formatters/grouped.py +++ b/prospector/formatters/grouped.py @@ -1,5 +1,6 @@ from collections import defaultdict from pathlib import Path +from typing import Optional from prospector.formatters.text import TextFormatter from prospector.message import Message @@ -15,10 +16,9 @@ def render_messages(self) -> str: "", ] - groups: dict[Path, dict[int, list[Message]]] = defaultdict(lambda: defaultdict(list)) + groups: dict[Path, dict[Optional[int], list[Message]]] = defaultdict(lambda: defaultdict(list)) for message in self.messages: - assert message.location.line is not None groups[self._make_path(message.location)][message.location.line].append(message) for filename in sorted(groups.keys()): diff --git a/prospector/formatters/pylint.py b/prospector/formatters/pylint.py index 1194e361..957d27d5 100644 --- a/prospector/formatters/pylint.py +++ b/prospector/formatters/pylint.py @@ -28,16 +28,26 @@ def render_messages(self) -> list[str]: # Missing function docstring template_location = ( - "%(path)s" + "" + if message.location.path is None + else "%(path)s" if message.location.line is None else "%(path)s:%(line)s" if message.location.character is None else "%(path)s:%(line)s:%(character)s" ) template_code = ( - "%(code)s(%(source)s)" if message.location.function is None else "[%(code)s(%(source)s), %(function)s]" + "(%(source)s)" + if message.code is None + else "%(code)s(%(source)s)" + if message.location.function is None + else "[%(code)s(%(source)s), %(function)s]" + ) + template = ( + f"{template_location}: {template_code}: %(message)s" + if template_location + else f"{template_code}: %(message)s" ) - template = f"{template_location}: {template_code}: %(message)s" output.append( template diff --git a/prospector/message.py b/prospector/message.py index b902329b..99570f29 100644 --- a/prospector/message.py +++ b/prospector/message.py @@ -3,9 +3,11 @@ class Location: + _path: Optional[Path] + def __init__( self, - path: Union[Path, str], + path: Optional[Union[Path, str]], module: Optional[str], function: Optional[str], line: Optional[int], @@ -15,6 +17,8 @@ def __init__( self._path = path.absolute() elif isinstance(path, str): self._path = Path(path).absolute() + elif path is None: + self._path = None else: raise ValueError self.module = module or None @@ -23,13 +27,15 @@ def __init__( self.character = None if character == -1 else character @property - def path(self) -> Path: + def path(self) -> Optional[Path]: return self._path - def absolute_path(self) -> Path: + def absolute_path(self) -> Optional[Path]: return self._path - def relative_path(self, root: Optional[Path]) -> Path: + def relative_path(self, root: Optional[Path]) -> Optional[Path]: + if self._path is None: + return None return self._path.relative_to(root) if root else self._path def __repr__(self) -> str: @@ -46,6 +52,13 @@ def __eq__(self, other: object) -> bool: def __lt__(self, other: "Location") -> bool: if not isinstance(other, Location): raise ValueError + + if self._path is None and other._path is None: + return False + if self._path is None: + return True + if other._path is None: + return False if self._path == other._path: if self.line == other.line: return (self.character or -1) < (other.character or -1) diff --git a/prospector/postfilter.py b/prospector/postfilter.py index 78ee07f7..72c893aa 100644 --- a/prospector/postfilter.py +++ b/prospector/postfilter.py @@ -29,7 +29,7 @@ def filter_messages(filepaths: List[Path], messages: List[Message]) -> List[Mess filtered = [] for message in messages: # first get rid of the pylint informational messages - relative_message_path = Path(message.location.path) + relative_message_path = message.location.path if message.source == "pylint" and message.code in ( "suppressed-message", diff --git a/prospector/suppression.py b/prospector/suppression.py index bc196889..b94f7754 100644 --- a/prospector/suppression.py +++ b/prospector/suppression.py @@ -24,6 +24,7 @@ import warnings from collections import defaultdict from pathlib import Path +from typing import Optional from prospector import encoding from prospector.exceptions import FatalProspectorException @@ -63,9 +64,11 @@ def get_noqa_suppressions(file_contents: list[str]) -> tuple[bool, set[int]]: } -def _parse_pylint_informational(messages: list[Message]) -> tuple[set[Path], dict[Path, dict[int, list[str]]]]: - ignore_files: set[Path] = set() - ignore_messages: dict[Path, dict[int, list[str]]] = defaultdict(lambda: defaultdict(list)) +def _parse_pylint_informational( + messages: list[Message], +) -> tuple[set[Optional[Path]], dict[Optional[Path], dict[int, list[str]]]]: + ignore_files: set[Optional[Path]] = set() + ignore_messages: dict[Optional[Path], dict[int, list[str]]] = defaultdict(lambda: defaultdict(list)) for message in messages: if message.source == "pylint": @@ -86,15 +89,15 @@ def _parse_pylint_informational(messages: list[Message]) -> tuple[set[Path], dic def get_suppressions( filepaths: list[Path], messages: list[Message] -) -> tuple[set[Path], dict[Path, set[int]], dict[Path, dict[int, set[tuple[str, str]]]]]: +) -> tuple[set[Optional[Path]], dict[Path, set[int]], dict[Optional[Path], dict[int, set[tuple[str, str]]]]]: """ Given every message which was emitted by the tools, and the list of files to inspect, create a list of files to ignore, and a map of filepath -> line-number -> codes to ignore """ - paths_to_ignore: set[Path] = set() + paths_to_ignore: set[Optional[Path]] = set() lines_to_ignore: dict[Path, set[int]] = defaultdict(set) - messages_to_ignore: dict[Path, dict[int, set[tuple[str, str]]]] = defaultdict(lambda: defaultdict(set)) + messages_to_ignore: dict[Optional[Path], dict[int, set[tuple[str, str]]]] = defaultdict(lambda: defaultdict(set)) # first deal with 'noqa' style messages for filepath in filepaths: @@ -113,12 +116,12 @@ def get_suppressions( # now figure out which messages were suppressed by pylint pylint_ignore_files, pylint_ignore_messages = _parse_pylint_informational(messages) paths_to_ignore |= pylint_ignore_files - for filepath, line in pylint_ignore_messages.items(): + for pylint_filepath, line in pylint_ignore_messages.items(): for line_number, codes in line.items(): for code in codes: - messages_to_ignore[filepath][line_number].add(("pylint", code)) + messages_to_ignore[pylint_filepath][line_number].add(("pylint", code)) if code in _PYLINT_EQUIVALENTS: for equivalent in _PYLINT_EQUIVALENTS[code]: - messages_to_ignore[filepath][line_number].add(equivalent) + messages_to_ignore[pylint_filepath][line_number].add(equivalent) return paths_to_ignore, lines_to_ignore, messages_to_ignore diff --git a/prospector/tools/ruff/__init__.py b/prospector/tools/ruff/__init__.py index 25dbf589..6aca01ba 100644 --- a/prospector/tools/ruff/__init__.py +++ b/prospector/tools/ruff/__init__.py @@ -47,6 +47,16 @@ def run(self, found_files: FileFinder) -> list[Message]: completed_process = subprocess.run( # noqa: S603 [self.ruff_bin, *self.ruff_args, *found_files.python_modules], capture_output=True ) + if not completed_process.stdout: + messages.append( + Message( + "ruff", + "", + Location(None, None, None, None, None), + completed_process.stderr.decode(), + ) + ) + return messages for message in json.loads(completed_process.stdout): sub_message = {} if message.get("url"): diff --git a/tests/test_message.py b/tests/test_message.py index 2577f82c..5ffcb0a8 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -25,7 +25,6 @@ def test_strings_or_paths(self): def test_bad_path_input(self): self.assertRaises(ValueError, Location, 3.2, "module", "func", 1, 2) - self.assertRaises(ValueError, Location, None, "module", "func", 1, 2) class LocationOrderTest(TestCase):