diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 2bb2f27dda99..16bbe00ab025 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -212,11 +212,18 @@ def verify_mypyfile( for m, o in stub.names.items() if o.module_public and (not m.startswith("_") or hasattr(runtime, m)) ) - # Check all things declared in module's __all__ - to_check.update(getattr(runtime, "__all__", [])) + runtime_public_contents = [ + m + for m in dir(runtime) + if not m.startswith("_") + # Ensure that the object's module is `runtime`, e.g. so that we don't pick up reexported + # modules and infinitely recurse. Unfortunately, there's no way to detect an explicit + # reexport missing from the stubs (that isn't specified in __all__) + and getattr(getattr(runtime, m), "__module__", None) == runtime.__name__ + ] + # Check all things declared in module's __all__, falling back to runtime_public_contents + to_check.update(getattr(runtime, "__all__", runtime_public_contents)) to_check.difference_update({"__file__", "__doc__", "__name__", "__builtins__", "__package__"}) - # We currently don't check things in the module that aren't in the stub, other than things that - # are in __all__, to avoid false positives. for entry in sorted(to_check): yield from verify( diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index ab6d6b87f6a8..1989f5fd46d6 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -69,9 +69,16 @@ def collect_cases(fn: Callable[..., Iterator[Case]]) -> Callable[..., None]: def test(*args: Any, **kwargs: Any) -> None: cases = list(fn(*args, **kwargs)) - expected_errors = set( - "{}.{}".format(TEST_MODULE_NAME, c.error) for c in cases if c.error is not None - ) + expected_errors = set() + for c in cases: + if c.error is None: + continue + expected_error = "{}.{}".format(TEST_MODULE_NAME, c.error) + assert expected_error not in expected_errors, ( + "collect_cases merges cases into a single stubtest invocation; we already " + "expect an error for {}".format(expected_error) + ) + expected_errors.add(expected_error) output = run_stubtest( stub="\n\n".join(textwrap.dedent(c.stub.lstrip("\n")) for c in cases), runtime="\n\n".join(textwrap.dedent(c.runtime.lstrip("\n")) for c in cases), @@ -580,6 +587,11 @@ def h(x: str): ... # Here we should only check that runtime has B, since the stub explicitly re-exports it yield Case(stub="from mystery import A, B as B # type: ignore", runtime="", error="B") + @collect_cases + 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") + @collect_cases def test_name_mangling(self) -> Iterator[Case]: yield Case( @@ -664,6 +676,11 @@ def test_ignore_flags(self) -> None: ) assert not output + output = run_stubtest( + stub="", runtime="def f(): pass", options=["--ignore-missing-stub"] + ) + assert not output + output = run_stubtest( stub="def f(__a): ...", runtime="def f(a): pass", options=["--ignore-positional-only"] )