8000 stubtest: error if a dunder method is missing from a stub (#12203) · python/mypy@9af578d · GitHub
[go: up one dir, main page]

Skip to content

Commit 9af578d

Browse files
authored
stubtest: error if a dunder method is missing from a stub (#12203)
1 parent 36d50d6 commit 9af578d

File tree

2 files changed

+78
-7
lines changed

2 files changed

+78
-7
lines changed

mypy/stubtest.py

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from mypy import nodes
2626
from mypy.config_parser import parse_config_file
2727
from mypy.options import Options
28-
from mypy.util import FancyFormatter, bytes_to_human_readable_repr, is_dunder, SPECIAL_DUNDERS
28+
from mypy.util import FancyFormatter, bytes_to_human_readable_repr, is_dunder
2929

3030

3131
class Missing:
@@ -243,6 +243,60 @@ def _belongs_to_runtime(r: types.ModuleType, attr: str) -> bool:
243243
)
244244

245245

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+
290+
if sys.version_info >= (3, 7):
291+
_WrapperDescriptorType = types.WrapperDescriptorType
292+
else:
293+
_WrapperDescriptorType = type(object.__init__)
294+
295+
296+
def is_private(name: str) -> bool:
297+
return name.startswith("_") and not is_dunder(name)
298+
299+
246300
@verify.register(nodes.TypeInfo)
247301
def verify_typeinfo(
248302
stub: nodes.TypeInfo, runtime: MaybeMissing[Type[Any]], object_path: List[str]
@@ -274,11 +328,9 @@ class SubClass(runtime): # type: ignore
274328

275329
# Check everything already defined in the stub
276330
to_check = set(stub.names)
277-
# There's a reasonable case to be made that we should always check all dunders, but it's
278-
# currently quite noisy. We could turn this into a denylist instead of an allowlist.
279331
to_check.update(
280332
# cast to workaround mypyc complaints
281-
m for m in cast(Any, vars)(runtime) if not m.startswith("_") or m in SPECIAL_DUNDERS
333+
m for m in cast(Any, vars)(runtime) if not is_private(m) and m not in IGNORED_DUNDERS
282334
)
283335

284336
for entry in sorted(to_check):
@@ -292,8 +344,16 @@ class SubClass(runtime): # type: ignore
292344
except Exception:
293345
# Catch all exceptions in case the runtime raises an unexpected exception
294346
# from __getattr__ or similar.
295-
pass
296-
else:
347+
continue
348+
# Do not error for an object missing from the stub
349+
# If the runtime object is a types.WrapperDescriptorType object
350+
# and has a non-special dunder name.
351+
# The vast majority of these are false positives.
352+
if not (
353+
isinstance(stub_to_verify, Missing)
354+
and isinstance(runtime_attr, _WrapperDescriptorType)
355+
and is_dunder(mangled_entry, exclude_special=True)
356+
):
297357
yield from verify(stub_to_verify, runtime_attr, object_path + [entry])
298358

299359

mypy/test/teststubtest.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def overload(func: _T) -> _T: ...
6565
VT = TypeVar('VT')
6666
6767
class object:
68+
__module__: str
6869
def __init__(self) -> None: pass
6970
class type: ...
7071
@@ -710,6 +711,16 @@ def h(x: str): ...
710711
yield Case(
711712
stub="from mystery import A, B as B, C as D # type: ignore", runtime="", error="B"
712713
)
714+
yield Case(
715+
stub="class Y: ...",
716+
runtime="__all__ += ['Y']\nclass Y:\n def __or__(self, other): return self|other",
717+
error="Y.__or__"
718+
)
719+
yield Case(
720+
stub="class Z: ...",
721+
runtime="__all__ += ['Z']\nclass Z:\n def __reduce__(self): return (Z,)",
722+
error=None
723+
)
713724

714725
@collect_cases
715726
def test_missing_no_runtime_all(self) -> Iterator[Case]:
@@ -731,7 +742,7 @@ def test_non_public_2(self) -> Iterator[Case]:
731742
yield Case(stub="g: int", runtime="def g(): ...", error="g")
732743

733744
@collect_cases
734-
def test_special_dunders(self) -> Iterator[Case]:
745+
def test_dunders(self) -> Iterator[Case]:
735746
yield Case(
736747
stub="class A:\n def __init__(self, a: int, b: int) -> None: ...",
737748
runtime="class A:\n def __init__(self, a, bx): pass",

0 commit comments

Comments
 (0)
0