8000 stubtest: error if a dunder method is missing from a stub by AlexWaygood · Pull Request #12203 · python/mypy · GitHub
[go: up one dir, main page]

Skip to content

stubtest: error if a dunder method is missing from a stub #12203

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Feb 19, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
stubtest: error if a dunder method is missing from a stub
  • Loading branch information
AlexWaygood committed Feb 17, 2022
commit 62b58754b13f522a52cebfeb65ad102db3da7010
66 changes: 61 additions & 5 deletions mypy/stubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from mypy import nodes
from mypy.config_parser import parse_config_file
from mypy.options import Options
from mypy.util import FancyFormatter, bytes_to_human_readable_repr, is_dunder, SPECIAL_DUNDERS
from mypy.util import FancyFormatter, bytes_to_human_readable_repr, is_dunder


class Missing:
Expand Down Expand Up @@ -243,6 +243,56 @@ def _belongs_to_runtime(r: types.ModuleType, attr: str) -> bool:
)


IGNORED_DUNDERS = frozenset({
# Very special attributes
"__weakref__",
"__slots__",
"__dict__",
"__text_signature__",
"__match_args__",
# Pickle methods
"__setstate__",
"__getstate__",
"__getnewargs__",
"__getinitargs__",
"__reduce_ex__",
# typing implementation details
"__parameters__",
"__origin__",
"__args__",
"__orig_bases__",
"__mro_entries__",
"__forward_is_class__",
"__forward_module__",
"__final__",
# isinstance/issubclass hooks that type-checkers don't usually care about
"__instancecheck__",
"__subclasshook__",
"__subclasscheck__",
# Dataclasses implementation details
"__dataclass_fields__",
"__dataclass_params__",
# ctypes weirdness
"__ctype_be__",
"__ctype_le__",
"__ctypes_from_outparam__",
# Two float methods only used internally for CPython test suite, not for public use
"__set_format__",
"__getformat__",
# These two are basically useless for type checkers
"__hash__",
"__getattr__",
"__abstractmethods__", # For some reason, mypy doesn't infer that classes with ABCMeta as the metaclass have this inherited
"__doc__", # Can only ever be str | None, who cares?
"__del__", # Only ever called when an object is being deleted, who cares?
"__new_member__", # If an enum class defines `__new__`, the method is renamed to be `__new_member__`
})


def is_private(name: str) -> bool:
return name.startswith("_") and not is_dunder(name)


@verify.register(nodes.TypeInfo)
def verify_typeinfo(
stub: nodes.TypeInfo, runtime: MaybeMissing[Type[Any]], object_path: List[str]
Expand Down Expand Up @@ -274,11 +324,9 @@ class SubClass(runtime): # type: ignore

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

for entry in sorted(to_check):
Expand Down Expand Up @@ -713,7 +761,15 @@ def verify_funcitem(
def verify_none(
stub: Missing, runtime: MaybeMissing[Any], object_path: List[str]
) -> Iterator[Error]:
yield Error(object_path, "is not present in stub", stub, runtime)
# Do not error for an object missing from the stub
# If the runtime object is a types.WrapperDescriptorType
# and has a non-special dunder name.
# The vast majority of these are false positives.
if not (
isinstance(runtime, types.WrapperDescriptorType)
and is_dunder(runtime.__name__, exclude_special=True)
):
yield Error(object_path, "is not present in stub", stub, runtime)


@verify.register(nodes.Var)
Expand Down
16 changes: 11 additions & 5 deletions mypy/test/teststubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def use_tmp_dir() -> Iterator[None]:
VT = TypeVar('VT')

class object:
__module__: str
def __init__(self) -> None: pass
class type: ...

Expand Down Expand Up @@ -678,7 +679,7 @@ def test_non_public_2(self) -> Iterator[Case]:
yield Case(stub="g: int", runtime="def g(): ...", error="g")

@collect_cases
def test_special_dunders(self) -> Iterator[Case]:
def test_dunders(self) -> Iterator[Case]:
yield Case(
stub="class A:\n def __init__(self, a: int, b: int) -> None: ...",
runtime="class A:\n def __init__(self, a, bx): pass",
Expand All @@ -689,21 +690,26 @@ def test_special_dunders(self) -> Iterator[Case]:
runtime="class B:\n def __call__(self, c, dx): pass",
error="B.__call__",
)
yield Case(
stub="class C:\n def __or__(self, other: C) -> C: ...",
runtime="class C: ...",
error="C.__or__",
)
if sys.version_info >= (3, 6):
yield Case(
stub=(
"class C:\n"
"class D:\n"
" def __init_subclass__(\n"
" cls, e: int = ..., **kwargs: int\n"
" ) -> None: ...\n"
),
runtime="class C:\n def __init_subclass__(cls, e=1, **kwargs): pass",
runtime="class D:\n def __init_subclass__(cls, e=1, **kwargs): pass",
error=None,
)
if sys.version_info >= (3, 9):
yield Case(
stub="class D:\n def __class_getitem__(cls, type: type) -> type: ...",
runtime="class D:\n def __class_getitem__(cls, type): ...",
stub="class E:\n def __class_getitem__(cls, type: type) -> type: ...",
runtime="class E:\n def __class_getitem__(cls, type): ...",
error=None,
)

Expand Down
0