10000 Allow per-module error codes by ilevkivskyi · Pull Request #13502 · python/mypy · GitHub
[go: up one dir, main page]

Skip to content

Allow per-module error codes #13502

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

Merged
merged 8 commits into from
Aug 25, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Add override tests; better code organization
  • Loading branch information
ilevkivskyi committed Aug 24, 2022
commit 7fd725859400c76913908122fd5ea0bef3ce4fb5
33 changes: 33 additions & 0 deletions docs/source/error_codes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,36 @@ which enables the ``no-untyped-def`` error code.
You can use :option:`--enable-error-code <mypy --enable-error-code>` to
enable specific error codes that don't have a dedicated command-line
flag or config file setting.

Per-module enabling/disabling error codes
-----------------------------------------

You can use :ref:`configuration file <config-file>` sections to enable or
disable specific error codes only in some modules. For example, this ``mypy.ini``
config will enable non-annotated empty containers in tests, while keeping
other parts of code checked in strict mode:

.. code-block:: ini

[mypy]
strict = True

[mypy-tests.*]
allow_untyped_defs = True
allow_untyped_calls = True
disable_error_code = var-annotated, has-type

Note that per-module enabling/disabling acts as override over the global
options. So that you don't need to repeat the error code lists for each
module if you have them in global config section. For example:

.. code-block:: ini

[mypy]
enable_error_code = truthy-bool, ignore-without-code, unused-awaitable

[mypy-extensions.*]
disable_error_code = unused-awaitable

The above config will allow unused awaitables in extension modules, but will
still keep the other two error codes enabled.
2 changes: 0 additions & 2 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,6 @@ def _build(
options.show_error_end,
lambda path: read_py_file(path, cached_read),
options.show_absolute_path,
options.enabled_error_codes,
options.disabled_error_codes,
options.many_errors_threshold,
options,
)
Expand Down
7 changes: 7 additions & 0 deletions mypy/config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,13 @@ def parse_config_file(
file=stderr,
)
updates = {k: v for k, v in updates.items() if k in PER_MODULE_OPTIONS}

# These two flags act as per-module overrides, so store the empty defaults.
if "disable_error_code" not in updates:
updates["disable_error_code"] = []
if "enable_error_code" not in updates:
updates["enable_error_code"] = []

globs = name[5:]
for glob in globs.split(","):
# For backwards compatibility, replace (back)slashes with dots.
Expand Down
24 changes: 5 additions & 19 deletions mypy/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,6 @@ def __init__(
show_error_end: bool = False,
read_source: Callable[[str], list[str] | None] | None = None,
show_absolute_path: bool = False,
enabled_error_codes: set[ErrorCode] | None = None,
disabled_error_codes: set[ErrorCode] | None = None,
many_errors_threshold: int = -1,
options: Options | None = None,
) -> None:
Expand All @@ -277,8 +275,6 @@ def __init__(
assert show_column_numbers, "Inconsistent formatting, must be prevented by argparse"
# We use fscache to read source code when showing snippets.
self.read_source = read_source
self.enabled_error_codes = enabled_error_codes or set()
self.disabled_error_codes = disabled_error_codes or set()
self.many_errors_threshold = many_errors_threshold
self.options = options
self.initialize()
Expand Down Expand Up @@ -588,25 +584,15 @@ def is_ignored_error(self, line: int, info: ErrorInfo, ignores: dict[int, list[s
return False

def is_error_code_enabled(self, error_code: ErrorCode) -> bool:
# Start with globally disabled/enabled codes.
current_mod_disabled = self.disabled_error_codes
current_mod_enabled = self.enabled_error_codes

module = self.current_module()
if self.options and module is not None:
# Clone is cached, so it is OK to call this often.
current_mod_options = self.options.clone_for_module(module)

# Similar to global codes enabling overrides disabling, so we start from latter.
for code_str in current_mod_options.disable_error_code:
code = error_codes[code_str]
current_mod_disabled.add(code)
current_mod_enabled.discard(code)

for code_str in current_mod_options.enable_error_code:
code = error_codes[code_str]
current_mod_enabled.add(code)
current_mod_disabled.discard(code)
current_mod_disabled = current_mod_options.disabled_error_codes
current_mod_enabled = current_mod_options.enabled_error_codes
else:
current_mod_disabled = set()
current_mod_enabled = set()

if error_code in current_mod_disabled:
return False
Expand Down
30 changes: 25 additions & 5 deletions mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@
import pprint
import re
import sys
from typing import TYPE_CHECKING, Any, Callable, Mapping, Pattern
from typing import Any, Callable, Dict, Mapping, Pattern
from typing_extensions import Final

from mypy import defaults
from mypy.errorcodes import ErrorCode, error_codes
from mypy.util import get_class_descriptors, replace_object_state

if TYPE_CHECKING:
from mypy.errorcodes import ErrorCode


class BuildType:
STANDARD: Final = 0
Expand All @@ -28,6 +26,7 @@ class BuildType:
"check_untyped_defs",
"debug_cache",
"disable_error_code",
"disabled_error_codes",
"disallow_any_decorated",
"disallow_any_explicit",
"disallow_any_expr",
Expand All @@ -39,6 +38,7 @@ class BuildType:
"disallow_untyped_decorators",
6D47 "disallow_untyped_defs",
"enable_error_code",
"enabled_error_codes",
"follow_imports",
"follow_imports_for_stubs",
"ignore_errors",
Expand Down Expand Up @@ -349,6 +349,20 @@ def apply_changes(self, changes: dict[str, object]) -> Options:
# This is the only option for which a per-module and a global
# option sometimes beheave differently.
new_options.ignore_missing_imports_per_module = True

# These two act as overrides, so apply them when cloning.
# Similar to global codes enabling overrides disabling, so we start from latter.
new_options.disabled_error_codes = self.disabled_error_codes.copy()
new_options.enabled_error_codes = self.enabled_error_codes.copy()
for code_str in new_options.disable_error_code:
code = error_codes[code_str]
new_options.disabled_error_codes.add(code)
new_options.enabled_error_codes.discard(code)
for code_str in new_options.enable_error_code:
code = error_codes[code_str]
new_options.enabled_error_codes.add(code)
new_options.disabled_error_codes.discard(code)

return new_options

def build_per_module_cache(self) -> None:
Expand Down Expand Up @@ -448,4 +462,10 @@ def compile_glob(self, s: str) -> Pattern[str]:
return re.compile(expr + "\\Z")

def select_options_affecting_cache(self) -> Mapping[str, object]:
return {opt: getattr(self, opt) for opt in OPTIONS_AFFECTING_CACHE}
result: Dict[str, object] = {}
for opt in OPTIONS_AFFECTING_CACHE:
val = getattr(self, opt)
if isinstance(val, set):
val = sorted(val)
result[opt] = val
return result
21 changes: 21 additions & 0 deletions test-data/unit/check-flags.test
Original file line number Diff line number Diff line change
Expand Up @@ -2071,3 +2071,24 @@ strict = True
allow_untyped_defs = True
allow_untyped_calls = True
disable_error_code = var-annotated

[case testPerModuleErrorCodesOverride]
# flags: --config-file tmp/mypy.ini
import tests.foo
import bar
[file bar.py]
def foo() -> int: ...
if foo: ... # E: Function "Callable[[], int]" could always be true in boolean context
42 + "no" # type: ignore # E: "type: ignore" comment without error code (consider "type: ignore[operator]" instead)
[file tests/__init__.py]
[file tests/foo.py]
def foo() -> int: ...
if foo: ... # E: Function "Callable[[], int]" could always be true in boolean context
42 + "no" # type: ignore
[file mypy.ini]
\[mypy]
enable_error_code = ignore-without-code, truthy-bool

\[mypy-tests.*]
disable_error_code = ignore-without-code
[builtins fixtures/async_await.pyi]
0