Skip to content

Commit 813b4d1

Browse files
Fix a bug where inline configurations of error codes would lose their values if accompanied by another inline configuration. (#19075)
The following code produces a name-defined code error, despite our instructions. How can it be?! ```py3 # mypy: disable-error-code=name-defined # mypy: strict-equality a ``` The answer is, there was a bug that caused all inline configurations, even those that didn't specify any enable/disables, to overwrite the lists of enable/disables. I have now fixed that. I've also added some tests. Closes #12342 — I discovered this problem while investigating the last foible of issue #12342 (itself of tangential interest to something else I was doing), which can now be closed. --------- Co-authored-by: Stanislav Terliakov <[email protected]>
1 parent c962993 commit 813b4d1

File tree

4 files changed

+171
-7
lines changed

4 files changed

+171
-7
lines changed

mypy/config_parser.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -650,17 +650,15 @@ def parse_mypy_comments(
650650
Returns a dictionary of options to be applied and a list of error messages
651651
generated.
652652
"""
653-
654653
errors: list[tuple[int, str]] = []
655-
sections = {}
654+
sections: dict[str, object] = {"enable_error_code": [], "disable_error_code": []}
656655

657656
for lineno, line in args:
658657
# In order to easily match the behavior for bools, we abuse configparser.
659658
# Oddly, the only way to get the SectionProxy object with the getboolean
660659
# method is to create a config parser.
661660
parser = configparser.RawConfigParser()
662661
options, parse_errors = mypy_comments_to_config_map(line, template)
663-
664662
if "python_version" in options:
665663
errors.append((lineno, "python_version not supported in inline configuration"))
666664
del options["python_version"]
@@ -690,9 +688,24 @@ def set_strict_flags() -> None:
690688
'(see "mypy -h" for the list of flags enabled in strict mode)',
691689
)
692690
)
693-
691+
# Because this is currently special-cased
692+
# (the new_sections for an inline config *always* includes 'disable_error_code' and
693+
# 'enable_error_code' fields, usually empty, which overwrite the old ones),
694+
# we have to manipulate them specially.
695+
# This could use a refactor, but so could the whole subsystem.
696+
if (
697+
"enable_error_code" in new_sections
698+
and isinstance(neec := new_sections["enable_error_code"], list)
699+
and isinstance(eec := sections.get("enable_error_code", []), list)
700+
):
701+
new_sections["enable_error_code"] = sorted(set(neec + eec))
702+
if (
703+
"disable_error_code" in new_sections
704+
and isinstance(ndec := new_sections["disable_error_code"], list)
705+
and isinstance(dec := sections.get("disable_error_code", []), list)
706+
):
707+
new_sections["disable_error_code"] = sorted(set(ndec + dec))
694708
sections.update(new_sections)
695-
696709
return sections, errors
697710

698711

mypy/errorcodes.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ def __init__(
3737
def __str__(self) -> str:
3838
return f"<ErrorCode {self.code}>"
3939

40+
def __repr__(self) -> str:
41+
"""This doesn't fulfill the goals of repr but it's better than the default view."""
42+
return f"<ErrorCode {self.category}: {self.code}>"
43+
4044
def __eq__(self, other: object) -> bool:
4145
if not isinstance(other, ErrorCode):
4246
return False

mypy/options.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,6 @@ def apply_changes(self, changes: dict[str, object]) -> Options:
507507
code = error_codes[code_str]
508508
new_options.enabled_error_codes.add(code)
509509
new_options.disabled_error_codes.discard(code)
510-
511510
return new_options
512511

513512
def compare_stable(self, other_snapshot: dict[str, object]) -> bool:

test-data/unit/check-inline-config.test

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,99 @@ enable_error_code = ignore-without-code, truthy-bool
211211
\[mypy-tests.*]
212212
disable_error_code = ignore-without-code
213213

214+
[case testInlineErrorCodesOverrideConfigSmall]
215+
# flags: --config-file tmp/mypy.ini
216+
import tests.baz
217+
[file tests/__init__.py]
218+
[file tests/baz.py]
219+
42 + "no" # type: ignore
220+
221+
[file mypy.ini]
222+
\[mypy]
223+
enable_error_code = ignore-without-code, truthy-bool
224+
225+
\[mypy-tests.*]
226+
disable_error_code = ignore-without-code
227+
228+
[case testInlineErrorCodesOverrideConfigSmall2]
229+
# flags: --config-file tmp/mypy.ini
230+
import tests.bar
231+
import tests.baz
232+
[file tests/__init__.py]
233+
[file tests/baz.py]
234+
42 + "no" # type: ignore
235+
[file tests/bar.py]
236+
# mypy: enable-error-code="ignore-without-code"
237+
238+
def foo() -> int: ...
239+
if foo: ... # E: Function "foo" could always be true in boolean context
240+
42 + "no" # type: ignore # E: "type: ignore" comment without error code (consider "type: ignore[operator]" instead)
241+
242+
[file mypy.ini]
243+
\[mypy]
244+
enable_error_code = ignore-without-code, truthy-bool
245+
246+
\[mypy-tests.*]
247+
disable_error_code = ignore-without-code
248+
249+
250+
[case testInlineErrorCodesOverrideConfigSmallBackward]
251+
# flags: --config-file tmp/mypy.ini
252+
import tests.bar
253+
import tests.baz
254+
[file tests/__init__.py]
255+
[file tests/baz.py]
256+
42 + "no" # type: ignore # E: "type: ignore" comment without error code (consider "type: ignore[operator]" instead)
257+
[file tests/bar.py]
258+
# mypy: disable-error-code="ignore-without-code"
259+
42 + "no" # type: ignore
260+
261+
[file mypy.ini]
262+
\[mypy]
263+
enable_error_code = ignore-without-code, truthy-bool
264+
265+
\[mypy-tests.*]
266+
enable_error_code = ignore-without-code
267+
268+
[case testInlineOverrideConfig]
269+
# flags: --config-file tmp/mypy.ini
270+
import foo
271+
import tests.bar
272+
import tests.baz
273+
[file foo.py]
274+
# mypy: disable-error-code="truthy-bool"
275+
class Foo:
276+
pass
277+
278+
foo = Foo()
279+
if foo: ...
280+
42 # type: ignore # E: Unused "type: ignore" comment
281+
282+
[file tests/__init__.py]
283+
[file tests/bar.py]
284+
# mypy: warn_unused_ignores
285+
286+
def foo() -> int: ...
287+
if foo: ... # E: Function "foo" could always be true in boolean context
288+
42 # type: ignore # E: Unused "type: ignore" comment
289+
290+
[file tests/baz.py]
291+
# mypy: disable-error-code="truthy-bool"
292+
class Foo:
293+
pass
294+
295+
foo = Foo()
296+
if foo: ...
297+
42 # type: ignore
298+
299+
[file mypy.ini]
300+
\[mypy]
301+
warn_unused_ignores = True
302+
303+
\[mypy-tests.*]
304+
warn_unused_ignores = False
305+
306+
214307
[case testIgnoreErrorsSimple]
215308
# mypy: ignore-errors=True
216309

@@ -324,6 +417,61 @@ foo = Foo()
324417
if foo: ...
325418
42 + "no" # type: ignore
326419

327-
328420
[case testInlinePythonVersion]
329421
# mypy: python-version=3.10 # E: python_version not supported in inline configuration
422+
423+
[case testInlineErrorCodesArentRuinedByOthersBaseCase]
424+
# mypy: disable-error-code=name-defined
425+
a
426+
427+
[case testInlineErrorCodesArentRuinedByOthersInvalid]
428+
# mypy: disable-error-code=name-defined
429+
# mypy: AMONGUS
430+
a
431+
[out]
432+
main:2: error: Unrecognized option: amongus = True
433+
434+
[case testInlineErrorCodesArentRuinedByOthersInvalidBefore]
435+
# mypy: AMONGUS
436+
# mypy: disable-error-code=name-defined
437+
a
438+
[out]
439+
main:1: error: Unrecognized option: amongus = True
440+
441+
[case testInlineErrorCodesArentRuinedByOthersSe]
442+
# mypy: disable-error-code=name-defined
443+
# mypy: strict-equality
444+
def is_magic(x: bytes) -> bool:
445+
y
446+
return x == 'magic' # E: Unsupported left operand type for == ("bytes")
447+
448+
[case testInlineConfigErrorCodesOffAndOn]
449+
# mypy: disable-error-code=name-defined
450+
# mypy: enable-error-code=name-defined
451+
a # E: Name "a" is not defined
452+
453+
[case testInlineConfigErrorCodesOnAndOff]
454+
# mypy: enable-error-code=name-defined
455+
# mypy: disable-error-code=name-defined
456+
a # E: Name "a" is not defined
457+
458+
[case testConfigFileErrorCodesOnAndOff]
459+
# flags: --config-file tmp/mypy.ini
460+
import foo
461+
[file foo.py]
462+
42 + "no" # type: ignore # E: "type: ignore" comment without error code (consider "type: ignore[operator]" instead)
463+
[file mypy.ini]
464+
\[mypy]
465+
enable_error_code = ignore-without-code
466+
disable_error_code = ignore-without-code
467+
468+
[case testInlineConfigBaseCaseWui]
469+
# mypy: warn_unused_ignores
470+
x = 1 # type: ignore # E: Unused "type: ignore" comment
471+
472+
[case testInlineConfigIsntRuinedByOthersInvalidWui]
473+
# mypy: warn_unused_ignores
474+
# mypy: AMONGUS
475+
x = 1 # type: ignore # E: Unused "type: ignore" comment
476+
[out]
477+
main:2: error: Unrecognized option: amongus = True

0 commit comments

Comments
 (0)