Add ability to disable qa for blocks of code

An ignore block can be defined with `# noqa: on` and `# noqa: off`
comments. Ignore block behaviour should be the same as inline ignore.
This commit is contained in:
Tom 2025-03-25 13:32:35 -04:00
parent e492aeb385
commit 6051897a91
4 changed files with 174 additions and 2 deletions

View file

@ -40,6 +40,25 @@ NOQA_INLINE_REGEXP = re.compile(
re.IGNORECASE,
)
NOQA_BLOCK_REGEXP = re.compile(
# We're looking for items that look like this:
# ``# noqa: off``
# ``# noqa:off``
# ``# noqa: Off``
# ``# noqa:Off``
# ``# noqa: off E123``
# ``# noqa: off E123,W451,F921``
# ``# noqa:off E123,W451,F921``
# ``# NoQA: off E123,W451,F921``
# ``# NOQA: off E123,W451,F921``
# ``# NOQA:off E123,W451,F921``
# We do not care about the casing of ``noqa``
# We want the desired block state
# We want a comma-separated list of errors
r"# noqa:[\s]?(?P<state>off|on)[\s]?(?P<codes>([A-Z]+[0-9]+(?:[,\s]+)?)+)?",
re.IGNORECASE,
)
NOQA_FILE = re.compile(r"\s*# flake8[:=]\s*noqa", re.I)
VALID_CODE_PREFIX = re.compile("^[A-Z]{1,3}[0-9]{0,3}$", re.ASCII)

View file

@ -423,8 +423,13 @@ class StyleGuide:
error_is_selected = (
self.should_report_error(error.code) is Decision.Selected
)
is_not_inline_ignored = error.is_inline_ignored(disable_noqa) is False
if error_is_selected and is_not_inline_ignored:
is_ignored = (
error.is_block_ignored(disable_noqa)
or error.is_inline_ignored(disable_noqa)
)
is_not_ignored = is_ignored is False
if error_is_selected and is_not_ignored:
self.formatter.handle(error)
self.stats.record(error)
return 1

View file

@ -19,6 +19,45 @@ def _find_noqa(physical_line: str) -> Match[str] | None:
return defaults.NOQA_INLINE_REGEXP.search(physical_line)
@functools.lru_cache(maxsize=512)
def _find_block_noqa(physical_line: str) -> Match[str] | None:
return defaults.NOQA_BLOCK_REGEXP.search(physical_line)
@functools.lru_cache(maxsize=8)
def _noqa_block_ranges(filename: str) -> Match[str] | None:
noqa_block_ranges = []
next_expected_state = "on"
current_block_start = None
current_block_codes = None
enumerated_lines = enumerate(linecache.getlines(filename), start=1)
for line_no, physical_line in enumerated_lines:
noqa_match = _find_block_noqa(physical_line)
if noqa_match is None:
continue
state = noqa_match.groupdict().get("state").lower()
codes = noqa_match.groupdict().get("codes")
if state != next_expected_state:
continue
elif state == "on":
next_expected_state = "off"
current_block_start = line_no
current_block_codes = codes
elif state == "off":
noqa_block_ranges.append((
range(current_block_start, line_no), current_block_codes,
))
next_expected_state = "on"
current_block_start = None
current_block_codes = None
return tuple(noqa_block_ranges)
class Violation(NamedTuple):
"""Class representing a violation reported by Flake8."""
@ -29,6 +68,54 @@ class Violation(NamedTuple):
text: str
physical_line: str | None
def is_block_ignored(self, disable_noqa: bool) -> bool:
"""Determine if line is between comments which define an ignore block.
:param disable_noqa:
Whether or not users have provided ``--disable-noqa``.
:returns:
True if error is ignored by block, False otherwise.
"""
if disable_noqa:
return False
filename = self.filename
line_number = self.line_number
def in_range(block_range):
return line_number in block_range
blocks_in_range = (
(block_line_range, codes)
for block_line_range, codes in _noqa_block_ranges(filename)
for is_in_range, codes in [(in_range(block_line_range), codes)]
if is_in_range
)
block_line_range, codes_str = next(blocks_in_range, (None, None))
if block_line_range is None:
LOG.debug("%r is not block ignored", self)
return False
if codes_str is None:
LOG.debug("%r is ignored by a blanket ``# noqa: on ... # noqa: off`` block", self)
return True
codes = set(utils.parse_comma_separated_list(codes_str))
if self.code in codes or self.code.startswith(tuple(codes)):
LOG.debug(
"%r is ignored specifically within ``# noqa: on %s ... # noqa: off`` block",
self,
codes_str,
)
return True
LOG.debug(
"%r is not ignored within ``# noqa: on %s ... # noqa: off`` block", self, codes_str
)
return False
def is_inline_ignored(self, disable_noqa: bool) -> bool:
"""Determine if a comment has been added to ignore this line.

View file

@ -51,3 +51,64 @@ def test_disable_is_inline_ignored():
assert error.is_inline_ignored(True) is False
assert getline.called is False
def _bi_tc(error_code, physical_line, block_start, filename, expected_result):
if block_start is None:
physical_lines = [physical_line]
line_no = 1
else:
block_end = "# noqa: off"
physical_lines = [block_start, physical_line, block_end]
line_no = 2
return (error_code, filename, physical_lines, line_no, expected_result)
@pytest.mark.parametrize(
"error_code,filename,physical_lines,line_no,expected_result",
[
_bi_tc("E111", "a = 1", None, "filename-1.py", False),
_bi_tc("E121", "a = 1", "# noqa: on E111", "filename-2.py", False),
_bi_tc("E121", "a = 1", "# noqa: on E111,W123,F821", "filename-3.py", False),
_bi_tc("E111", "a = 1", "# noqa: on E111,W123,F821", "filename-4.py", True),
_bi_tc("W123", "a = 1", "# noqa: on E111,W123,F821", "filename-5.py", True),
_bi_tc("W123", "a = 1", "# noqa: on E111, W123,F821", "filename-6.py", True),
_bi_tc("E111", "a = 1", "# noqa: on E11,W123,F821", "filename-7.py", True),
_bi_tc("E121", "a = 1", "# noqa:on E111,W123,F821", "filename-8.py", False),
_bi_tc("E111", "a = 1", "# noqa:on E111,W123,F821", "filename-9.py", True),
_bi_tc("W123", "a = 1", "# noqa:on E111,W123,F821", "filename-10.py", True),
_bi_tc("W123", "a = 1", "# noqa:on E111, W123,F821", "filename-11.py", True),
_bi_tc("E111", "a = 1", "# noqa:on E11,W123,F821", "filename-12.py", True),
_bi_tc("E111", "a = 1", "# noqa: on - We do not care", "filename-13.py", True),
_bi_tc("E111", "a = 1", "# noqa: on We do not care", "filename-14.py", True),
_bi_tc("E111", "a = 1", "# noqa:On We do not care", "filename-15.py", True),
_bi_tc("ABC123", "a = 1", "# noqa: on ABC123", "filename-16.py", True),
_bi_tc("E111", "a = 1", "# noqa: on ABC123", "filename-17.py", False),
_bi_tc("ABC123", "a = 1", "# noqa: on ABC124", "filename-18.py", False),
_bi_tc("ABC123", "a = 1", "# noqa: ABC124", "filename-19.py", False),
_bi_tc("ABC123", "a = 1", "# noqa: off ABC124", "filename-19.py", False),
],
)
def test_is_block_ignored(
error_code,
filename,
physical_lines,
line_no,
expected_result,
):
"""Verify that we detect block usage of ``# noqa: off/on``."""
error = Violation(error_code, filename, line_no, 1, "error text", None)
with mock.patch("linecache.getlines", return_value=physical_lines):
assert error.is_block_ignored(False) is expected_result
def test_disable_is_block_ignored():
"""Verify that is_block_ignored exits immediately if disabling NoQA."""
error = Violation("E121", "filename.py", 1, 1, "error text", "line")
with mock.patch("linecache.getlines") as getlines:
assert error.is_block_ignored(True) is False
assert getlines.called is False