From 242873a2e8a1d98762b30fcf7b28a699a230279d Mon Sep 17 00:00:00 2001 From: Jannick Kremer Date: Wed, 4 Dec 2024 08:00:03 +0100 Subject: [PATCH] Implement flag to allow typechecking of untyped modules (#17712) Add a flag and config ini options `"follow_untyped_imports"`. Setting it to `True` instructs mypy to typecheck also modules that do not have stubs or a `py.typed` marker. Fixes #8545 --- docs/source/command_line.rst | 4 ++++ docs/source/config_file.rst | 12 ++++++++++++ docs/source/running_mypy.rst | 6 ++++++ mypy/main.py | 5 +++++ mypy/modulefinder.py | 9 +++++++-- mypy/options.py | 3 +++ test-data/unit/pep561.test | 7 +++++++ 7 files changed, 44 insertions(+), 2 deletions(-) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 92db5d59d0ee..e65317331d55 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -166,6 +166,10 @@ imports. For more details, see :ref:`ignore-missing-imports`. +.. option:: --follow-untyped-imports + + This flag makes mypy analyze imports without stubs or a py.typed marker. + .. option:: --follow-imports {normal,silent,skip,error} This flag adjusts how mypy follows imported modules that were not diff --git a/docs/source/config_file.rst b/docs/source/config_file.rst index 310d0c3dbcb1..e970c23a9ecb 100644 --- a/docs/source/config_file.rst +++ b/docs/source/config_file.rst @@ -315,6 +315,18 @@ section of the command line docs. match the name of the *imported* module, not the module containing the import statement. +.. confval:: follow_untyped_imports + + :type: boolean + :default: False + + Typechecks imports from modules that do not have stubs or a py.typed marker. + + If this option is used in a per-module section, the module name should + match the name of the *imported* module, not the module containing the + import statement. Note that scanning all unannotated modules might + significantly increase the runtime of your mypy calls. + .. confval:: follow_imports :type: string diff --git a/docs/source/running_mypy.rst b/docs/source/running_mypy.rst index a8ebc61d4774..91fe525c46e0 100644 --- a/docs/source/running_mypy.rst +++ b/docs/source/running_mypy.rst @@ -321,6 +321,12 @@ not catch errors in its use. recommend avoiding ``--ignore-missing-imports`` if possible: it's equivalent to adding a ``# type: ignore`` to all unresolved imports in your codebase. +4. To make mypy typecheck imports from modules without stubs or a py.typed + marker, you can set the :option:`--follow-untyped-imports ` + command line flag or set the :confval:`follow_untyped_imports` config file option to True, + either in the global section of your mypy config file, or individually on a + per-module basis. + Library stubs not installed --------------------------- diff --git a/mypy/main.py b/mypy/main.py index ae3e7d75be7f..fb4a1f61a01d 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -580,6 +580,11 @@ def add_invertible_flag( action="store_true", help="Silently ignore imports of missing modules", ) + imports_group.add_argument( + "--follow-untyped-imports", + action="store_true", + help="Typecheck modules without stubs or py.typed marker", + ) imports_group.add_argument( "--follow-imports", choices=["normal", "silent", "skip", "error"], diff --git a/mypy/modulefinder.py b/mypy/modulefinder.py index bc11c1304776..fdd89837002f 100644 --- a/mypy/modulefinder.py +++ b/mypy/modulefinder.py @@ -334,10 +334,11 @@ def _typeshed_has_version(self, module: str) -> bool: return version >= min_version and (max_version is None or version <= max_version) def _find_module_non_stub_helper( - self, components: list[str], pkg_dir: str + self, id: str, pkg_dir: str ) -> OnePackageDir | ModuleNotFoundReason: plausible_match = False dir_path = pkg_dir + components = id.split(".") for index, component in enumerate(components): dir_path = os_path_join(dir_path, component) if self.fscache.isfile(os_path_join(dir_path, "py.typed")): @@ -350,6 +351,10 @@ def _find_module_non_stub_helper( if not self.fscache.isdir(dir_path): break if plausible_match: + if self.options: + module_specific_options = self.options.clone_for_module(id) + if module_specific_options.follow_untyped_imports: + return os.path.join(pkg_dir, *components[:-1]), False return ModuleNotFoundReason.FOUND_WITHOUT_TYPE_HINTS else: return ModuleNotFoundReason.NOT_FOUND @@ -463,7 +468,7 @@ def _find_module(self, id: str, use_typeshed: bool) -> ModuleSearchResult: third_party_stubs_dirs.append((path, True)) else: third_party_stubs_dirs.append((path, True)) - non_stub_match = self._find_module_non_stub_helper(components, pkg_dir) + non_stub_match = self._find_module_non_stub_helper(id, pkg_dir) if isinstance(non_stub_match, ModuleNotFoundReason): if non_stub_match is ModuleNotFoundReason.FOUND_WITHOUT_TYPE_HINTS: found_possible_third_party_missing_type_hints = True diff --git a/mypy/options.py b/mypy/options.py index 367267d1053a..561b23fec7d0 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -42,6 +42,7 @@ class BuildType: "extra_checks", "follow_imports_for_stubs", "follow_imports", + "follow_untyped_imports", "ignore_errors", "ignore_missing_imports", "implicit_optional", @@ -113,6 +114,8 @@ def __init__(self) -> None: self.ignore_missing_imports = False # Is ignore_missing_imports set in a per-module section self.ignore_missing_imports_per_module = False + # Typecheck modules without stubs or py.typed marker + self.follow_untyped_imports = False self.follow_imports = "normal" # normal|silent|skip|error # Whether to respect the follow_imports setting even for stub files. # Intended to be used for disabling specific stubs. diff --git a/test-data/unit/pep561.test b/test-data/unit/pep561.test index 9969c2894c36..fb303a8fb5ec 100644 --- a/test-data/unit/pep561.test +++ b/test-data/unit/pep561.test @@ -187,6 +187,13 @@ b.bf(1) testNamespacePkgWStubsWithNamespacePackagesFlag.py:7: error: Argument 1 to "bf" has incompatible type "int"; expected "bool" testNamespacePkgWStubsWithNamespacePackagesFlag.py:8: error: Argument 1 to "bf" has incompatible type "int"; expected "bool" +[case testMissingPytypedFlag] +# pkgs: typedpkg_ns_b +# flags: --namespace-packages --follow-untyped-imports +import typedpkg_ns.b.bbb as b +b.bf("foo", "bar") +[out] +testMissingPytypedFlag.py:4: error: Too many arguments for "bf" [case testTypedPkgNamespaceRegFromImportTwiceMissing] # pkgs: typedpkg_ns_a