Skip to content

Commit 0b303b5

Browse files
authored
stubtest: error if typeshed is missing modules from the stdlib (#15729)
We currently flag modules missing from third-party stubs in stubtest, but don't do similarly for typeshed's stdlib stubs. This PR adds that functionality for typeshed's stdlib stubs as well.
1 parent 6f650cf commit 0b303b5

File tree

1 file changed

+74
-9
lines changed

1 file changed

+74
-9
lines changed

mypy/stubtest.py

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import copy
1212
import enum
1313
import importlib
14+
import importlib.machinery
1415
import inspect
1516
import os
1617
import pkgutil
@@ -25,7 +26,7 @@
2526
from contextlib import redirect_stderr, redirect_stdout
2627
from functools import singledispatch
2728
from pathlib import Path
28-
from typing import Any, Generic, Iterator, TypeVar, Union
29+
from typing import AbstractSet, Any, Generic, Iterator, TypeVar, Union
2930
from typing_extensions import get_origin, is_typeddict
3031

3132
import mypy.build
@@ -1639,7 +1640,7 @@ def get_stub(module: str) -> nodes.MypyFile | None:
16391640

16401641
def get_typeshed_stdlib_modules(
16411642
custom_typeshed_dir: str | None, version_info: tuple[int, int] | None = None
1642-
) -> list[str]:
1643+
) -> set[str]:
16431644
"""Returns a list of stdlib modules in typeshed (for current Python version)."""
16441645
stdlib_py_versions = mypy.modulefinder.load_stdlib_py_versions(custom_typeshed_dir)
16451646
if version_info is None:
@@ -1661,14 +1662,75 @@ def exists_in_version(module: str) -> bool:
16611662
typeshed_dir = Path(mypy.build.default_data_dir()) / "typeshed"
16621663
stdlib_dir = typeshed_dir / "stdlib"
16631664

1664-
modules = []
1665+
modules: set[str] = set()
16651666
for path in stdlib_dir.rglob("*.pyi"):
16661667
if path.stem == "__init__":
16671668
path = path.parent
16681669
module = ".".join(path.relative_to(stdlib_dir).parts[:-1] + (path.stem,))
16691670
if exists_in_version(module):
1670-
modules.append(module)
1671-
return sorted(modules)
1671+
modules.add(module)
1672+
return modules
1673+
1674+
1675+
def get_importable_stdlib_modules() -> set[str]:
1676+
"""Return all importable stdlib modules at runtime."""
1677+
all_stdlib_modules: AbstractSet[str]
1678+
if sys.version_info >= (3, 10):
1679+
all_stdlib_modules = sys.stdlib_module_names
1680+
else:
1681+
all_stdlib_modules = set(sys.builtin_module_names)
1682+
python_exe_dir = Path(sys.executable).parent
1683+
for m in pkgutil.iter_modules():
1684+
finder = m.module_finder
1685+
if isinstance(finder, importlib.machinery.FileFinder):
1686+
finder_path = Path(finder.path)
1687+
if (
1688+
python_exe_dir in finder_path.parents
1689+
and "site-packages" not in finder_path.parts
1690+
):
1691+
all_stdlib_modules.add(m.name)
1692+
1693+
importable_stdlib_modules: set[str] = set()
1694+
for module_name in all_stdlib_modules:
1695+
if module_name in ANNOYING_STDLIB_MODULES:
1696+
continue
1697+
1698+
try:
1699+
runtime = silent_import_module(module_name)
1700+
except ImportError:
1701+
continue
1702+
else:
1703+
importable_stdlib_modules.add(module_name)
1704+
1705+
try:
1706+
# some stdlib modules (e.g. `nt`) don't have __path__ set...
1707+
runtime_path = runtime.__path__
1708+
runtime_name = runtime.__name__
1709+
except AttributeError:
1710+
continue
1711+
1712+
for submodule in pkgutil.walk_packages(runtime_path, runtime_name + "."):
1713+
submodule_name = submodule.name
1714+
1715+
# There are many annoying *.__main__ stdlib modules,
1716+
# and including stubs for them isn't really that useful anyway:
1717+
# tkinter.__main__ opens a tkinter windows; unittest.__main__ raises SystemExit; etc.
1718+
#
1719+
# The idlelib.* submodules are similarly annoying in opening random tkinter windows,
1720+
# and we're unlikely to ever add stubs for idlelib in typeshed
1721+
# (see discussion in https://github.com/python/typeshed/pull/9193)
1722+
if submodule_name.endswith(".__main__") or submodule_name.startswith("idlelib."):
1723+
continue
1724+
1725+
try:
1726+
silent_import_module(submodule_name)
1727+
# importing multiprocessing.popen_forkserver on Windows raises AttributeError...
1728+
except Exception:
1729+
continue
1730+
else:
1731+
importable_stdlib_modules.add(submodule_name)
1732+
1733+
return importable_stdlib_modules
16721734

16731735

16741736
def get_allowlist_entries(allowlist_file: str) -> Iterator[str]:
@@ -1699,6 +1761,10 @@ class _Arguments:
16991761
version: str
17001762

17011763

1764+
# typeshed added a stub for __main__, but that causes stubtest to check itself
1765+
ANNOYING_STDLIB_MODULES: typing_extensions.Final = frozenset({"antigravity", "this", "__main__"})
1766+
1767+
17021768
def test_stubs(args: _Arguments, use_builtins_fixtures: bool = False) -> int:
17031769
"""This is stubtest! It's time to test the stubs!"""
17041770
# Load the allowlist. This is a series of strings corresponding to Error.object_desc
@@ -1721,10 +1787,9 @@ def test_stubs(args: _Arguments, use_builtins_fixtures: bool = False) -> int:
17211787
"cannot pass both --check-typeshed and a list of modules",
17221788
)
17231789
return 1
1724-
modules = get_typeshed_stdlib_modules(args.custom_typeshed_dir)
1725-
# typeshed added a stub for __main__, but that causes stubtest to check itself
1726-
annoying_modules = {"antigravity", "this", "__main__"}
1727-
modules = [m for m in modules if m not in annoying_modules]
1790+
typeshed_modules = get_typeshed_stdlib_modules(args.custom_typeshed_dir)
1791+
runtime_modules = get_importable_stdlib_modules()
1792+
modules = sorted((typeshed_modules | runtime_modules) - ANNOYING_STDLIB_MODULES)
17281793

17291794
if not modules:
17301795
print(_style("error:", color="red", bold=True), "no modules to check")

0 commit comments

Comments
 (0)