Skip to content

Commit fe31334

Browse files
sigvefbelm0pre-commit-ci[bot]ilevkivskyi
authored
Support error codes from plugins in options (#19719)
Mypy has options for enabling or disabling specific error codes. These work fine, except that it is not possible to enable or disable error codes from plugins, only mypy's original error codes. The crux of the issue is that mypy validates and rejects unknown error codes passed in the options before it loads plugins and learns about the any error codes that might get registered. There are many ways to solve this. This commit tries to find a pragmatic solution where the relevant options parsing is deferred until after plugin loading. Error code validation in the config parser, where plugins are not loaded yet, is also skipped entirely, since the error code options are re-validated later anyway. This means that this commit introduces a small observable change in behavior when running with invalid error codes specified, as shown in the test test_config_file_error_codes_invalid. This fixes #12987. --------- Co-authored-by: John Belmonte <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Ivan Levkivskyi <[email protected]>
1 parent a35e84b commit fe31334

File tree

6 files changed

+67
-37
lines changed

6 files changed

+67
-37
lines changed

mypy/build.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ def __init__(self, manager: BuildManager, graph: Graph) -> None:
143143
self.errors: list[str] = [] # Filled in by build if desired
144144

145145

146+
def build_error(msg: str) -> NoReturn:
147+
raise CompileError([f"mypy: error: {msg}"])
148+
149+
146150
def build(
147151
sources: list[BuildSource],
148152
options: Options,
@@ -241,6 +245,9 @@ def _build(
241245
errors = Errors(options, read_source=lambda path: read_py_file(path, cached_read))
242246
plugin, snapshot = load_plugins(options, errors, stdout, extra_plugins)
243247

248+
# Validate error codes after plugins are loaded.
249+
options.process_error_codes(error_callback=build_error)
250+
244251
# Add catch-all .gitignore to cache dir if we created it
245252
cache_dir_existed = os.path.isdir(options.cache_dir)
246253

mypy/config_parser.py

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
import sys
99
from io import StringIO
1010

11-
from mypy.errorcodes import error_codes
12-
1311
if sys.version_info >= (3, 11):
1412
import tomllib
1513
else:
@@ -87,15 +85,6 @@ def complain(x: object, additional_info: str = "") -> Never:
8785
complain(v)
8886

8987

90-
def validate_codes(codes: list[str]) -> list[str]:
91-
invalid_codes = set(codes) - set(error_codes.keys())
92-
if invalid_codes:
93-
raise argparse.ArgumentTypeError(
94-
f"Invalid error code(s): {', '.join(sorted(invalid_codes))}"
95-
)
96-
return codes
97-
98-
9988
def validate_package_allow_list(allow_list: list[str]) -> list[str]:
10089
for p in allow_list:
10190
msg = f"Invalid allow list entry: {p}"
@@ -209,8 +198,8 @@ def split_commas(value: str) -> list[str]:
209198
[p.strip() for p in split_commas(s)]
210199
),
211200
"enable_incomplete_feature": lambda s: [p.strip() for p in split_commas(s)],
212-
"disable_error_code": lambda s: validate_codes([p.strip() for p in split_commas(s)]),
213-
"enable_error_code": lambda s: validate_codes([p.strip() for p in split_commas(s)]),
201+
"disable_error_code": lambda s: [p.strip() for p in split_commas(s)],
202+
"enable_error_code": lambda s: [p.strip() for p in split_commas(s)],
214203
"package_root": lambda s: [p.strip() for p in split_commas(s)],
215204
"cache_dir": expand_path,
216205
"python_executable": expand_path,
@@ -234,8 +223,8 @@ def split_commas(value: str) -> list[str]:
234223
"always_false": try_split,
235224
"untyped_calls_exclude": lambda s: validate_package_allow_list(try_split(s)),
236225
"enable_incomplete_feature": try_split,
237-
"disable_error_code": lambda s: validate_codes(try_split(s)),
238-
"enable_error_code": lambda s: validate_codes(try_split(s)),
226+
"disable_error_code": lambda s: try_split(s),
227+
"enable_error_code": lambda s: try_split(s),
239228
"package_root": try_split,
240229
"exclude": str_or_array_as_list,
241230
"packages": try_split,

mypy/main.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1462,7 +1462,6 @@ def set_strict_flags() -> None:
14621462
validate_package_allow_list(options.untyped_calls_exclude)
14631463
validate_package_allow_list(options.deprecated_calls_exclude)
14641464

1465-
options.process_error_codes(error_callback=parser.error)
14661465
options.process_incomplete_features(error_callback=parser.error, warning_callback=print)
14671466

14681467
# Compute absolute path for custom typeshed (if present).

mypy/test/teststubtest.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from collections.abc import Iterator
1313
from typing import Any, Callable
1414

15+
from pytest import raises
16+
1517
import mypy.stubtest
1618
from mypy import build, nodes
1719
from mypy.modulefinder import BuildSource
@@ -171,7 +173,12 @@ def build_helper(source: str) -> build.BuildResult:
171173

172174

173175
def run_stubtest_with_stderr(
174-
stub: str, runtime: str, options: list[str], config_file: str | None = None
176+
stub: str,
177+
runtime: str,
178+
options: list[str],
179+
config_file: str | None = None,
180+
output: io.StringIO | None = None,
181+
outerr: io.StringIO | None = None,
175182
) -> tuple[str, str]:
176183
with use_tmp_dir(TEST_MODULE_NAME) as tmp_dir:
177184
with open("builtins.pyi", "w") as f:
@@ -188,8 +195,8 @@ def run_stubtest_with_stderr(
188195
with open(f"{TEST_MODULE_NAME}_config.ini", "w") as f:
189196
f.write(config_file)
190197
options = options + ["--mypy-config-file", f"{TEST_MODULE_NAME}_config.ini"]
191-
output = io.StringIO()
192-
outerr = io.StringIO()
198+
output = io.StringIO() if output is None else output
199+
outerr = io.StringIO() if outerr is None else outerr
193200
with contextlib.redirect_stdout(output), contextlib.redirect_stderr(outerr):
194201
test_stubs(parse_options([TEST_MODULE_NAME] + options), use_builtins_fixtures=True)
195202
filtered_output = remove_color_code(
@@ -2888,14 +2895,20 @@ def test_config_file_error_codes_invalid(self) -> None:
28882895
runtime = "temp = 5\n"
28892896
stub = "temp: int\n"
28902897
config_file = "[mypy]\ndisable_error_code = not-a-valid-name\n"
2891-
output, outerr = run_stubtest_with_stderr(
2892-
stub=stub, runtime=runtime, options=[], config_file=config_file
2893-
)
2894-
assert output == "Success: no issues found in 1 module\n"
2895-
assert outerr == (
2896-
"test_module_config.ini: [mypy]: disable_error_code: "
2897-
"Invalid error code(s): not-a-valid-name\n"
2898-
)
2898+
output = io.StringIO()
2899+
outerr = io.StringIO()
2900+
with raises(SystemExit):
2901+
run_stubtest_with_stderr(
2902+
stub=stub,
2903+
runtime=runtime,
2904+
options=[],
2905+
config_file=config_file,
2906+
output=output,
2907+
outerr=outerr,
2908+
)
2909+
2910+
assert output.getvalue() == "error: Invalid error code(s): not-a-valid-name\n"
2911+
assert outerr.getvalue() == ""
28992912

29002913
def test_config_file_wrong_incomplete_feature(self) -> None:
29012914
runtime = "x = 1\n"
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[case testCustomErrorCodeFromPluginIsTargetable]
2+
# flags: --config-file tmp/mypy.ini --show-error-codes
3+
4+
def main() -> None:
5+
return
6+
main() # E: Custom error [custom]
7+
8+
[file mypy.ini]
9+
\[mypy]
10+
plugins=<ROOT>/test-data/unit/plugins/custom_errorcode.py
11+
12+
[case testCustomErrorCodeCanBeDisabled]
13+
# flags: --config-file tmp/mypy.ini --show-error-codes --disable-error-code=custom
14+
15+
def main() -> None:
16+
return
17+
main() # no output expected when disabled
18+
19+
[file mypy.ini]
20+
\[mypy]
21+
plugins=<ROOT>/test-data/unit/plugins/custom_errorcode.py
22+
23+
[case testCustomErrorCodeCanBeReenabled]
24+
# flags: --config-file tmp/mypy.ini --show-error-codes --disable-error-code=custom --enable-error-code=custom
25+
26+
def main() -> None:
27+
return
28+
main() # E: Custom error [custom]
29+
30+
[file mypy.ini]
31+
\[mypy]
32+
plugins=<ROOT>/test-data/unit/plugins/custom_errorcode.py

test-data/unit/cmdline.test

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -960,8 +960,6 @@ src/foo/bar.py: note: Common resolutions include: a) adding `__init__.py` somewh
960960
[file test.py]
961961
x = 1
962962
[out]
963-
usage: mypy [-h] [-v] [-V] [more options; see below]
964-
[-m MODULE] [-p PACKAGE] [-c PROGRAM_TEXT] [files ...]
965963
mypy: error: Invalid error code(s): YOLO
966964
== Return code: 2
967965

@@ -970,8 +968,6 @@ mypy: error: Invalid error code(s): YOLO
970968
[file test.py]
971969
x = 1
972970
[out]
973-
usage: mypy [-h] [-v] [-V] [more options; see below]
974-
[-m MODULE] [-p PACKAGE] [-c PROGRAM_TEXT] [files ...]
975971
mypy: error: Invalid error code(s): YOLO
976972
== Return code: 2
977973

@@ -980,8 +976,6 @@ mypy: error: Invalid error code(s): YOLO
980976
[file test.py]
981977
x = 1
982978
[out]
983-
usage: mypy [-h] [-v] [-V] [more options; see below]
984-
[-m MODULE] [-p PACKAGE] [-c PROGRAM_TEXT] [files ...]
985979
mypy: error: Invalid error code(s): YOLO, YOLO2
986980
== Return code: 2
987981

@@ -990,8 +984,6 @@ mypy: error: Invalid error code(s): YOLO, YOLO2
990984
[file test.py]
991985
x = 1
992986
[out]
993-
usage: mypy [-h] [-v] [-V] [more options; see below]
994-
[-m MODULE] [-p PACKAGE] [-c PROGRAM_TEXT] [files ...]
995987
mypy: error: Invalid error code(s): YOLO
996988
== Return code: 2
997989

@@ -1000,8 +992,6 @@ mypy: error: Invalid error code(s): YOLO
1000992
[file test.py]
1001993
x = 1
1002994
[out]
1003-
usage: mypy [-h] [-v] [-V] [more options; see below]
1004-
[-m MODULE] [-p PACKAGE] [-c PROGRAM_TEXT] [files ...]
1005995
mypy: error: Invalid error code(s): YOLO
1006996
== Return code: 2
1007997

0 commit comments

Comments
 (0)