Skip to content

Commit af3c8be

Browse files
authored
stubtest: improve handling of special dunders (python#9626)
Reckon with the fact that __init_subclass__ and __class_getitem__ are special cased to be implicit classmethods. Fix some false negatives for other special dunders. Co-authored-by: hauntsaninja <>
1 parent db8de92 commit af3c8be

File tree

2 files changed

+50
-13
lines changed

2 files changed

+50
-13
lines changed

mypy/stubtest.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -244,11 +244,13 @@ def verify_typeinfo(
244244
yield Error(object_path, "is not a type", stub, runtime, stub_desc=repr(stub))
245245
return
246246

247+
# Check everything already defined in the stub
247248
to_check = set(stub.names)
248-
dunders_to_check = ("__init__", "__new__", "__call__", "__class_getitem__")
249-
# cast to workaround mypyc complaints
249+
# There's a reasonable case to be made that we should always check all dunders, but it's
250+
# currently quite noisy. We could turn this into a denylist instead of an allowlist.
250251
to_check.update(
251-
m for m in cast(Any, vars)(runtime) if m in dunders_to_check or not m.startswith("_")
252+
# cast to workaround mypyc complaints
253+
m for m in cast(Any, vars)(runtime) if not m.startswith("_") or m in SPECIAL_DUNDERS
252254
)
253255

254256
for entry in sorted(to_check):
@@ -265,8 +267,8 @@ def verify_typeinfo(
265267
def _verify_static_class_methods(
266268
stub: nodes.FuncItem, runtime: types.FunctionType, object_path: List[str]
267269
) -> Iterator[str]:
268-
if stub.name == "__new__":
269-
# Special cased by Python, so never declared as staticmethod
270+
if stub.name in ("__new__", "__init_subclass__", "__class_getitem__"):
271+
# Special cased by Python, so don't bother checking
270272
return
271273
if inspect.isbuiltin(runtime):
272274
# The isinstance checks don't work reliably for builtins, e.g. datetime.datetime.now, so do
@@ -303,8 +305,8 @@ def _verify_arg_name(
303305
stub_arg: nodes.Argument, runtime_arg: inspect.Parameter, function_name: str
304306
) -> Iterator[str]:
305307
"""Checks whether argument names match."""
306-
# Ignore exact names for all dunder methods other than __init__
307-
if is_dunder(function_name, exclude_init=True):
308+
# Ignore exact names for most dunder methods
309+
if is_dunder(function_name, exclude_special=True):
308310
return
309311

310312
def strip_prefix(s: str, prefix: str) -> str:
@@ -468,8 +470,8 @@ def from_overloadedfuncdef(stub: nodes.OverloadedFuncDef) -> "Signature[nodes.Ar
468470
lies it might try to tell.
469471
470472
"""
471-
# For all dunder methods other than __init__, just assume all args are positional-only
472-
assume_positional_only = is_dunder(stub.name, exclude_init=True)
473+
# For most dunder methods, just assume all args are positional-only
474+
assume_positional_only = is_dunder(stub.name, exclude_special=True)
473475

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

661+
if stub.name in ("__init_subclass__", "__class_getitem__"):
662+
# These are implicitly classmethods. If the stub chooses not to have @classmethod, we
663+
# should remove the cls argument
664+
if stub.arguments[0].variable.name == "cls":
665+
stub = copy.copy(stub)
666+
stub.arguments = stub.arguments[1:]
667+
659668
stub_sig = Signature.from_funcitem(stub)
660669
runtime_sig = Signature.from_inspect_signature(signature)
661670

@@ -846,13 +855,16 @@ def verify_typealias(
846855
yield None
847856

848857

849-
def is_dunder(name: str, exclude_init: bool = False) -> bool:
858+
SPECIAL_DUNDERS = ("__init__", "__new__", "__call__", "__init_subclass__", "__class_getitem__")
859+
860+
861+
def is_dunder(name: str, exclude_special: bool = False) -> bool:
850862
"""Returns whether name is a dunder name.
851863
852-
:param exclude_init: Whether to return False for __init__
864+
:param exclude_special: Whether to return False for a couple special dunder methods.
853865
854866
"""
855-
if exclude_init and name == "__init__":
867+
if exclude_special and name in SPECIAL_DUNDERS:
856868
return False
857869
return name.startswith("__") and name.endswith("__")
858870

mypy/test/teststubtest.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,31 @@ def test_missing_no_runtime_all(self) -> Iterator[Case]:
628628
yield Case(stub="", runtime="import sys", error=None)
629629
yield Case(stub="", runtime="def g(): ...", error="g")
630630

631+
@collect_cases
632+
def test_special_dunders(self) -> Iterator[Case]:
633+
yield Case(
634+
stub="class A:\n def __init__(self, a: int, b: int) -> None: ...",
635+
runtime="class A:\n def __init__(self, a, bx): pass",
636+
error="A.__init__",
637+
)
638+
yield Case(
639+
stub="class B:\n def __call__(self, c: int, d: int) -> None: ...",
640+
runtime="class B:\n def __call__(self, c, dx): pass",
641+
error="B.__call__",
642+
)
643+
if sys.version_info >= (3, 6):
644+
yield Case(
645+
stub="class C:\n def __init_subclass__(cls, e: int, **kwargs: int) -> None: ...",
646+
runtime="class C:\n def __init_subclass__(cls, e, **kwargs): pass",
647+
error=None,
648+
)
649+
if sys.version_info >= (3, 9):
650+
yield Case(
651+
stub="class D:\n def __class_getitem__(cls, type: type) -> type: ...",
652+
runtime="class D:\n def __class_getitem__(cls, type): ...",
653+
error=None,
654+
)
655+
631656
@collect_cases
632657
def test_name_mangling(self) -> Iterator[Case]:
633658
yield Case(

0 commit comments

Comments
 (0)
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