1111import copy
1212import enum
1313import importlib
14+ import importlib .machinery
1415import inspect
1516import os
1617import pkgutil
2526from contextlib import redirect_stderr , redirect_stdout
2627from functools import singledispatch
2728from pathlib import Path
28- from typing import Any , Generic , Iterator , TypeVar , Union
29+ from typing import AbstractSet , Any , Generic , Iterator , TypeVar , Union
2930from typing_extensions import get_origin , is_typeddict
3031
3132import mypy .build
@@ -1639,7 +1640,7 @@ def get_stub(module: str) -> nodes.MypyFile | None:
16391640
16401641def 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
16741736def 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+
17021768def 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