Skip to content

Commit 332e788

Browse files
authored
stubtest: fallback to dir if runtime doesn't have __all__ (#9523)
Co-authored-by: hauntsaninja <>
1 parent 4a89008 commit 332e788

File tree

2 files changed

+31
-7
lines changed

2 files changed

+31
-7
lines changed

mypy/stubtest.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,11 +212,18 @@ def verify_mypyfile(
212212
for m, o in stub.names.items()
213213
if o.module_public and (not m.startswith("_") or hasattr(runtime, m))
214214
)
215-
# Check all things declared in module's __all__
216-
to_check.update(getattr(runtime, "__all__", []))
215+
runtime_public_contents = [
216+
m
217+
for m in dir(runtime)
218+
if not m.startswith("_")
219+
# Ensure that the object's module is `runtime`, e.g. so that we don't pick up reexported
220+
# modules and infinitely recurse. Unfortunately, there's no way to detect an explicit
221+
# reexport missing from the stubs (that isn't specified in __all__)
222+
and getattr(getattr(runtime, m), "__module__", None) == runtime.__name__
223+
]
224+
# Check all things declared in module's __all__, falling back to runtime_public_contents
225+
to_check.update(getattr(runtime, "__all__", runtime_public_contents))
217226
to_check.difference_update({"__file__", "__doc__", "__name__", "__builtins__", "__package__"})
218-
# We currently don't check things in the module that aren't in the stub, other than things that
219-
# are in __all__, to avoid false positives.
220227

221228
for entry in sorted(to_check):
222229
yield from verify(

mypy/test/teststubtest.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,16 @@ def collect_cases(fn: Callable[..., Iterator[Case]]) -> Callable[..., None]:
6969

7070
def test(*args: Any, **kwargs: Any) -> None:
7171
cases = list(fn(*args, **kwargs))
72-
expected_errors = set(
73-
"{}.{}".format(TEST_MODULE_NAME, c.error) for c in cases if c.error is not None
74-
)
72+
expected_errors = set()
73+
for c in cases:
74+
if c.error is None:
75+
continue
76+
expected_error = "{}.{}".format(TEST_MODULE_NAME, c.error)
77+
assert expected_error not in expected_errors, (
78+
"collect_cases merges cases into a single stubtest invocation; we already "
79+
"expect an error for {}".format(expected_error)
80+
)
81+
expected_errors.add(expected_error)
7582
output = run_stubtest(
7683
stub="\n\n".join(textwrap.dedent(c.stub.lstrip("\n")) for c in cases),
7784
runtime="\n\n".join(textwrap.dedent(c.runtime.lstrip("\n")) for c in cases),
@@ -582,6 +589,11 @@ def h(x: str): ...
582589
stub="from mystery import A, B as B, C as D # type: ignore", runtime="", error="B"
583590
)
584591

592+
@collect_cases
593+
def test_missing_no_runtime_all(self) -> Iterator[Case]:
594+
yield Case(stub="", runtime="import sys", error=None)
595+
yield Case(stub="", runtime="def g(): ...", error="g")
596+
585597
@collect_cases
586598
def test_name_mangling(self) -> Iterator[Case]:
587599
yield Case(
@@ -666,6 +678,11 @@ def test_ignore_flags(self) -> None:
666678
)
667679
assert not output
668680

681+
output = run_stubtest(
682+
stub="", runtime="def f(): pass", options=["--ignore-missing-stub"]
683+
)
684+
assert not output
685+
669686
output = run_stubtest(
670687
stub="def f(__a): ...", runtime="def f(a): pass", options=["--ignore-positional-only"]
671688
)

0 commit comments

Comments
 (0)