Skip to content

Commit 4026cb8

Browse files
authored
Make --ignore-missing-imports less strict (#5228)
Fixes #5226 With this change, the following ``` import existing_package.non_existing_mod x = existing_package.non_existing_mod.some_func() ``` will be allowed under `--ignore-missing-imports`
1 parent a55635d commit 4026cb8

File tree

5 files changed

+67
-6
lines changed

5 files changed

+67
-6
lines changed

docs/source/command_line.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,21 @@ Here are some more useful flags:
297297

298298
- ``--ignore-missing-imports`` suppresses error messages about imports
299299
that cannot be resolved (see :ref:`follow-imports` for some examples).
300+
This doesn't suppress errors about missing names in successfully resolved
301+
modules. For example, if one has the following files::
302+
303+
package/__init__.py
304+
package/mod.py
305+
306+
Then mypy will generate the following errors with ``--ignore-missing-imports``:
307+
308+
.. code-block:: python
309+
310+
import package.unknown # No error, ignored
311+
x = package.unknown.func() # OK
312+
313+
from package import unknown # No error, ignored
314+
from package.mod import NonExisting # Error: Module has no attribute 'NonExisting'
300315
301316
- ``--no-strict-optional`` disables strict checking of ``Optional[...]``
302317
types and ``None`` values. With this option, mypy doesn't

docs/source/config_file.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ overridden by the pattern sections matching the module name.
201201
strict Optional checks. If False, mypy treats ``None`` as
202202
compatible with every type.
203203

204-
**Note::** This was False by default
204+
**Note:** This was False by default
205205
in mypy versions earlier than 0.600.
206206

207207
- ``disallow_any_unimported`` (Boolean, default false) disallows usage of types that come

mypy/semanal.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1329,15 +1329,34 @@ def add_submodules_to_parent_modules(self, id: str, module_public: bool) -> None
13291329
while '.' in id:
13301330
parent, child = id.rsplit('.', 1)
13311331
parent_mod = self.modules.get(parent)
1332-
if parent_mod and child not in parent_mod.names:
1332+
if parent_mod and self.allow_patching(parent_mod, child):
13331333
child_mod = self.modules.get(id)
13341334
if child_mod:
13351335
sym = SymbolTableNode(MODULE_REF, child_mod,
13361336
module_public=module_public,
13371337
no_serialize=True)
1338-
parent_mod.names[child] = sym
1338+
else:
1339+
# Construct a dummy Var with Any type.
1340+
any_type = AnyType(TypeOfAny.from_unimported_type,
1341+
missing_import_name=id)
1342+
var = Var(child, any_type)
1343+
var._fullname = child
1344+
var.is_ready = True
1345+
var.is_suppressed_import = True
1346+
sym = SymbolTableNode(GDEF, var,
1347+
module_public=module_public,
1348+
no_serialize=True)
1349+
parent_mod.names[child] = sym
13391350
id = parent
13401351

1352+
def allow_patching(self, parent_mod: MypyFile, child: str) -> bool:
1353+
if child not in parent_mod.names:
1354+
return True
1355+
node = parent_mod.names[child].node
1356+
if isinstance(node, Var) and node.is_suppressed_import:
1357+
return True
1358+
return False
1359+
13411360
def add_module_symbol(self, id: str, as_id: str, module_public: bool,
13421361
context: Context, module_hidden: bool = False) -> None:
13431362
if id in self.modules:

test-data/unit/check-modules.test

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2312,3 +2312,33 @@ def run() -> None:
23122312
[out]
23132313
tmp/p/b.py:4: error: Revealed type is 'builtins.int'
23142314
tmp/p/__init__.py:3: error: Revealed type is 'builtins.int'
2315+
2316+
[case testMissingSubmoduleImportedWithIgnoreMissingImports]
2317+
# flags: --ignore-missing-imports
2318+
import whatever.works
2319+
import a.b
2320+
x = whatever.works.f()
2321+
y = a.b.f()
2322+
[file a/__init__.py]
2323+
# empty
2324+
[out]
2325+
2326+
[case testMissingSubmoduleImportedWithIgnoreMissingImportsStub]
2327+
# flags: --ignore-missing-imports --follow-imports=skip
2328+
import whatever.works
2329+
import a.b
2330+
x = whatever.works.f()
2331+
y = a.b.f()
2332+
[file a/__init__.pyi]
2333+
# empty
2334+
[out]
2335+
2336+
[case testMissingSubmoduleImportedWithIgnoreMissingImportsNested]
2337+
# flags: --ignore-missing-imports
2338+
import a.b.c.d
2339+
y = a.b.c.d.f()
2340+
[file a/__init__.py]
2341+
# empty
2342+
[file a/b/__init__.py]
2343+
# empty
2344+
[out]

test-data/unit/fine-grained-modules.test

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,6 @@ main:1: error: Cannot find module named 'p.a'
221221
==
222222
main:1: error: Cannot find module named 'p.a'
223223
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
224-
main:2: error: Module has no attribute "a"
225224
==
226225
main:2: error: Argument 1 to "f" has incompatible type "int"; expected "str"
227226
[builtins fixtures/module.pyi]
@@ -826,7 +825,6 @@ a.py:2: error: Too many arguments for "g"
826825
==
827826
a.py:1: error: Cannot find module named 'm.x'
828827
a.py:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
829-
a.py:2: error: Module has no attribute "x"
830828

831829
[case testDeletePackage1]
832830
import p.a
@@ -873,7 +871,6 @@ main:2: error: Argument 1 to "f" has incompatible type "int"; expected "str"
873871
==
874872
main:1: error: Cannot find module named 'p.a'
875873
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)
876-
main:2: error: Module has no attribute "a"
877874
==
878875
main:1: error: Cannot find module named 'p'
879876
main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help)

0 commit comments

Comments
 (0)