Skip to content

Commit e46bf57

Browse files
committed
Add support for external codes
1 parent fe5befa commit e46bf57

File tree

3 files changed

+40
-15
lines changed

3 files changed

+40
-15
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ Options
9292
`noqa-no-include-name`
9393
: Do not include plugin name in messages (default setting)
9494

95+
`noqa-external`
96+
: List of codes comming from non-Flake8 tools. Use this to allow
97+
interoperability between flake8-noqa and non-Flake8 tools also using `# noqa`
98+
suppression comments.
99+
95100
All options may be specified on the command line with a `--` prefix,
96101
or can be placed in your flake8 config file.
97102

flake8_noqa/noqa_filter.py

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ class Options(Protocol):
8282

8383
noqa_require_code: bool
8484
noqa_include_name: bool
85+
external: list[str]
8586

8687

8788
class NoqaFilter:
@@ -91,6 +92,7 @@ class NoqaFilter:
9192
version: ClassVar[str] = package_version
9293
plugin_name: ClassVar[str]
9394
require_code: ClassVar[bool]
95+
external: list[str]
9496
_filters: ClassVar[list[NoqaFilter]] = []
9597

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

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

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

151+
def _is_external_code(self, code: str) -> bool:
152+
return any(code.startswith(external_code) for external_code in self.external)
153+
143154
def violations(self) -> Iterator[tuple[int, int, str, Any]]:
144155
"""Private iterator to return violations."""
145156
for comment in InlineComment.file_comments(self.filename):
146-
reports = Report.reports_from(self.filename, comment.start_line, comment.end_line)
157+
reports = set(Report.reports_from(self.filename, comment.start_line, comment.end_line))
147158
comment_codes = set(comment.code_list)
148159
if (comment_codes):
149-
matched_codes: set[str] = set()
150-
for code in reports:
151-
if (code in comment_codes):
152-
matched_codes.add(code)
153-
if (matched_codes):
154-
if (len(matched_codes) < len(comment_codes)):
155-
unmatched_codes = comment_codes - matched_codes
156-
yield self._message(comment.token, Message.NOQA_UNMATCHED_CODES,
157-
comment=comment.text, unmatched=', '.join(unmatched_codes),
158-
plural='codes' if (1 < len(unmatched_codes)) else 'code')
159-
else:
160+
external_comment_codes = {code for code in comment_codes if self._is_external_code(code)}
161+
matched_and_external_codes = (reports & comment_codes) | external_comment_codes
162+
if (not matched_and_external_codes):
160163
yield self._message(comment.token, Message.NOQA_NO_MATCHING_CODES, comment=comment.text)
161-
162-
pass
164+
else:
165+
unmatched_codes = comment_codes - matched_and_external_codes
166+
if (unmatched_codes):
167+
yield self._message(comment.token, Message.NOQA_UNMATCHED_CODES,
168+
comment=comment.text, unmatched=', '.join(unmatched_codes),
169+
plural='codes' if (1 < len(unmatched_codes)) else 'code')
163170
else: # blanket noqa
164171
if (reports):
165172
if (self.require_code):
166173
yield self._message(comment.token, Message.NOQA_REQUIRE_CODE,
167174
comment=comment.text, noqa_strip=comment.noqa.strip(),
168-
codes=', '.join(sorted(set(reports))))
175+
codes=', '.join(sorted(reports)))
169176

170177
else:
171178
yield self._message(comment.token, Message.NOQA_NO_VIOLATIONS, comment=comment.text)

test.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,19 @@ def test_inlude_name(self) -> None:
152152
'1:5: NQA002 (flake8-noqa) "# noqa E225" must have a colon, e.g. "# noqa: E225"',
153153
])
154154

155+
def test_external_code(self) -> None:
156+
# Test that the comma-separated list works
157+
self.assertEqual(flake8("x = 1 # noqa: EXT001", ['--noqa-external=EXT,OTHER']), [])
158+
# Test with code group
159+
self.assertEqual(flake8("x = 1 # noqa: EXT001", ['--noqa-external=EXT']), [])
160+
# Test with exact code
161+
self.assertEqual(flake8("x = 1 # noqa: EXT001", ['--noqa-external=EXT001']), [])
162+
# Test that it's not included in "no matching violations" / "unmatched code"
163+
self.assertEqual(flake8('x = 1 # noqa: EXT001, X101', ['--noqa-external=EXT']), [
164+
'1:5: NQA103 "# noqa: EXT001, X101" has unmatched code, remove X101',
165+
])
166+
self.assertEqual(flake8('x=1 # noqa: E225, EXT001', ['--noqa-external=EXT']), [])
167+
155168

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

0 commit comments

Comments
 (0)