Skip to content

Commit 9c338ec

Browse files
committed
Ignore the blended associated code
1 parent 224fc52 commit 9c338ec

File tree

9 files changed

+169
-19
lines changed

9 files changed

+169
-19
lines changed

prospector/postfilter.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
from pathlib import Path
2+
from typing import Optional
23

34
from prospector.message import Message
45
from prospector.suppression import get_suppressions
6+
from prospector.tools.base import ToolBase
57

68

7-
def filter_messages(filepaths: list[Path], messages: list[Message]) -> list[Message]:
9+
def filter_messages(
10+
filepaths: list[Path],
11+
messages: list[Message],
12+
tools: Optional[dict[str, ToolBase]] = None,
13+
blending: bool = False,
14+
blend_combos: Optional[list[list[tuple[str, str]]]] = None,
15+
) -> list[Message]:
816
"""
917
This method post-processes all messages output by all tools, in order to filter
1018
out any based on the overall output.
@@ -23,7 +31,9 @@ def filter_messages(filepaths: list[Path], messages: list[Message]) -> list[Mess
2331
This method uses the information about suppressed messages from pylint to
2432
squash the unwanted redundant error from pyflakes and frosted.
2533
"""
26-
paths_to_ignore, lines_to_ignore, messages_to_ignore = get_suppressions(filepaths, messages)
34+
paths_to_ignore, lines_to_ignore, messages_to_ignore = get_suppressions(
35+
filepaths, messages, tools, blending, blend_combos
36+
)
2737

2838
filtered = []
2939
for message in messages:

prospector/run.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from prospector.formatters import FORMATTERS, Formatter
1717
from prospector.message import Location, Message
1818
from prospector.tools import DEPRECATED_TOOL_NAMES
19+
from prospector.tools.base import ToolBase
1920
from prospector.tools.utils import CaptureOutput
2021

2122

@@ -25,7 +26,9 @@ def __init__(self, config: ProspectorConfig) -> None:
2526
self.summary: Optional[dict[str, Any]] = None
2627
self.messages = config.messages
2728

28-
def process_messages(self, found_files: FileFinder, messages: list[Message]) -> list[Message]:
29+
def process_messages(
30+
self, found_files: FileFinder, messages: list[Message], tools: dict[str, tools.ToolBase]
31+
) -> list[Message]:
2932
if self.config.blending:
3033
messages = blender.blend(messages)
3134

@@ -37,7 +40,7 @@ def process_messages(self, found_files: FileFinder, messages: list[Message]) ->
3740
updated.append(msg)
3841
messages = updated
3942

40-
return postfilter.filter_messages(found_files.python_modules, messages)
43+
return postfilter.filter_messages(found_files.python_modules, messages, tools, self.config.blending)
4144

4245
def execute(self) -> None:
4346
deprecated_names = self.config.replace_deprecated_tool_names()
@@ -70,6 +73,8 @@ def execute(self) -> None:
7073
messages.append(message)
7174
warnings.warn(msg, category=DeprecationWarning, stacklevel=0)
7275

76+
running_tools: dict[str, ToolBase] = {}
77+
7378
# Run the tools
7479
for tool in self.config.get_tools(found_files):
7580
for name, cls in tools.TOOLS.items():
@@ -79,6 +84,8 @@ def execute(self) -> None:
7984
else:
8085
toolname = "Unknown"
8186

87+
running_tools[toolname] = tool
88+
8289
try:
8390
# Tools can output to stdout/stderr in unexpected places, for example,
8491
# pydocstyle emits warnings about __all__ and as pyroma exec's the setup.py
@@ -116,7 +123,7 @@ def execute(self) -> None:
116123
)
117124
messages.append(message)
118125

119-
messages = self.process_messages(found_files, messages)
126+
messages = self.process_messages(found_files, messages, running_tools)
120127

121128
summary["message_count"] = len(messages)
122129
summary["completed"] = datetime.now()

prospector/suppression.py

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@
2727
from typing import Optional
2828

2929
from prospector import encoding
30+
from prospector.blender import BLEND_COMBOS
3031
from prospector.exceptions import FatalProspectorException
3132
from prospector.message import Message
33+
from prospector.tools.base import ToolBase
3234

3335
_FLAKE8_IGNORE_FILE = re.compile(r"flake8[:=]\s*noqa", re.IGNORECASE)
3436
_PEP8_IGNORE_LINE = re.compile(r"#\s*noqa(\s*#.*)?$", re.IGNORECASE)
@@ -51,6 +53,17 @@ def __init__(
5153
def __str__(self) -> str:
5254
return self.code if self.source is None else f"{self.source}.{self.code}"
5355

56+
def __repr__(self) -> str:
57+
return f"<{type(self).__name__} {self}>"
58+
59+
def __eq__(self, value: object) -> bool:
60+
if not isinstance(value, Ignore):
61+
return False
62+
return self.code == value.code and self.source == value.source
63+
64+
def __hash__(self) -> int:
65+
return hash((self.source, self.code))
66+
5467

5568
def get_noqa_suppressions(file_contents: list[str]) -> tuple[bool, set[int], dict[int, set[Ignore]]]:
5669
"""
@@ -81,15 +94,6 @@ def get_noqa_suppressions(file_contents: list[str]) -> tuple[bool, set[int], dic
8194
return ignore_whole_file, ignore_lines, messages_to_ignore
8295

8396

84-
_PYLINT_EQUIVALENTS = {
85-
# TODO: blending has this info already?
86-
"unused-import": (
87-
("pyflakes", "FL0001"),
88-
("frosted", "E101"),
89-
)
90-
}
91-
92-
9397
def _parse_pylint_informational(
9498
messages: list[Message],
9599
) -> tuple[set[Optional[Path]], dict[Optional[Path], dict[int, list[str]]]]:
@@ -113,17 +117,43 @@ def _parse_pylint_informational(
113117
return ignore_files, ignore_messages
114118

115119

120+
def _process_tool_ignores(
121+
tools_ignore: dict[Path, dict[int, set[Ignore]]],
122+
blend_combos_dict: dict[Ignore, set[Ignore]],
123+
messages_to_ignore: dict[Optional[Path], dict[int, set[Ignore]]],
124+
) -> None:
125+
for path, lines_ignore in tools_ignore.items():
126+
for line, ignores in lines_ignore.items():
127+
for ignore in ignores:
128+
if ignore in blend_combos_dict:
129+
messages_to_ignore[path][line].update(blend_combos_dict[ignore])
130+
131+
116132
def get_suppressions(
117-
filepaths: list[Path], messages: list[Message]
133+
filepaths: list[Path],
134+
messages: list[Message],
135+
tools: Optional[dict[str, ToolBase]] = None,
136+
blending: bool = False,
137+
blend_combos: Optional[list[list[tuple[str, str]]]] = None,
118138
) -> tuple[set[Optional[Path]], dict[Path, set[int]], dict[Optional[Path], dict[int, set[Ignore]]]]:
119139
"""
120140
Given every message which was emitted by the tools, and the
121141
list of files to inspect, create a list of files to ignore,
122142
and a map of filepath -> line-number -> codes to ignore
123143
"""
144+
tools = tools or {}
145+
blend_combos = blend_combos or BLEND_COMBOS
146+
blend_combos_dict: dict[Ignore, set[Ignore]] = {}
147+
if blending:
148+
for combo in blend_combos:
149+
ignore_combos = {Ignore(tool, code) for tool, code in combo}
150+
for ignore in ignore_combos:
151+
blend_combos_dict[ignore] = ignore_combos
152+
124153
paths_to_ignore: set[Optional[Path]] = set()
125154
lines_to_ignore: dict[Path, set[int]] = defaultdict(set)
126155
messages_to_ignore: dict[Optional[Path], dict[int, set[Ignore]]] = defaultdict(lambda: defaultdict(set))
156+
tools_ignore: dict[Path, dict[int, set[Ignore]]] = defaultdict(lambda: defaultdict(set))
127157

128158
# First deal with 'noqa' style messages
129159
for filepath in filepaths:
@@ -141,6 +171,17 @@ def get_suppressions(
141171
for line, codes_ignore in file_messages_to_ignore.items():
142172
messages_to_ignore[filepath][line] |= codes_ignore
143173

174+
if blending:
175+
for line_number, line_content in enumerate(file_contents):
176+
for tool_name, tool in tools.items():
177+
tool_ignores = tool.get_ignored_codes(line_content)
178+
for tool_ignore in tool_ignores:
179+
tools_ignore[filepath][line_number + 1].add(Ignore(tool_name, tool_ignore))
180+
181+
# Ignore the blending messages
182+
if blending:
183+
_process_tool_ignores(tools_ignore, blend_combos_dict, messages_to_ignore)
184+
144185
# Now figure out which messages were suppressed by pylint
145186
pylint_ignore_files, pylint_ignore_messages = _parse_pylint_informational(messages)
146187
paths_to_ignore |= pylint_ignore_files
@@ -149,9 +190,5 @@ def get_suppressions(
149190
for code in codes:
150191
ignore = Ignore("pylint", code)
151192
messages_to_ignore[pylint_filepath][line_number].add(ignore)
152-
if code in _PYLINT_EQUIVALENTS:
153-
for ignore_source, ignore_code in _PYLINT_EQUIVALENTS[code]:
154-
ignore = Ignore(ignore_source, ignore_code)
155-
messages_to_ignore[pylint_filepath][line_number].add(ignore)
156193

157194
return paths_to_ignore, lines_to_ignore, messages_to_ignore

prospector/tools/base.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,10 @@ def run(self, found_files: FileFinder) -> list[Message]:
4040
standard prospector Message and Location objects.
4141
"""
4242
raise NotImplementedError
43+
44+
def get_ignored_codes(self, line: str) -> list[str]:
45+
"""
46+
Return a list of error codes that the tool will ignore from a line of code.
47+
"""
48+
del line # unused
49+
return []

prospector/tools/mypy/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import re
12
from multiprocessing import Process, Queue
23
from typing import TYPE_CHECKING, Any, Callable, Optional
34

@@ -14,6 +15,8 @@
1415
if TYPE_CHECKING:
1516
from prospector.config import ProspectorConfig
1617

18+
_IGNORE_RE = re.compile(r"#\s*type:\s*ignore\[([^#]*[^# ])\](\s*#.*)?$", re.IGNORECASE)
19+
1720

1821
def format_message(message: str) -> Message:
1922
character: Optional[int]
@@ -105,3 +108,9 @@ def run(self, found_files: FileFinder) -> list[Message]:
105108
report, _ = result[0], result[1:] # noqa
106109

107110
return [format_message(message) for message in report.splitlines()]
111+
112+
def get_ignored_codes(self, line: str) -> list[str]:
113+
match = _IGNORE_RE.search(line)
114+
if match:
115+
return match.group(1).split(",")
116+
return []

prospector/tools/pylint/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121

2222
_UNUSED_WILDCARD_IMPORT_RE = re.compile(r"^Unused import(\(s\))? (.*) from wildcard import")
2323

24+
_IGNORE_RE = re.compile(r"#\s*pylint:\s*disable=([^#]*[^#\s])(?:\s*#.*)?$", re.IGNORECASE)
25+
2426

2527
def _is_in_dir(subpath: Path, path: Path) -> bool:
2628
return subpath.parent == path
@@ -266,3 +268,9 @@ def run(self, found_files: FileFinder) -> list[Message]:
266268

267269
messages = self._collector.get_messages()
268270
return self.combine(messages)
271+
272+
def get_ignored_codes(self, line: str) -> list[str]:
273+
match = _IGNORE_RE.search(line)
274+
if match:
275+
return match.group(1).split(",")
276+
return []

prospector/tools/ruff/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import re
23
import subprocess # nosec
34
from typing import TYPE_CHECKING, Any
45

@@ -11,6 +12,8 @@
1112
if TYPE_CHECKING:
1213
from prospector.config import ProspectorConfig
1314

15+
_IGNORE_RE = re.compile(r"#\s*noqa:([^#]*[^# ])(?:\s*#.*)?$", re.IGNORECASE)
16+
1417

1518
class RuffTool(ToolBase):
1619
def configure(self, prospector_config: "ProspectorConfig", _: Any) -> None:
@@ -84,3 +87,9 @@ def run(self, found_files: FileFinder) -> list[Message]:
8487
)
8588
)
8689
return messages
90+
91+
def get_ignored_codes(self, line: str) -> list[str]:
92+
match = _IGNORE_RE.search(line)
93+
if match:
94+
return match.group(1).split(",")
95+
return []
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import unittest
2+
from pathlib import Path
3+
4+
from prospector.message import Location, Message
5+
from prospector.suppression import Ignore, get_suppressions
6+
from prospector.tools.mypy import MypyTool
7+
from prospector.tools.pylint import PylintTool
8+
9+
10+
class BlenderSuppressionsTest(unittest.TestCase):
11+
def test_blender_suppressions_pylint(self):
12+
path = Path(__file__).parent / "testdata" / "test_blender_suppressions" / "test.py"
13+
messages = [
14+
Message("pylint", "n2", Location(path, None, None, 2, 0), "message1"),
15+
Message("other", "o2", Location(path, None, None, 2, 0), "message1"),
16+
]
17+
tools = {"pylint": PylintTool()}
18+
blend_combos = [[("pylint", "n2"), ("other", "o2")]]
19+
20+
_, _, messages_to_ignore = get_suppressions([path], messages, tools, blending=False, blend_combos=blend_combos)
21+
assert messages_to_ignore == {path: {1: {Ignore(None, "n1")}}}
22+
23+
_, _, messages_to_ignore = get_suppressions([path], messages, tools, blending=True, blend_combos=blend_combos)
24+
assert path in messages_to_ignore
25+
assert 2 in messages_to_ignore[path]
26+
assert messages_to_ignore[path][2] == {Ignore("pylint", "n2"), Ignore("other", "o2")}
27+
28+
def test_blender_suppressions_mypy(self):
29+
path = Path(__file__).parent / "testdata" / "test_blender_suppressions" / "test.py"
30+
messages = [
31+
Message("mypy", "n3", Location(path, None, None, 3, 0), "message1"),
32+
Message("other", "o3", Location(path, None, None, 3, 0), "message1"),
33+
]
34+
tools = {"mypy": MypyTool()}
35+
blend_combos = [[("mypy", "n3"), ("other", "o3")]]
36+
37+
_, _, messages_to_ignore = get_suppressions([path], messages, tools, blending=False, blend_combos=blend_combos)
38+
assert messages_to_ignore == {path: {1: {Ignore(None, "n1")}}}
39+
40+
_, _, messages_to_ignore = get_suppressions([path], messages, tools, blending=True, blend_combos=blend_combos)
41+
assert path in messages_to_ignore
42+
assert 3 in messages_to_ignore[path]
43+
assert messages_to_ignore[path][3] == {Ignore("mypy", "n3"), Ignore("other", "o3")}
44+
45+
def test_blender_suppressions_ruff(self):
46+
path = Path(__file__).parent / "testdata" / "test_blender_suppressions" / "test.py"
47+
messages = [
48+
Message("ruff", "n1", Location(path, None, None, 1, 0), "message1"),
49+
Message("other", "o1", Location(path, None, None, 1, 0), "message1"),
50+
]
51+
tools = {"ruff": MypyTool()}
52+
blend_combos = [[("ruff", "n1"), ("other", "o1")]]
53+
54+
_, _, messages_to_ignore = get_suppressions([path], messages, tools, blending=False, blend_combos=blend_combos)
55+
assert messages_to_ignore == {path: {1: {Ignore(None, "n1")}}}
56+
57+
_, _, messages_to_ignore = get_suppressions([path], messages, tools, blending=True, blend_combos=blend_combos)
58+
assert path in messages_to_ignore
59+
assert 4 in messages_to_ignore[path]
60+
assert messages_to_ignore[path][4] == {Ignore("ruff", "n1"), Ignore("other", "o1"), Ignore(None, "n1")}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import test # noqa: n1
2+
import test # pylint: disable=n2
3+
import test # type: ignore[n3]

0 commit comments

Comments
 (0)