diff --git a/mypy/main.py b/mypy/main.py index 49a395b478b3..f177bb1c2062 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -23,7 +23,7 @@ from mypy.find_sources import InvalidSourceList, create_source_list from mypy.fscache import FileSystemCache from mypy.modulefinder import BuildSource, FindModuleCache, SearchPaths, get_search_dirs, mypy_path -from mypy.options import COMPLETE_FEATURES, INCOMPLETE_FEATURES, BuildType, Options +from mypy.options import INCOMPLETE_FEATURES, BuildType, Options from mypy.split_namespace import SplitNamespace from mypy.version import __version__ @@ -1336,13 +1336,7 @@ def set_strict_flags() -> None: validate_package_allow_list(options.untyped_calls_exclude) options.process_error_codes(error_callback=parser.error) - - # Validate incomplete features. - for feature in options.enable_incomplete_feature: - if feature not in INCOMPLETE_FEATURES | COMPLETE_FEATURES: - parser.error(f"Unknown incomplete feature: {feature}") - if feature in COMPLETE_FEATURES: - print(f"Warning: {feature} is already enabled by default") + options.process_incomplete_features(error_callback=parser.error, warning_callback=print) # Compute absolute path for custom typeshed (if present). if options.custom_typeshed_dir is not None: diff --git a/mypy/options.py b/mypy/options.py index 5ab397e0e156..5e64d5e40035 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -433,6 +433,16 @@ def process_error_codes(self, *, error_callback: Callable[[str], Any]) -> None: # Enabling an error code always overrides disabling self.disabled_error_codes -= self.enabled_error_codes + def process_incomplete_features( + self, *, error_callback: Callable[[str], Any], warning_callback: Callable[[str], Any] + ) -> None: + # Validate incomplete features. + for feature in self.enable_incomplete_feature: + if feature not in INCOMPLETE_FEATURES | COMPLETE_FEATURES: + error_callback(f"Unknown incomplete feature: {feature}") + if feature in COMPLETE_FEATURES: + warning_callback(f"Warning: {feature} is already enabled by default") + def apply_changes(self, changes: dict[str, object]) -> Options: # Note: effects of this method *must* be idempotent. new_options = Options() diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 6299f21e48e9..ca17ccfe2f5b 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -1945,7 +1945,13 @@ def error_callback(msg: str) -> typing.NoReturn: print(_style("error:", color="red", bold=True), msg) sys.exit(1) + def warning_callback(msg: str) -> None: + print(_style("warning:", color="yellow", bold=True), msg) + options.process_error_codes(error_callback=error_callback) + options.process_incomplete_features( + error_callback=error_callback, warning_callback=warning_callback + ) try: modules = build_stubs(modules, options, find_submodules=not args.check_typeshed) diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 1cc6c38e6e85..c10c683ffdac 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -2490,6 +2490,20 @@ def test_config_file_error_codes(self) -> None: output = run_stubtest(stub=stub, runtime=runtime, options=[], config_file=config_file) assert output == "Success: no issues found in 1 module\n" + def test_config_file_wrong_incomplete_feature(self) -> None: + runtime = "x = 1\n" + stub = "x: int\n" + config_file = "[mypy]\nenable_incomplete_feature = Unpack\n" + output = run_stubtest(stub=stub, runtime=runtime, options=[], config_file=config_file) + assert output == ( + "warning: Warning: Unpack is already enabled by default\n" + "Success: no issues found in 1 module\n" + ) + + config_file = "[mypy]\nenable_incomplete_feature = not-a-valid-name\n" + with self.assertRaises(SystemExit): + run_stubtest(stub=stub, runtime=runtime, options=[], config_file=config_file) + def test_no_modules(self) -> None: output = io.StringIO() with contextlib.redirect_stdout(output):