Skip to content

Commit 58514a9

Browse files
authored
stubtest: error if module level dunder is missing, housekeeping (#12217)
Basically a follow up to #12203 New errors in typeshed from this: ``` _decimal.__libmpdec_version__ is not present in stub _heapq.__about__ is not present in stub builtins.__build_class__ is not present in stub cgitb.__UNDEF__ is not present in stub decimal.__libmpdec_version__ is not present in stub sys.__unraisablehook__ is not present in stub ``` Some general housekeeping, moving things around, renaming things, adding some comments.
1 parent f98881c commit 58514a9

File tree

2 files changed

+96
-54
lines changed

2 files changed

+96
-54
lines changed

mypy/stubtest.py

Lines changed: 88 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@ def get_description(self, concise: bool = False) -> str:
144144
return "".join(output)
145145

146146

147+
# ====================
148+
# Core logic
149+
# ====================
150+
151+
147152
def test_module(module_name: str) -> Iterator[Error]:
148153
"""Tests a given module's stub against introspecting it at runtime.
149154
@@ -204,7 +209,7 @@ def verify_mypyfile(
204209
to_check = set(
205210
m
206211
for m, o in stub.names.items()
207-
if not o.module_hidden and (not m.startswith("_") or hasattr(runtime, m))
212+
if not o.module_hidden and (not is_probably_private(m) or hasattr(runtime, m))
208213
)
209214

210215
def _belongs_to_runtime(r: types.ModuleType, attr: str) -> bool:
@@ -220,15 +225,15 @@ def _belongs_to_runtime(r: types.ModuleType, attr: str) -> bool:
220225
else [
221226
m
222227
for m in dir(runtime)
223-
if not m.startswith("_")
228+
if not is_probably_private(m)
224229
# Ensure that the object's module is `runtime`, since in the absence of __all__ we
225230
# don't have a good way to detect re-exports at runtime.
226231
and _belongs_to_runtime(runtime, m)
227232
]
228233
)
229234
# Check all things declared in module's __all__, falling back to our best guess
230235
to_check.update(runtime_public_contents)
231-
to_check.difference_update({"__file__", "__doc__", "__name__", "__builtins__", "__package__"})
236+
to_check.difference_update(IGNORED_MODULE_DUNDERS)
232237

233238
for entry in sorted(to_check):
234239
stub_entry = stub.names[entry].node if entry in stub.names else MISSING
@@ -243,60 +248,12 @@ def _belongs_to_runtime(r: types.ModuleType, attr: str) -> bool:
243248
)
244249

245250

246-
IGNORED_DUNDERS = frozenset({
247-
# Very special attributes
248-
"__weakref__",
249-
"__slots__",
250-
"__dict__",
251-
"__text_signature__",
252-
# Pickle methods
253-
"__setstate__",
254-
"__getstate__",
255-
"__getnewargs__",
256-
"__getinitargs__",
257-
"__reduce_ex__",
258-
"__reduce__",
259-
# typing implementation details
260-
"__parameters__",
261-
"__origin__",
262-
"__args__",
263-
"__orig_bases__",
264-
"__final__",
265-
# isinstance/issubclass hooks that type-checkers don't usually care about
266-
"__instancecheck__",
267-
"__subclasshook__",
268-
"__subclasscheck__",
269-
# Dataclasses implementation details
270-
"__dataclass_fields__",
271-
"__dataclass_params__",
272-
# ctypes weirdness
273-
"__ctype_be__",
274-
"__ctype_le__",
275-
"__ctypes_from_outparam__",
276-
# These two are basically useless for type checkers
277-
"__hash__",
278-
"__getattr__",
279-
# For some reason, mypy doesn't infer classes with metaclass=ABCMeta inherit this attribute
280-
"__abstractmethods__",
281-
# Ideally we'd include __match_args__ in stubs,
282-
# but this currently has issues
283-
"__match_args__",
284-
"__doc__", # Can only ever be str | None, who cares?
285-
"__del__", # Only ever called when an object is being deleted, who cares?
286-
"__new_member__", # If an enum defines __new__, the method is renamed as __new_member__
287-
})
288-
289-
290251
if sys.version_info >= (3, 7):
291252
_WrapperDescriptorType = types.WrapperDescriptorType
292253
else:
293254
_WrapperDescriptorType = type(object.__init__)
294255

295256

296-
def is_private(name: str) -> bool:
297-
return name.startswith("_") and not is_dunder(name)
298-
299-
300257
@verify.register(nodes.TypeInfo)
301258
def verify_typeinfo(
302259
stub: nodes.TypeInfo, runtime: MaybeMissing[Type[Any]], object_path: List[str]
@@ -330,7 +287,9 @@ class SubClass(runtime): # type: ignore
330287
to_check = set(stub.names)
331288
to_check.update(
332289
# cast to workaround mypyc complaints
333-
m for m in cast(Any, vars)(runtime) if not is_private(m) and m not in IGNORED_DUNDERS
290+
m
291+
for m in cast(Any, vars)(runtime)
292+
if not is_probably_private(m) and m not in ALLOW_MISSING_CLASS_DUNDERS
334293
)
335294

336295
for entry in sorted(to_check):
@@ -1009,6 +968,78 @@ def verify_typealias(
1009968
)
1010969

1011970

971+
# ====================
972+
# Helpers
973+
# ====================
974+
975+
976+
IGNORED_MODULE_DUNDERS = frozenset(
977+
{
978+
"__file__",
979+
"__doc__",
980+
"__name__",
981+
"__builtins__",
982+
"__package__",
983+
"__cached__",
984+
"__loader__",
985+
"__spec__",
986+
"__path__", # mypy adds __path__ to packages, but C packages don't have it
987+
"__getattr__", # resulting behaviour might be typed explicitly
988+
# TODO: remove the following from this list
989+
"__author__",
990+
"__version__",
991+
"__copyright__",
992+
}
993+
)
994+
995+
ALLOW_MISSING_CLASS_DUNDERS = frozenset(
996+
{
997+
# Special attributes
998+
"__dict__",
999+
"__text_signature__",
1000+
"__weakref__",
1001+
"__del__", # Only ever called when an object is being deleted, who cares?
1002+
# These two are basically useless for type checkers
1003+
"__hash__",
1004+
"__getattr__", # resulting behaviour might be typed explicitly
1005+
# isinstance/issubclass hooks that type-checkers don't usually care about
1006+
"__instancecheck__",
1007+
"__subclasshook__",
1008+
"__subclasscheck__",
1009+
# Pickle methods
1010+
"__setstate__",
1011+
"__getstate__",
1012+
"__getnewargs__",
1013+
"__getinitargs__",
1014+
"__reduce_ex__",
1015+
"__reduce__",
1016+
# ctypes weirdness
1017+
"__ctype_be__",
1018+
"__ctype_le__",
1019+
"__ctypes_from_outparam__",
1020+
# mypy limitations
1021+
"__abstractmethods__", # Classes with metaclass=ABCMeta inherit this attribute
1022+
"__new_member__", # If an enum defines __new__, the method is renamed as __new_member__
1023+
"__dataclass_fields__", # Generated by dataclasses
1024+
"__dataclass_params__", # Generated by dataclasses
1025+
"__doc__", # mypy's semanal for namedtuples assumes this is str, not Optional[str]
1026+
# typing implementation details, consider removing some of these:
1027+
"__parameters__",
1028+
"__origin__",
1029+
"__args__",
1030+
"__orig_bases__",
1031+
"__final__",
1032+
# Consider removing these:
1033+
"__match_args__",
1034+
"__slots__",
1035+
}
1036+
)
1037+
1038+
1039+
def is_probably_private(name: str) -> bool:
1040+
return name.startswith("_") and not is_dunder(name)
1041+
1042+
10121043
def is_probably_a_function(runtime: Any) -> bool:
10131044
return (
10141045
isinstance(runtime, (types.FunctionType, types.BuiltinFunctionType))
@@ -1151,6 +1182,11 @@ def anytype() -> mypy.types.AnyType:
11511182
return mypy.types.LiteralType(value=value, fallback=fallback)
11521183

11531184

1185+
# ====================
1186+
# Build and entrypoint
1187+
# ====================
1188+
1189+
11541190
_all_stubs: Dict[str, nodes.MypyFile] = {}
11551191

11561192

mypy/test/teststubtest.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,11 @@ def test(*args: Any, **kwargs: Any) -> None:
143143
for c in cases:
144144
if c.error is None:
145145
continue
146-
expected_error = "{}.{}".format(TEST_MODULE_NAME, c.error)
146+
expected_error = c.error
147+
if expected_error == "":
148+
expected_error = TEST_MODULE_NAME
149+
elif not expected_error.startswith(f"{TEST_MODULE_NAME}."):
150+
expected_error = f"{TEST_MODULE_NAME}.{expected_error}"
147151
assert expected_error not in expected_errors, (
148152
"collect_cases merges cases into a single stubtest invocation; we already "
149153
"expect an error for {}".format(expected_error)
@@ -730,7 +734,9 @@ def test_missing_no_runtime_all(self) -> Iterator[Case]:
730734

731735
@collect_cases
732736
def test_non_public_1(self) -> Iterator[Case]:
733-
yield Case(stub="__all__: list[str]", runtime="", error=None) # dummy case
737+
yield Case(
738+
stub="__all__: list[str]", runtime="", error="test_module.__all__"
739+
) # dummy case
734740
yield Case(stub="_f: int", runtime="def _f(): ...", error="_f")
735741

736742
@collect_cases

0 commit comments

Comments
 (0)