Skip to content

Fix a bug where inline configurations of error codes would lose their values if accompanied by another inline configuration. #19075

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions mypy/config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -639,17 +639,15 @@ def parse_mypy_comments(
Returns a dictionary of options to be applied and a list of error messages
generated.
"""

errors: list[tuple[int, str]] = []
sections = {}
sections: dict[str, object] = {"enable_error_code": [], "disable_error_code": []}

for lineno, line in args:
# In order to easily match the behavior for bools, we abuse configparser.
# Oddly, the only way to get the SectionProxy object with the getboolean
# method is to create a config parser.
parser = configparser.RawConfigParser()
options, parse_errors = mypy_comments_to_config_map(line, template)

if "python_version" in options:
errors.append((lineno, "python_version not supported in inline configuration"))
del options["python_version"]
Expand Down Expand Up @@ -679,9 +677,24 @@ def set_strict_flags() -> None:
'(see "mypy -h" for the list of flags enabled in strict mode)',
)
)

# Because this is currently special-cased
# (the new_sections for an inline config *always* includes 'disable_error_code' and
# 'enable_error_code' fields, usually empty, which overwrite the old ones),
# we have to manipulate them specially.
# This could use a refactor, but so could the whole subsystem.
if (
"enable_error_code" in new_sections
and isinstance(neec := new_sections["enable_error_code"], list)
and isinstance(eec := sections.get("enable_error_code", []), list)
):
new_sections["enable_error_code"] = sorted(set(neec + eec))
if (
"disable_error_code" in new_sections
and isinstance(ndec := new_sections["disable_error_code"], list)
and isinstance(dec := sections.get("disable_error_code", []), list)
):
new_sections["disable_error_code"] = sorted(set(ndec + dec))
sections.update(new_sections)

return sections, errors


Expand Down
4 changes: 4 additions & 0 deletions mypy/errorcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ def __init__(
def __str__(self) -> str:
return f"<ErrorCode {self.code}>"

def __repr__(self) -> str:
"""This doesn't fulfill the goals of repr but it's better than the default view."""
return self.code

def __eq__(self, other: object) -> bool:
if not isinstance(other, ErrorCode):
return False
Expand Down
1 change: 0 additions & 1 deletion mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,6 @@ def apply_changes(self, changes: dict[str, object]) -> Options:
code = error_codes[code_str]
new_options.enabled_error_codes.add(code)
new_options.disabled_error_codes.discard(code)

return new_options

def compare_stable(self, other_snapshot: dict[str, object]) -> bool:
Expand Down
150 changes: 149 additions & 1 deletion test-data/unit/check-inline-config.test
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,99 @@ enable_error_code = ignore-without-code, truthy-bool
\[mypy-tests.*]
disable_error_code = ignore-without-code

[case testInlineErrorCodesOverrideConfigSmall]
# flags: --config-file tmp/mypy.ini
import tests.baz
[file tests/__init__.py]
[file tests/baz.py]
42 + "no" # type: ignore

[file mypy.ini]
\[mypy]
enable_error_code = ignore-without-code, truthy-bool

\[mypy-tests.*]
disable_error_code = ignore-without-code

[case testInlineErrorCodesOverrideConfigSmall2]
# flags: --config-file tmp/mypy.ini
import tests.bar
import tests.baz
[file tests/__init__.py]
[file tests/baz.py]
42 + "no" # type: ignore
[file tests/bar.py]
# mypy: enable-error-code="ignore-without-code"

def foo() -> int: ...
if foo: ... # E: Function "foo" could always be true in boolean context
42 + "no" # type: ignore # E: "type: ignore" comment without error code (consider "type: ignore[operator]" instead)

[file mypy.ini]
\[mypy]
enable_error_code = ignore-without-code, truthy-bool

\[mypy-tests.*]
disable_error_code = ignore-without-code


[case testInlineErrorCodesOverrideConfigSmallBackward]
# flags: --config-file tmp/mypy.ini
import tests.bar
import tests.baz
[file tests/__init__.py]
[file tests/baz.py]
42 + "no" # type: ignore # E: "type: ignore" comment without error code (consider "type: ignore[operator]" instead)
[file tests/bar.py]
# mypy: disable-error-code="ignore-without-code"
42 + "no" # type: ignore

[file mypy.ini]
\[mypy]
enable_error_code = ignore-without-code, truthy-bool

\[mypy-tests.*]
enable_error_code = ignore-without-code

[case testInlineOverrideConfig]
# flags: --config-file tmp/mypy.ini
import foo
import tests.bar
import tests.baz
[file foo.py]
# mypy: disable-error-code="truthy-bool"
class Foo:
pass

foo = Foo()
if foo: ...
42 # type: ignore # E: Unused "type: ignore" comment

[file tests/__init__.py]
[file tests/bar.py]
# mypy: warn_unused_ignores

def foo() -> int: ...
if foo: ... # E: Function "foo" could always be true in boolean context
42 # type: ignore # E: Unused "type: ignore" comment

[file tests/baz.py]
# mypy: disable-error-code="truthy-bool"
class Foo:
pass

foo = Foo()
if foo: ...
42 # type: ignore

[file mypy.ini]
\[mypy]
warn_unused_ignores = True

\[mypy-tests.*]
warn_unused_ignores = False


[case testIgnoreErrorsSimple]
# mypy: ignore-errors=True

Expand Down Expand Up @@ -324,6 +417,61 @@ foo = Foo()
if foo: ...
42 + "no" # type: ignore


[case testInlinePythonVersion]
# mypy: python-version=3.10 # E: python_version not supported in inline configuration

[case testInlineErrorCodesArentRuinedByOthersBaseCase]
# mypy: disable-error-code=name-defined
a

[case testInlineErrorCodesArentRuinedByOthersInvalid]
# mypy: disable-error-code=name-defined
# mypy: AMONGUS
a
[out]
main:2: error: Unrecognized option: amongus = True

[case testInlineErrorCodesArentRuinedByOthersInvalidBefore]
# mypy: AMONGUS
# mypy: disable-error-code=name-defined
a
[out]
main:1: error: Unrecognized option: amongus = True

[case testInlineErrorCodesArentRuinedByOthersSe]
# mypy: disable-error-code=name-defined
# mypy: strict-equality
def is_magic(x: bytes) -> bool:
y
return x == 'magic' # E: Unsupported left operand type for == ("bytes")

[case testInlineConfigErrorCodesOffAndOn]
# mypy: disable-error-code=name-defined
# mypy: enable-error-code=name-defined
a # E: Name "a" is not defined

[case testInlineConfigErrorCodesOnAndOff]
# mypy: enable-error-code=name-defined
# mypy: disable-error-code=name-defined
a # E: Name "a" is not defined

[case testConfigFileErrorCodesOnAndOff]
# flags: --config-file tmp/mypy.ini
import foo
[file foo.py]
42 + "no" # type: ignore # E: "type: ignore" comment without error code (consider "type: ignore[operator]" instead)
[file mypy.ini]
\[mypy]
enable_error_code = ignore-without-code
disable_error_code = ignore-without-code

[case testInlineConfigBaseCaseWui]
# mypy: warn_unused_ignores
x = 1 # type: ignore # E: Unused "type: ignore" comment

[case testInlineConfigIsntRuinedByOthersInvalidWui]
# mypy: warn_unused_ignores
# mypy: AMONGUS
x = 1 # type: ignore # E: Unused "type: ignore" comment
[out]
main:2: error: Unrecognized option: amongus = True