Skip to content

Commit b4aa788

Browse files
committed
Fix method plugin hooks to use defining class fullname
method_fullname() now resolves the class where a method was defined via get_containing_type_info(), matching the documented plugin hook semantics (Base.method rather than Derived.method for inherited calls). Fixes #19181
1 parent 29e9c43 commit b4aa788

5 files changed

Lines changed: 53 additions & 9 deletions

File tree

mypy/checkexpr.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -679,8 +679,8 @@ def check_str_format_call(self, e: CallExpr) -> None:
679679
self.strfrm_checker.check_str_format_call(e, format_value)
680680

681681
def method_fullname(self, object_type: Type, method_name: str) -> str | None:
682-
"""Convert a method name to a fully qualified name, based on the type of the object that
683-
it is invoked on. Return `None` if the name of `object_type` cannot be determined.
682+
"""Convert a method name to a fully qualified name, based on the class where the
683+
method was defined. Return `None` if the name of `object_type` cannot be determined.
684684
"""
685685
object_type = get_proper_type(object_type)
686686

@@ -694,12 +694,15 @@ def method_fullname(self, object_type: Type, method_name: str) -> str | None:
694694

695695
type_name = None
696696
if isinstance(object_type, Instance):
697-
type_name = object_type.type.fullname
697+
info = object_type.type.get_containing_type_info(method_name)
698+
type_name = info.fullname if info is not None else object_type.type.fullname
698699
elif isinstance(object_type, (TypedDictType, LiteralType)):
699700
info = object_type.fallback.type.get_containing_type_info(method_name)
700701
type_name = info.fullname if info is not None else None
701702
elif isinstance(object_type, TupleType):
702-
type_name = tuple_fallback(object_type).type.fullname
703+
fallback = tuple_fallback(object_type)
704+
info = fallback.type.get_containing_type_info(method_name)
705+
type_name = info.fullname if info is not None else fallback.type.fullname
703706

704707
if type_name:
705708
return f"{type_name}.{method_name}"

mypy/plugin.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,7 @@ def get_method_signature_hook(
647647
may infer a better type for the method. The hook is also called for special
648648
Python dunder methods except __init__ and __new__ (use get_function_hook to customize
649649
class instantiation). This function is called with the method full name using
650-
the class of the object on which the method is called. For example, in this code:
650+
the class where it was _defined_. For example, in this code:
651651
652652
from lib import Special
653653
@@ -663,7 +663,7 @@ class Derived(Base):
663663
x: Special
664664
y = x[0]
665665
666-
this method is called with '__main__.Derived.method', and then with
666+
this method is called with '__main__.Base.method', and then with
667667
'lib.Special.__getitem__'.
668668
"""
669669
return None
@@ -672,7 +672,7 @@ def get_method_hook(self, fullname: str) -> Callable[[MethodContext], Type] | No
672672
"""Adjust return type of a method call.
673673
674674
This is the same as get_function_hook(), but is called with the
675-
method full name (using the class of the object on which the method is called).
675+
method full name (again, using the class where the method is defined).
676676
"""
677677
return None
678678

test-data/unit/check-custom-plugin.test

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,22 @@ plugins=<ROOT>/test-data/unit/plugins/fully_qualified_test_hook.py
602602
[builtins fixtures/classmethod.pyi]
603603
[typing fixtures/typing-typeddict.pyi]
604604

605+
[case testMethodSignatureHookUsesDefiningClass]
606+
# flags: --config-file tmp/mypy.ini
607+
from typing import Any
608+
609+
class Base:
610+
def method(self, arg: Any) -> Any: ...
611+
612+
class Derived(Base): ...
613+
614+
var: Derived
615+
reveal_type(var.method(42)) # N: Revealed type is "builtins.int"
616+
617+
[file mypy.ini]
618+
\[mypy]
619+
plugins=<ROOT>/test-data/unit/plugins/method_hook_defining_class.py
620+
605621
[case testDynamicClassPlugin]
606622
# flags: --config-file tmp/mypy.ini
607623
from mod import declarative_base, Column, Instr

test-data/unit/plugins/arg_names.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ def get_method_hook(self, fullname: str) -> Callable[[MethodContext], Type] | No
2626
"mod.Class.mystaticmethod",
2727
"mod.ClassUnfilled.method",
2828
"mod.ClassStarExpr.method",
29-
"mod.ClassChild.method",
30-
"mod.ClassChild.myclassmethod",
29+
"mod.Base.method",
30+
"mod.Base.myclassmethod",
3131
}:
3232
return extract_classname_and_set_as_return_type_method
3333
return None
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from __future__ import annotations
2+
3+
from typing import Callable
4+
5+
from mypy.plugin import MethodSigContext, Plugin
6+
from mypy.types import CallableType
7+
8+
9+
class DefiningClassPlugin(Plugin):
10+
def get_method_signature_hook(
11+
self, fullname: str
12+
) -> Callable[[MethodSigContext], CallableType] | None:
13+
if fullname == "__main__.Base.method":
14+
return defining_class_hook
15+
return None
16+
17+
18+
def defining_class_hook(ctx: MethodSigContext) -> CallableType:
19+
return ctx.default_signature.copy_modified(
20+
ret_type=ctx.api.named_generic_type("builtins.int", [])
21+
)
22+
23+
24+
def plugin(version: str) -> type[DefiningClassPlugin]:
25+
return DefiningClassPlugin

0 commit comments

Comments
 (0)