Skip to content

Commit 4adeace

Browse files
committed
Stubtest: verify the presence and contents of __all__ in a stub
1 parent 85fc99c commit 4adeace

File tree

1 file changed

+56
-4
lines changed

1 file changed

+56
-4
lines changed

mypy/stubtest.py

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import warnings
1616
from functools import singledispatch
1717
from pathlib import Path
18-
from typing import Any, Dict, Generic, Iterator, List, Optional, Tuple, TypeVar, Union, cast
18+
from typing import Any, Dict, Generic, Iterator, List, Optional, Set, Tuple, TypeVar, Union, cast
1919

2020
from typing_extensions import Type
2121

@@ -189,6 +189,50 @@ def verify(
189189
yield Error(object_path, "is an unknown mypy node", stub, runtime)
190190

191191

192+
IGNORED_MODULE_DUNDERS = frozenset({
193+
"__file__",
194+
"__doc__",
195+
"__name__",
196+
"__builtins__",
197+
"__package__"
198+
})
199+
200+
201+
def _verify_exported_names(
202+
object_path: List[str], stub: nodes.MypyFile, runtime_all_as_set: Set[str]
203+
) -> Iterator[Error]:
204+
public_names_in_stub = {
205+
m
206+
for m, o in stub.names.items()
207+
if o.module_public and m not in IGNORED_MODULE_DUNDERS
208+
}
209+
if not runtime_all_as_set.symmetric_difference(public_names_in_stub):
210+
return
211+
sorted_runtime_names = sorted(
212+
runtime_all_as_set,
213+
key=lambda name: ((name not in public_names_in_stub), name)
214+
)
215+
sorted_names_in_stub = sorted(
216+
public_names_in_stub,
217+
key=lambda name: ((name not in runtime_all_as_set), name)
218+
)
219+
# pass in MISSING instead of the stub and runtime objects,
220+
# as the line numbers aren't very relevant here.
221+
yield Error(
222+
object_path,
223+
(
224+
"module: names exported from the stub "
225+
"do not correspond to the names exported at runtime. "
226+
"(Note: This may be due to a missing or inaccurate "
227+
"`__all__` in the stub.)"
228+
),
229+
MISSING,
230+
MISSING,
231+
stub_desc=f"Names exported are: {sorted_names_in_stub}",
232+
runtime_desc=f"Names exported are: {sorted_runtime_names}"
233+
)
234+
235+
192236
@verify.register(nodes.MypyFile)
193237
def verify_mypyfile(
194238
stub: nodes.MypyFile, runtime: MaybeMissing[types.ModuleType], object_path: List[str]
@@ -200,6 +244,14 @@ def verify_mypyfile(
200244
yield Error(object_path, "is not a module", stub, runtime)
201245
return
202246

247+
runtime_all_as_set: Optional[Set[str]]
248+
249+
if hasattr(runtime, "__all__"):
250+
runtime_all_as_set = set(runtime.__all__)
251+
yield from _verify_exported_names(object_path, stub, runtime_all_as_set)
252+
else:
253+
runtime_all_as_set = None
254+
203255
# Check things in the stub
204256
to_check = set(
205257
m
@@ -215,8 +267,8 @@ def _belongs_to_runtime(r: types.ModuleType, attr: str) -> bool:
215267
return not isinstance(obj, types.ModuleType)
216268

217269
runtime_public_contents = (
218-
runtime.__all__
219-
if hasattr(runtime, "__all__")
270+
["__all__", *runtime_all_as_set]
271+
if runtime_all_as_set is not None
220272
else [
221273
m
222274
for m in dir(runtime)
@@ -228,7 +280,7 @@ def _belongs_to_runtime(r: types.ModuleType, attr: str) -> bool:
228280
)
229281
# Check all things declared in module's __all__, falling back to our best guess
230282
to_check.update(runtime_public_contents)
231-
to_check.difference_update({"__file__", "__doc__", "__name__", "__builtins__", "__package__"})
283+
to_check.difference_update(IGNORED_MODULE_DUNDERS)
232284

233285
for entry in sorted(to_check):
234286
stub_entry = stub.names[entry].node if entry in stub.names else MISSING

0 commit comments

Comments
 (0)