Skip to content

stubtest: improve handling of special dunders #9626

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 1 commit into from
Oct 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
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
38 changes: 25 additions & 13 deletions mypy/stubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,11 +244,13 @@ def verify_typeinfo(
yield Error(object_path, "is not a type", stub, runtime, stub_desc=repr(stub))
return

# Check everything already defined in the stub
to_check = set(stub.names)
dunders_to_check = ("__init__", "__new__", "__call__", "__class_getitem__")
# cast to workaround mypyc complaints
# 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(
m for m in cast(Any, vars)(runtime) if m in dunders_to_check or not m.startswith("_")
# cast to workaround mypyc complaints
m for m in cast(Any, vars)(runtime) if not m.startswith("_") or m in SPECIAL_DUNDERS
)

for entry in sorted(to_check):
Expand All @@ -265,8 +267,8 @@ def verify_typeinfo(
def _verify_static_class_methods(
stub: nodes.FuncItem, runtime: types.FunctionType, object_path: List[str]
) -> Iterator[str]:
if stub.name == "__new__":
# Special cased by Python, so never declared as staticmethod
if stub.name in ("__new__", "__init_subclass__", "__class_getitem__"):
# Special cased by Python, so don't bother checking
return
if inspect.isbuiltin(runtime):
# The isinstance checks don't work reliably for builtins, e.g. datetime.datetime.now, so do
Expand Down Expand Up @@ -303,8 +305,8 @@ def _verify_arg_name(
stub_arg: nodes.Argument, runtime_arg: inspect.Parameter, function_name: str
) -> Iterator[str]:
"""Checks whether argument names match."""
# Ignore exact names for all dunder methods other than __init__
if is_dunder(function_name, exclude_init=True):
# Ignore exact names for most dunder methods
if is_dunder(function_name, exclude_special=True):
return

def strip_prefix(s: str, prefix: str) -> str:
Expand Down Expand Up @@ -468,8 +470,8 @@ def from_overloadedfuncdef(stub: nodes.OverloadedFuncDef) -> "Signature[nodes.Ar
lies it might try to tell.

"""
# For all dunder methods other than __init__, just assume all args are positional-only
assume_positional_only = is_dunder(stub.name, exclude_init=True)
# For most dunder methods, just assume all args are positional-only
assume_positional_only = is_dunder(stub.name, exclude_special=True)

all_args = {} # type: Dict[str, List[Tuple[nodes.Argument, int]]]
for func in map(_resolve_funcitem_from_decorator, stub.items):
Expand Down Expand Up @@ -548,7 +550,7 @@ def _verify_signature(
runtime_arg.kind == inspect.Parameter.POSITIONAL_ONLY
and not stub_arg.variable.name.startswith("__")
and not stub_arg.variable.name.strip("_") == "self"
and not is_dunder(function_name) # noisy for dunder methods
and not is_dunder(function_name, exclude_special=True) # noisy for dunder methods
):
yield (
'stub argument "{}" should be positional-only '
Expand Down Expand Up @@ -656,6 +658,13 @@ def verify_funcitem(
# catch RuntimeError because of https://bugs.python.org/issue39504
return

if stub.name in ("__init_subclass__", "__class_getitem__"):
# These are implicitly classmethods. If the stub chooses not to have @classmethod, we
# should remove the cls argument
if stub.arguments[0].variable.name == "cls":
stub = copy.copy(stub)
stub.arguments = stub.arguments[1:]

stub_sig = Signature.from_funcitem(stub)
runtime_sig = Signature.from_inspect_signature(signature)

Expand Down Expand Up @@ -846,13 +855,16 @@ def verify_typealias(
yield None


def is_dunder(name: str, exclude_init: bool = False) -> bool:
SPECIAL_DUNDERS = ("__init__", "__new__", "__call__", "__init_subclass__", "__class_getitem__")


def is_dunder(name: str, exclude_special: bool = False) -> bool:
"""Returns whether name is a dunder name.

:param exclude_init: Whether to return False for __init__
:param exclude_special: Whether to return False for a couple special dunder methods.

"""
if exclude_init and name == "__init__":
if exclude_special and name in SPECIAL_DUNDERS:
return False
return name.startswith("__") and name.endswith("__")

Expand Down
25 changes: 25 additions & 0 deletions mypy/test/teststubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,31 @@ def test_missing_no_runtime_all(self) -> Iterator[Case]:
yield Case(stub="", runtime="import sys", error=None)
yield Case(stub="", runtime="def g(): ...", error="g")

@collect_cases
def test_special_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",
error="A.__init__",
)
yield Case(
stub="class B:\n def __call__(self, c: int, d: int) -> None: ...",
runtime="class B:\n def __call__(self, c, dx): pass",
error="B.__call__",
)
if sys.version_info >= (3, 6):
yield Case(
stub="class C:\n def __init_subclass__(cls, e: int, **kwargs: int) -> None: ...",
runtime="class C:\n def __init_subclass__(cls, e, **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): ...",
error=None,
)

@collect_cases
def test_name_mangling(self) -> Iterator[Case]:
yield Case(
Expand Down
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy