Skip to content

Commit

Permalink
Add support for external codes
Browse files Browse the repository at this point in the history
  • Loading branch information
Avasam committed Nov 24, 2024
1 parent fe5befa commit 7bbc0b1
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 15 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ Options
`noqa-no-include-name`
: Do not include plugin name in messages (default setting)

`noqa-external`
: List of codes coming from non-Flake8 tools. Use this to allow
interoperability between flake8-noqa and non-Flake8 tools also using `# noqa`
suppression comments.

All options may be specified on the command line with a `--` prefix,
or can be placed in your flake8 config file.

Expand Down
37 changes: 22 additions & 15 deletions flake8_noqa/noqa_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ class Options(Protocol):

noqa_require_code: bool
noqa_include_name: bool
external: list[str]


class NoqaFilter:
Expand All @@ -91,6 +92,7 @@ class NoqaFilter:
version: ClassVar[str] = package_version
plugin_name: ClassVar[str]
require_code: ClassVar[bool]
external: list[str]
_filters: ClassVar[list[NoqaFilter]] = []

tree: ast.AST
Expand All @@ -111,12 +113,18 @@ def add_options(cls, option_manager: flake8.options.manager.OptionManager) -> No
option_manager.add_option('--noqa-no-include-name', default=None, action='store_false',
parse_from_config=False, dest='noqa_include_name',
help='Remove plugin name from messages')
option_manager.add_option('--noqa-external', default=[], comma_separated_list=True,
parse_from_config=True, dest='external',
help='List of codes coming from non-Flake8 tools. Use this to allow '
'interoperability between flake8-noqa and non-Flake8 tools also using `# noqa` '
'suppression comments.')

@classmethod
def parse_options(cls, options: Options) -> None:
"""Parse plugin options."""
cls.plugin_name = (' (' + cls.name + ')') if (options.noqa_include_name) else ''
cls.require_code = options.noqa_require_code
cls.external = options.external

@classmethod
def filters(cls) -> Sequence[NoqaFilter]:
Expand All @@ -140,32 +148,31 @@ def __iter__(self) -> Iterator[tuple[int, int, str, Any]]:
def _message(self, token: tokenize.TokenInfo, message: Message, **kwargs) -> tuple[int, int, str, Any]:
return (token.start[0], token.start[1], f'{message.code}{self.plugin_name} {message.text(**kwargs)}', type(self))

def _is_external_code(self, code: str) -> bool:
return any(code.startswith(external_code) for external_code in self.external)

def violations(self) -> Iterator[tuple[int, int, str, Any]]:
"""Private iterator to return violations."""
for comment in InlineComment.file_comments(self.filename):
reports = Report.reports_from(self.filename, comment.start_line, comment.end_line)
reports = set(Report.reports_from(self.filename, comment.start_line, comment.end_line))
comment_codes = set(comment.code_list)
if (comment_codes):
matched_codes: set[str] = set()
for code in reports:
if (code in comment_codes):
matched_codes.add(code)
if (matched_codes):
if (len(matched_codes) < len(comment_codes)):
unmatched_codes = comment_codes - matched_codes
yield self._message(comment.token, Message.NOQA_UNMATCHED_CODES,
comment=comment.text, unmatched=', '.join(unmatched_codes),
plural='codes' if (1 < len(unmatched_codes)) else 'code')
else:
external_comment_codes = {code for code in comment_codes if self._is_external_code(code)}
matched_and_external_codes = (reports & comment_codes) | external_comment_codes
if (not matched_and_external_codes):
yield self._message(comment.token, Message.NOQA_NO_MATCHING_CODES, comment=comment.text)

pass
else:
unmatched_codes = comment_codes - matched_and_external_codes
if (unmatched_codes):
yield self._message(comment.token, Message.NOQA_UNMATCHED_CODES,
comment=comment.text, unmatched=', '.join(unmatched_codes),
plural='codes' if (1 < len(unmatched_codes)) else 'code')
else: # blanket noqa
if (reports):
if (self.require_code):
yield self._message(comment.token, Message.NOQA_REQUIRE_CODE,
comment=comment.text, noqa_strip=comment.noqa.strip(),
codes=', '.join(sorted(set(reports))))
codes=', '.join(sorted(reports)))

else:
yield self._message(comment.token, Message.NOQA_NO_VIOLATIONS, comment=comment.text)
Expand Down
13 changes: 13 additions & 0 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,19 @@ def test_inlude_name(self) -> None:
'1:5: NQA002 (flake8-noqa) "# noqa E225" must have a colon, e.g. "# noqa: E225"',
])

def test_external_code(self) -> None:
# Test that the comma-separated list works
self.assertEqual(flake8("x = 1 # noqa: EXT001", ['--noqa-external=EXT,OTHER']), [])
# Test with code group
self.assertEqual(flake8("x = 1 # noqa: EXT001", ['--noqa-external=EXT']), [])
# Test with exact code
self.assertEqual(flake8("x = 1 # noqa: EXT001", ['--noqa-external=EXT001']), [])
# Test that it's not included in "no matching violations" / "unmatched code"
self.assertEqual(flake8('x = 1 # noqa: EXT001, X101', ['--noqa-external=EXT']), [
'1:5: NQA103 "# noqa: EXT001, X101" has unmatched code, remove X101',
])
self.assertEqual(flake8('x=1 # noqa: E225, EXT001', ['--noqa-external=EXT']), [])


if __name__ == '__main__':
unittest.main()

0 comments on commit 7bbc0b1

Please sign in to comment.