Skip to content

Stubtest: verify the contents of __all__ in a stub #12214

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Aug 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 48 additions & 5 deletions mypy/stubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from contextlib import redirect_stderr, redirect_stdout
from functools import singledispatch
from pathlib import Path
from typing import Any, Dict, Generic, Iterator, List, Optional, Tuple, TypeVar, Union, cast
from typing import Any, Dict, Generic, Iterator, List, Optional, Set, Tuple, TypeVar, Union, cast

import typing_extensions
from typing_extensions import Type, get_origin
Expand Down Expand Up @@ -243,6 +243,38 @@ def verify(
yield Error(object_path, "is an unknown mypy node", stub, runtime)


def _verify_exported_names(
object_path: List[str], stub: nodes.MypyFile, runtime_all_as_set: Set[str]
) -> Iterator[Error]:
public_names_in_stub = {m for m, o in stub.names.items() if o.module_public}
names_in_stub_not_runtime = sorted(public_names_in_stub - runtime_all_as_set)
names_in_runtime_not_stub = sorted(runtime_all_as_set - public_names_in_stub)
if not (names_in_runtime_not_stub or names_in_stub_not_runtime):
return
yield Error(
object_path,
(
"module: names exported from the stub "
"do not correspond to the names exported at runtime.\n"
"(Note: This is probably either due to an inaccurate "
"`__all__` in the stub, "
"or due to a name being declared in `__all__` "
"but not actually defined in the stub.)"
),
# pass in MISSING instead of the stub and runtime objects,
# as the line numbers aren't very relevant here,
# and it makes for a prettier error message.
stub_object=MISSING,
runtime_object=MISSING,
stub_desc=(
f"Names exported in the stub but not at runtime: " f"{names_in_stub_not_runtime}"
),
runtime_desc=(
f"Names exported at runtime but not in the stub: " f"{names_in_runtime_not_stub}"
),
)


@verify.register(nodes.MypyFile)
def verify_mypyfile(
stub: nodes.MypyFile, runtime: MaybeMissing[types.ModuleType], object_path: List[str]
Expand All @@ -254,6 +286,17 @@ def verify_mypyfile(
yield Error(object_path, "is not a module", stub, runtime)
return

runtime_all_as_set: Optional[Set[str]]

if hasattr(runtime, "__all__"):
runtime_all_as_set = set(runtime.__all__)
if "__all__" in stub.names:
# Only verify the contents of the stub's __all__
# if the stub actually defines __all__
yield from _verify_exported_names(object_path, stub, runtime_all_as_set)
else:
runtime_all_as_set = None

# Check things in the stub
to_check = {
m
Expand All @@ -272,16 +315,16 @@ def _belongs_to_runtime(r: types.ModuleType, attr: str) -> bool:
return not isinstance(obj, types.ModuleType)

runtime_public_contents = (
runtime.__all__
if hasattr(runtime, "__all__")
else [
runtime_all_as_set
if runtime_all_as_set is not None
else {
m
for m in dir(runtime)
if not is_probably_private(m)
# Ensure that the object's module is `runtime`, since in the absence of __all__ we
# don't have a good way to detect re-exports at runtime.
and _belongs_to_runtime(runtime, m)
]
}
)
# Check all things declared in module's __all__, falling back to our best guess
to_check.update(runtime_public_contents)
Expand Down
31 changes: 31 additions & 0 deletions mypy/test/teststubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,37 @@ def f(): return 3
error=None,
)

@collect_cases
def test_all_at_runtime_not_stub(self) -> Iterator[Case]:
yield Case(
stub="Z: int",
runtime="""
__all__ = []
Z = 5""",
error=None,
)

@collect_cases
def test_all_in_stub_not_at_runtime(self) -> Iterator[Case]:
yield Case(stub="__all__ = ()", runtime="", error="__all__")

@collect_cases
def test_all_in_stub_different_to_all_at_runtime(self) -> Iterator[Case]:
# We *should* emit an error with the module name itself,
# if the stub *does* define __all__,
# but the stub's __all__ is inconsistent with the runtime's __all__
yield Case(
stub="""
__all__ = ['foo']
foo: str
""",
runtime="""
__all__ = []
foo = 'foo'
""",
error="",
)

@collect_cases
def test_missing(self) -> Iterator[Case]:
yield Case(stub="x = 5", runtime="", error="x")
Expand Down