21
21
from contextlib import redirect_stderr , redirect_stdout
22
22
from functools import singledispatch
23
23
from pathlib import Path
24
- from typing import Any , Dict , Generic , Iterator , List , Optional , Tuple , TypeVar , Union , cast
24
+ from typing import Any , Dict , Generic , Iterator , List , Optional , Set , Tuple , TypeVar , Union , cast
25
25
26
26
import typing_extensions
27
27
from typing_extensions import Type , get_origin
@@ -243,6 +243,38 @@ def verify(
243
243
yield Error (object_path , "is an unknown mypy node" , stub , runtime )
244
244
245
245
246
+ def _verify_exported_names (
247
+ object_path : List [str ], stub : nodes .MypyFile , runtime_all_as_set : Set [str ]
248
+ ) -> Iterator [Error ]:
249
+ public_names_in_stub = {m for m , o in stub .names .items () if o .module_public }
250
+ names_in_stub_not_runtime = sorted (public_names_in_stub - runtime_all_as_set )
251
+ names_in_runtime_not_stub = sorted (runtime_all_as_set - public_names_in_stub )
252
+ if not (names_in_runtime_not_stub or names_in_stub_not_runtime ):
253
+ return
254
+ yield Error (
255
+ object_path ,
256
+ (
257
+ "module: names exported from the stub "
258
+ "do not correspond to the names exported at runtime.\n "
259
+ "(Note: This is probably either due to an inaccurate "
260
+ "`__all__` in the stub, "
261
+ "or due to a name being declared in `__all__` "
262
+ "but not actually defined in the stub.)"
263
+ ),
264
+ # pass in MISSING instead of the stub and runtime objects,
265
+ # as the line numbers aren't very relevant here,
266
+ # and it makes for a prettier error message.
267
+ stub_object = MISSING ,
268
+ runtime_object = MISSING ,
269
+ stub_desc = (
270
+ f"Names exported in the stub but not at runtime: " f"{ names_in_stub_not_runtime } "
271
+ ),
272
+ runtime_desc = (
273
+ f"Names exported at runtime but not in the stub: " f"{ names_in_runtime_not_stub } "
274
+ ),
275
+ )
276
+
277
+
246
278
@verify .register (nodes .MypyFile )
247
279
def verify_mypyfile (
248
280
stub : nodes .MypyFile , runtime : MaybeMissing [types .ModuleType ], object_path : List [str ]
@@ -254,6 +286,17 @@ def verify_mypyfile(
254
286
yield Error (object_path , "is not a module" , stub , runtime )
255
287
return
256
288
289
+ runtime_all_as_set : Optional [Set [str ]]
290
+
291
+ if hasattr (runtime , "__all__" ):
292
+ runtime_all_as_set = set (runtime .__all__ )
293
+ if "__all__" in stub .names :
294
+ # Only verify the contents of the stub's __all__
295
+ # if the stub actually defines __all__
296
+ yield from _verify_exported_names (object_path , stub , runtime_all_as_set )
297
+ else :
298
+ runtime_all_as_set = None
299
+
257
300
# Check things in the stub
258
301
to_check = {
259
302
m
@@ -272,16 +315,16 @@ def _belongs_to_runtime(r: types.ModuleType, attr: str) -> bool:
272
315
return not isinstance (obj , types .ModuleType )
273
316
274
317
runtime_public_contents = (
275
- runtime . __all__
276
- if hasattr ( runtime , "__all__" )
277
- else [
318
+ runtime_all_as_set
319
+ if runtime_all_as_set is not None
320
+ else {
278
321
m
279
322
for m in dir (runtime )
280
323
if not is_probably_private (m )
281
324
# Ensure that the object's module is `runtime`, since in the absence of __all__ we
282
325
# don't have a good way to detect re-exports at runtime.
283
326
and _belongs_to_runtime (runtime , m )
284
- ]
327
+ }
285
328
)
286
329
# Check all things declared in module's __all__, falling back to our best guess
287
330
to_check .update (runtime_public_contents )
0 commit comments