Skip to content

Commit da21dae

Browse files
authored
Behave consistently when type-checking stub package directly (#21330)
Type-checking stub packages directly always worked, and people rely on this, e.g. people run `mypy scipy-stubs` in CI. However, there is a problem if the same exact stubs are not installed, then the incremental mode (and therefore parallel mode) is completely busted. This is because when computing dependencies `is_module()` returns False for these, and therefore most dependencies are missing. There is a simple fix however: if a module is in source set, it should be definitely a module. I also update some blocker error formatting in related code to be more consistent (e.g. use capitalized errors).
1 parent a850b66 commit da21dae

7 files changed

Lines changed: 52 additions & 31 deletions

File tree

mypy/build.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -913,20 +913,20 @@ def __init__(
913913
continue
914914
path = self.find_module_cache.find_module(module, fast_path=True)
915915
if not isinstance(path, str):
916-
raise CompileError(
917-
[f"Failed to find builtin module {module}, perhaps typeshed is broken?"]
916+
build_error(
917+
f'Failed to find builtin module "{module}", perhaps typeshed is broken?'
918918
)
919919
if is_typeshed_file(options.abs_custom_typeshed_dir, path) or is_stub_package_file(
920920
path
921921
):
922922
continue
923923

924-
raise CompileError(
925-
[
926-
f'mypy: "{os.path.relpath(path)}" shadows library module "{module}"',
927-
f'note: A user-defined top-level module with name "{module}" is not supported',
928-
]
924+
self.errors.set_file(path, module, options)
925+
self.error(None, f'This file shadows library module "{module}"', blocker=True)
926+
self.note(
927+
None, f'A user-defined top-level module with name "{module}" is not supported'
929928
)
929+
self.errors.raise_error()
930930

931931
if metastore is None:
932932
metastore = create_metastore(options, parallel_worker=parallel_worker)
@@ -1255,7 +1255,19 @@ def all_imported_modules_in_file(self, file: MypyFile) -> list[tuple[int, str, i
12551255
return res
12561256

12571257
def is_module(self, id: str) -> bool:
1258-
"""Is there a file in the file system corresponding to module id?"""
1258+
"""Does the given fullname refer to a module?
1259+
1260+
Note: this does not always verify that the module exists and relies on
1261+
previously executed logic in find_module_and_diagnose().
1262+
"""
1263+
if id in self.modules:
1264+
# Micro-optimization, if we already found it, it is definitely a module.
1265+
return True
1266+
if id in self.source_set.source_modules:
1267+
# Special case: if a module is passed on command line, we accept it even
1268+
# if we would not resolve it using regular mechanisms. This makes behavior
1269+
# consistent in cases like `mypy foo-stubs`, where stubs are not installed.
1270+
return True
12591271
return find_module_simple(id, self) is not None
12601272

12611273
def parse_file(
@@ -3149,7 +3161,7 @@ def get_source(self) -> str:
31493161
assert ioerr.errno is not None
31503162
raise CompileError(
31513163
[
3152-
"mypy: error: cannot read file '{}': {}".format(
3164+
"mypy: error: Cannot read file '{}': {}".format(
31533165
self.path.replace(os.getcwd() + os.sep, ""),
31543166
os.strerror(ioerr.errno),
31553167
)
@@ -3158,9 +3170,9 @@ def get_source(self) -> str:
31583170
) from ioerr
31593171
except (UnicodeDecodeError, DecodeError) as decodeerr:
31603172
if self.path.endswith(".pyd"):
3161-
err = f"{self.path}: error: stubgen does not support .pyd files"
3173+
err = f"{self.path}: error: Stubgen does not support .pyd files"
31623174
else:
3163-
err = f"{self.path}: error: cannot decode file: {str(decodeerr)}"
3175+
err = f"{self.path}: error: Cannot decode file: {str(decodeerr)}"
31643176
raise CompileError([err], module_with_blocker=self.id) from decodeerr
31653177
elif self.path and self.manager.fscache.isdir(self.path):
31663178
source = ""
@@ -3782,7 +3794,7 @@ def find_module_and_diagnose(
37823794
# If we can't find a root source it's always fatal.
37833795
# TODO: This might hide non-fatal errors from
37843796
# root sources processed earlier.
3785-
raise CompileError([f"mypy: can't find module '{id}'"])
3797+
raise CompileError([f'mypy: error: Cannot find module "{id}"'])
37863798
else:
37873799
raise ModuleNotFound
37883800

@@ -3911,7 +3923,7 @@ def module_not_found(
39113923
)
39123924
if target == "builtins":
39133925
manager.error(
3914-
line, "Cannot find 'builtins' module. Typeshed appears broken!", blocker=True
3926+
line, 'Cannot find "builtins" module. Typeshed appears broken!', blocker=True
39153927
)
39163928
errors.raise_error()
39173929
else:

mypy/main.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1577,7 +1577,8 @@ def set_strict_flags() -> None:
15771577
reason = cache.find_module(p)
15781578
if reason is ModuleNotFoundReason.FOUND_WITHOUT_TYPE_HINTS:
15791579
fail(
1580-
f"Package '{p}' cannot be type checked due to missing py.typed marker. See https://mypy.readthedocs.io/en/stable/installed_packages.html for more details",
1580+
f"Package '{p}' cannot be type checked due to missing py.typed marker.\n"
1581+
"See https://mypy.readthedocs.io/en/stable/installed_packages.html for more details",
15811582
stderr,
15821583
options,
15831584
)
@@ -1698,7 +1699,7 @@ def read_types_packages_to_install(cache_dir: str, after_run: bool) -> list[str]
16981699
if not after_run:
16991700
sys.stderr.write(
17001701
"error: Can't determine which types to install with no files to check "
1701-
+ "(and no cache from previous mypy run)\n"
1702+
"(and no cache from previous mypy run)\n"
17021703
)
17031704
else:
17041705
sys.stderr.write(

test-data/unit/check-incremental.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2334,7 +2334,7 @@ tmp/c.py:1: error: Module "d" has no attribute "x"
23342334
[delete nonexistent.py.2]
23352335
[out]
23362336
[out2]
2337-
mypy: error: cannot read file 'tmp/nonexistent.py': No such file or directory
2337+
mypy: error: Cannot read file 'tmp/nonexistent.py': No such file or directory
23382338

23392339
[case testSerializeAbstractPropertyIncremental]
23402340
from abc import abstractmethod

test-data/unit/cmdline.test

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ sub.pkg contains __init__.py but is not a valid Python package name
117117
[file a.py]
118118
# coding: uft-8
119119
[out]
120-
a.py: error: cannot decode file: unknown encoding: uft-8
120+
a.py: error: Cannot decode file: unknown encoding: uft-8
121121
== Return code: 2
122122

123123
[case testCannotIgnoreDuplicateModule]
@@ -416,7 +416,7 @@ int_pow.py:11: note: Revealed type is "Any"
416416
[case testMissingFile]
417417
# cmd: mypy nope.py
418418
[out]
419-
mypy: error: cannot read file 'nope.py': No such file or directory
419+
mypy: error: Cannot read file 'nope.py': No such file or directory
420420
== Return code: 2
421421

422422
[case testModulesAndPackages]
@@ -638,7 +638,7 @@ c.py:1: error: Name "fail" is not defined
638638
\[mypy]
639639
files = config.py
640640
[out]
641-
mypy: error: cannot read file 'override.py': No such file or directory
641+
mypy: error: Cannot read file 'override.py': No such file or directory
642642
== Return code: 2
643643

644644
[case testErrorSummaryOnSuccess]
@@ -695,7 +695,7 @@ Found 2 errors in 2 files (checked 2 source files)
695695
[case testErrorSummaryOnBadUsage]
696696
# cmd: mypy --error-summary missing.py
697697
[out]
698-
mypy: error: cannot read file 'missing.py': No such file or directory
698+
mypy: error: Cannot read file 'missing.py': No such file or directory
699699
Found 1 error in 1 file (errors prevented further checking)
700700
== Return code: 2
701701

@@ -771,7 +771,7 @@ imp.py:2: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#miss
771771
[file a.pyd]
772772
# coding: uft-8
773773
[out]
774-
a.pyd: error: stubgen does not support .pyd files
774+
a.pyd: error: Stubgen does not support .pyd files
775775
== Return code: 2
776776

777777
[case testDuplicateModules]
@@ -1073,8 +1073,8 @@ except ValueError:
10731073
from typing import *
10741074
sys.path = path
10751075
[out]
1076-
mypy: "typing.py" shadows library module "typing"
1077-
note: A user-defined top-level module with name "typing" is not supported
1076+
typing.py: error: This file shadows library module "typing"
1077+
typing.py: note: A user-defined top-level module with name "typing" is not supported
10781078
== Return code: 2
10791079

10801080
[case testCustomTypeshedDirWithRelativePathDoesNotCrash]
@@ -1091,7 +1091,7 @@ note: A user-defined top-level module with name "typing" is not supported
10911091
[file dir/stdlib/collections/__init__.pyi]
10921092
[file dir/stdlib/VERSIONS]
10931093
[out]
1094-
Failed to find builtin module mypy_extensions, perhaps typeshed is broken?
1094+
mypy: error: Failed to find builtin module "mypy_extensions", perhaps typeshed is broken?
10951095
== Return code: 2
10961096

10971097
[case testCustomTypeshedDirFilePassedExplicitly]
@@ -1284,3 +1284,11 @@ pass
12841284
[out]
12851285
error: cache must be enabled in parallel mode
12861286
== Return code: 2
1287+
1288+
[case testCheckingStubPackagesWorksInParallelMode]
1289+
# cmd: mypy foo-stubs --num-workers=4
1290+
[file foo-stubs/__init__.pyi]
1291+
[file foo-stubs/api/__init__.pyi]
1292+
from foo.api import bar as bar
1293+
[file foo-stubs/api/bar.pyi]
1294+
[out]

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ def f(x: int) -> None: ...
502502
def f(x: str) -> None: ...
503503
[out]
504504
==
505-
a.py: error: cannot decode file: 'ascii' codec can't decode byte 0xc3 in position 16: ordinal not in range(128)
505+
a.py: error: Cannot decode file: 'ascii' codec can't decode byte 0xc3 in position 16: ordinal not in range(128)
506506
==
507507
main:2: error: Argument 1 to "f" has incompatible type "int"; expected "str"
508508

@@ -518,7 +518,7 @@ def f(x: int) -> None: ...
518518
def f(x: str) -> None: ...
519519
[out]
520520
==
521-
a.py: error: cannot decode file: 'ascii' codec can't decode byte 0xc3 in position 17: ordinal not in range(128)
521+
a.py: error: Cannot decode file: 'ascii' codec can't decode byte 0xc3 in position 17: ordinal not in range(128)
522522
==
523523
main:2: error: Argument 1 to "f" has incompatible type "int"; expected "str"
524524

@@ -532,6 +532,6 @@ a.f(1)
532532
[file a.py.2]
533533
def f(x: str) -> None: ...
534534
[out]
535-
a.py: error: cannot decode file: 'ascii' codec can't decode byte 0xc3 in position 16: ordinal not in range(128)
535+
a.py: error: Cannot decode file: 'ascii' codec can't decode byte 0xc3 in position 16: ordinal not in range(128)
536536
==
537537
main:3: error: Argument 1 to "f" has incompatible type "int"; expected "str"

test-data/unit/fine-grained.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6068,9 +6068,9 @@ a.py:1: error: "int" not callable
60686068
[file a.py.2]
60696069
1()
60706070
[out]
6071-
mypy: error: cannot read file 'tmp/nonexistent.py': No such file or directory
6071+
mypy: error: Cannot read file 'tmp/nonexistent.py': No such file or directory
60726072
==
6073-
mypy: error: cannot read file 'tmp/nonexistent.py': No such file or directory
6073+
mypy: error: Cannot read file 'tmp/nonexistent.py': No such file or directory
60746074

60756075
[case testNonExistentFileOnCommandLine2]
60766076
# cmd: mypy a.py

test-data/unit/pythoneval.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1518,8 +1518,8 @@ async def foo() -> None:
15181518
x = 0
15191519
1 + ''
15201520
[out]
1521-
mypy: "tmp/typing.py" shadows library module "typing"
1522-
note: A user-defined top-level module with name "typing" is not supported
1521+
typing.py: error: This file shadows library module "typing"
1522+
typing.py: note: A user-defined top-level module with name "typing" is not supported
15231523

15241524

15251525
[case testNoPython3StubAvailable]

0 commit comments

Comments
 (0)