From 26db8cbd3c8f739f28f770f82becaab626e0778e Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Fri, 2 Jan 2026 15:48:43 -0800 Subject: [PATCH 1/3] Support per-module strict config Supersedes #12174 I'd been rewriting that PR in place, but is easier to just make a new one. See #18070 for the extra checks related change. --- mypy/config_parser.py | 18 +++++++++++++----- mypy/main.py | 15 +++------------ mypy/options.py | 2 +- mypy/stubtest.py | 7 ++----- mypy/test/testfinegrained.py | 2 +- test-data/unit/check-flags.test | 27 +++++++++++++++++++++++++++ 6 files changed, 47 insertions(+), 24 deletions(-) diff --git a/mypy/config_parser.py b/mypy/config_parser.py index 0effaaba20a0..cb4596473d89 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -301,7 +301,7 @@ def _find_config_file( def parse_config_file( options: Options, - set_strict_flags: Callable[[], None], + strict_flag_assignments: Sequence[tuple[str, object]], filename: str | None, stdout: TextIO | None = None, stderr: TextIO | None = None, @@ -327,6 +327,9 @@ def parse_config_file( options.config_file = file_read os.environ["MYPY_CONFIG_FILE_DIR"] = os.path.dirname(os.path.abspath(file_read)) + def set_strict_flags(updates: dict[str, object]) -> None: + updates.update(strict_flag_assignments) + if "mypy" not in parser: if filename or os.path.basename(file_read) not in defaults.SHARED_CONFIG_NAMES: print(f"{file_read}: No [mypy] section in config file", file=stderr) @@ -340,11 +343,16 @@ def parse_config_file( setattr(options, k, v) options.report_dirs.update(report_dirs) + def set_strict_flags_section(updates: dict[str, object]) -> None: + for dest, value in strict_flag_assignments: + if dest in PER_MODULE_OPTIONS: + updates[dest] = value + for name, section in parser.items(): if name.startswith("mypy-"): prefix = get_prefix(file_read, name) updates, report_dirs = parse_section( - prefix, options, set_strict_flags, section, config_types, stderr + prefix, options, set_strict_flags_section, section, config_types, stderr ) if report_dirs: print( @@ -483,7 +491,7 @@ def destructure_overrides(toml_data: dict[str, Any]) -> dict[str, Any]: def parse_section( prefix: str, template: Options, - set_strict_flags: Callable[[], None], + set_strict_flags: Callable[[dict[str, object]], None], section: Mapping[str, Any], config_types: dict[str, Any], stderr: TextIO = sys.stderr, @@ -576,7 +584,7 @@ def parse_section( continue if key == "strict": if v: - set_strict_flags() + set_strict_flags(results) continue results[options_key] = v @@ -675,7 +683,7 @@ def parse_mypy_comments( stderr = StringIO() strict_found = False - def set_strict_flags() -> None: + def set_strict_flags(updates: dict[str, object]) -> None: nonlocal strict_found strict_found = True diff --git a/mypy/main.py b/mypy/main.py index 864fe4b4febf..10dfb6bd95cf 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -1397,21 +1397,15 @@ def process_options( parser.error(f"Cannot find config file '{config_file}'") options = Options() - strict_option_set = False - - def set_strict_flags() -> None: - nonlocal strict_option_set - strict_option_set = True - for dest, value in strict_flag_assignments: - setattr(options, dest, value) # Parse config file first, so command line can override. - parse_config_file(options, set_strict_flags, config_file, stdout, stderr) + parse_config_file(options, strict_flag_assignments, config_file, stdout, stderr) # Set strict flags before parsing (if strict mode enabled), so other command # line options can override. if getattr(dummy, "special-opts:strict"): - set_strict_flags() + for dest, value in strict_flag_assignments: + setattr(options, dest, value) # Override cache_dir if provided in the environment environ_cache_dir = os.getenv("MYPY_CACHE_DIR", "") @@ -1529,9 +1523,6 @@ def set_strict_flags() -> None: if options.logical_deps: options.cache_fine_grained = True - if options.strict_concatenate and not strict_option_set: - print("Warning: --strict-concatenate is deprecated; use --extra-checks instead") - # Set target. if special_opts.modules + special_opts.packages: options.build_type = BuildType.MODULE diff --git a/mypy/options.py b/mypy/options.py index cb5088af7e79..537b97bfe53c 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -238,7 +238,7 @@ def __init__(self) -> None: # Disable treating bytearray and memoryview as subtypes of bytes self.strict_bytes = False - # Deprecated, use extra_checks instead. + # Make arguments prepended via Concatenate be truly positional-only. self.strict_concatenate = False # Enable additional checks that are technically correct but impractical. diff --git a/mypy/stubtest.py b/mypy/stubtest.py index ff5a04b740eb..ce5e15727214 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -2279,15 +2279,12 @@ def test_stubs(args: _Arguments, use_builtins_fixtures: bool = False) -> int: options.abs_custom_typeshed_dir = os.path.abspath(options.custom_typeshed_dir) options.config_file = args.mypy_config_file options.use_builtins_fixtures = use_builtins_fixtures + options.per_module_options = {} options.show_traceback = args.show_traceback options.pdb = args.pdb if options.config_file: - - def set_strict_flags() -> None: # not needed yet - return - - parse_config_file(options, set_strict_flags, options.config_file, sys.stdout, sys.stderr) + parse_config_file(options, [], options.config_file, sys.stdout, sys.stderr) def error_callback(msg: str) -> typing.NoReturn: print(_style("error:", color="red", bold=True), msg) diff --git a/mypy/test/testfinegrained.py b/mypy/test/testfinegrained.py index b098c1fb0ad2..2db93d7b7e58 100644 --- a/mypy/test/testfinegrained.py +++ b/mypy/test/testfinegrained.py @@ -161,7 +161,7 @@ def get_options(self, source: str, testcase: DataDrivenTestCase, build_cache: bo for name, _ in testcase.files: if "mypy.ini" in name or "pyproject.toml" in name: - parse_config_file(options, lambda: None, name) + parse_config_file(options, [], name) break return options diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index 8a76cfa9675f..570ff530f191 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -2152,6 +2152,33 @@ disallow_subclassing_any = true module = 'm' disallow_subclassing_any = false +[case testStrictPerModule] +# flags: --config-file tmp/mypy.ini + +import strictmodule +import loosemodule +a = 0 # type: ignore + +[file strictmodule.py] +def foo(): # E: Function is missing a return type annotation \ + # N: Use "-> None" if function does not return a value + 1 + "asdf" # E: Unsupported operand types for + ("int" and "str") + +a = 0 # type: ignore # E: Unused "type: ignore" comment + + +[file loosemodule.py] +def foo(): + 1 + "asdf" + +a = 0 # type: ignore +1 + "asdf" # E: Unsupported operand types for + ("int" and "str") + +[file mypy.ini] +[mypy] +strict = False +[mypy-strictmodule] +strict = True [case testNoImplicitOptionalPerModule] # flags: --config-file tmp/mypy.ini From e34fa07e4cf45701cccfa758d121b5e9030fe80c Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Fri, 2 Jan 2026 16:17:10 -0800 Subject: [PATCH 2/3] fix escaping --- test-data/unit/check-flags.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index 570ff530f191..02dfc8326b5c 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -2175,9 +2175,9 @@ a = 0 # type: ignore 1 + "asdf" # E: Unsupported operand types for + ("int" and "str") [file mypy.ini] -[mypy] +\[mypy] strict = False -[mypy-strictmodule] +\[mypy-strictmodule] strict = True [case testNoImplicitOptionalPerModule] From 1709cb3a407305e2143bb49cd0de1d18e1bc2cb3 Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Fri, 2 Jan 2026 20:49:38 -0800 Subject: [PATCH 3/3] sort keys --- mypy/config_parser.py | 2 +- test-data/unit/check-flags.test | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/mypy/config_parser.py b/mypy/config_parser.py index cb4596473d89..a515cbcc3844 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -510,7 +510,7 @@ def parse_section( "disabled_error_codes": "disable_error_code", } - for key in section: + for key in sorted(section, key=lambda k: -1 if k in {"strict"} else 0): invert = False options_key = key if key in config_types: diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index 02dfc8326b5c..05ec49f27057 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -2180,6 +2180,29 @@ strict = False \[mypy-strictmodule] strict = True + +[case testStrictPerModuleOverride] +# flags: --config-file tmp/mypy.ini + +import strictmodule +import strictermodule + +[file strictmodule.py] +x: list +0 # type: ignore + +[file strictermodule.py] +x: list # E: Missing type arguments for generic type "list" +0 # type: ignore # E: Unused "type: ignore" comment + +[file mypy.ini] +\[mypy] +disallow_any_generics = false +strict = true +warn_unused_ignores = false +\[mypy-strictermodule] +strict = true + [case testNoImplicitOptionalPerModule] # flags: --config-file tmp/mypy.ini import m