15
15
import warnings
16
16
from functools import singledispatch
17
17
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
19
19
20
20
from typing_extensions import Type
21
21
@@ -189,6 +189,50 @@ def verify(
189
189
yield Error (object_path , "is an unknown mypy node" , stub , runtime )
190
190
191
191
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
+
192
236
@verify .register (nodes .MypyFile )
193
237
def verify_mypyfile (
194
238
stub : nodes .MypyFile , runtime : MaybeMissing [types .ModuleType ], object_path : List [str ]
@@ -200,6 +244,14 @@ def verify_mypyfile(
200
244
yield Error (object_path , "is not a module" , stub , runtime )
201
245
return
202
246
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
+
203
255
# Check things in the stub
204
256
to_check = set (
205
257
m
@@ -215,8 +267,8 @@ def _belongs_to_runtime(r: types.ModuleType, attr: str) -> bool:
215
267
return not isinstance (obj , types .ModuleType )
216
268
217
269
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
220
272
else [
221
273
m
222
274
for m in dir (runtime )
@@ -228,7 +280,7 @@ def _belongs_to_runtime(r: types.ModuleType, attr: str) -> bool:
228
280
)
229
281
# Check all things declared in module's __all__, falling back to our best guess
230
282
to_check .update (runtime_public_contents )
231
- to_check .difference_update ({ "__file__" , "__doc__" , "__name__" , "__builtins__" , "__package__" } )
283
+ to_check .difference_update (IGNORED_MODULE_DUNDERS )
232
284
233
285
for entry in sorted (to_check ):
234
286
stub_entry = stub .names [entry ].node if entry in stub .names else MISSING
0 commit comments