From 8bf3d38c8ce3a4c40da8f6d4adf25ccd03d3e09a Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 9 Dec 2022 20:50:22 +0000 Subject: [PATCH 1/3] Stubtest: Improve heuristics for determining whether global-namespace names are imported --- mypy/stubtest.py | 49 ++++++++++++++++++++++++++++++++++----- mypy/test/teststubtest.py | 3 +++ 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 5e39b996076b..c31b475f3281 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -15,6 +15,7 @@ import os import pkgutil import re +import symtable import sys import traceback import types @@ -281,6 +282,35 @@ def _verify_exported_names( ) +def _get_imported_symbol_names(runtime: types.ModuleType) -> frozenset[str] | None: + """Retrieve the names in the global namespace which are known to be imported. + + 1). Use inspect to retrieve the source code of the module + 2). Use symtable to parse the source and retrieve names that are known to be imported + from other modules. + + If either of the above steps fails, return `None`. + + Note that if a set of names is returned, + it won't include names imported via `from foo import *` imports. + """ + try: + sourcelines = inspect.getsourcelines(runtime)[0] + except (OSError, TypeError, SyntaxError): + return None + + source = "".join(sourcelines) + if not source.strip(): + return None + + try: + module_symtable = symtable.symtable(source, runtime.__name__, "exec") + except SyntaxError: + return None + + return frozenset(sym.get_name() for sym in module_symtable.get_symbols() if sym.is_imported()) + + @verify.register(nodes.MypyFile) def verify_mypyfile( stub: nodes.MypyFile, runtime: MaybeMissing[types.ModuleType], object_path: list[str] @@ -310,15 +340,22 @@ def verify_mypyfile( if not o.module_hidden and (not is_probably_private(m) or hasattr(runtime, m)) } + imported_symbols = _get_imported_symbol_names(runtime) + def _belongs_to_runtime(r: types.ModuleType, attr: str) -> bool: obj = getattr(r, attr) - try: - obj_mod = getattr(obj, "__module__", None) - except Exception: + if isinstance(obj, types.ModuleType): return False - if obj_mod is not None: - return bool(obj_mod == r.__name__) - return not isinstance(obj, types.ModuleType) + if callable(obj): + try: + obj_mod = getattr(obj, "__module__", None) + except Exception: + return False + if obj_mod is not None: + return bool(obj_mod == r.__name__) + if imported_symbols is not None: + return attr not in imported_symbols + return True runtime_public_contents = ( runtime_all_as_set diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 812333e3feb4..5e59d8efec63 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -1082,6 +1082,9 @@ def test_missing_no_runtime_all(self) -> Iterator[Case]: yield Case(stub="", runtime="import sys", error=None) yield Case(stub="", runtime="def g(): ...", error="g") yield Case(stub="", runtime="CONSTANT = 0", error="CONSTANT") + yield Case(stub="", runtime="import re; constant = re.compile('foo')", error="constant") + yield Case(stub="", runtime="from json.scanner import NUMBER_RE", error=None) + yield Case(stub="", runtime="from string import ascii_letters", error=None) @collect_cases def test_non_public_1(self) -> Iterator[Case]: From f132b19c67437da13269c1409be65af6106f092d Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 9 Dec 2022 22:38:27 +0000 Subject: [PATCH 2/3] Just use `getsource()`, not `getsourcelines()` --- mypy/stubtest.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index c31b475f3281..305492df7c73 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -295,11 +295,10 @@ def _get_imported_symbol_names(runtime: types.ModuleType) -> frozenset[str] | No it won't include names imported via `from foo import *` imports. """ try: - sourcelines = inspect.getsourcelines(runtime)[0] + source = inspect.getsource(runtime) except (OSError, TypeError, SyntaxError): return None - source = "".join(sourcelines) if not source.strip(): return None From 6543bef34d232d7502446e2986a1acddd02c8a53 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 22 Dec 2022 13:22:16 +0000 Subject: [PATCH 3/3] Address review --- mypy/stubtest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 305492df7c73..7d656fd69c34 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -300,7 +300,9 @@ def _get_imported_symbol_names(runtime: types.ModuleType) -> frozenset[str] | No return None if not source.strip(): - return None + # The source code for the module was an empty file, + # no point in parsing it with symtable + return frozenset() try: module_symtable = symtable.symtable(source, runtime.__name__, "exec")