Skip to content
Open
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
20 changes: 16 additions & 4 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -679,8 +679,11 @@ def check_str_format_call(self, e: CallExpr) -> None:
self.strfrm_checker.check_str_format_call(e, format_value)

def method_fullname(self, object_type: Type, method_name: str) -> str | None:
"""Convert a method name to a fully qualified name, based on the type of the object that
it is invoked on. Return `None` if the name of `object_type` cannot be determined.
"""Convert a method name to a fully qualified name.

By default this uses the class of the object where the method is called.
If the use_method_hook_defining_class option is enabled, this uses the class
where the method was defined.
"""
object_type = get_proper_type(object_type)

Expand All @@ -694,12 +697,21 @@ def method_fullname(self, object_type: Type, method_name: str) -> str | None:

type_name = None
if isinstance(object_type, Instance):
type_name = object_type.type.fullname
if self.chk.options.use_method_hook_defining_class:
info = object_type.type.get_containing_type_info(method_name)
type_name = info.fullname if info is not None else object_type.type.fullname
else:
type_name = object_type.type.fullname
elif isinstance(object_type, (TypedDictType, LiteralType)):
info = object_type.fallback.type.get_containing_type_info(method_name)
type_name = info.fullname if info is not None else None
elif isinstance(object_type, TupleType):
type_name = tuple_fallback(object_type).type.fullname
fallback = tuple_fallback(object_type)
if self.chk.options.use_method_hook_defining_class:
info = fallback.type.get_containing_type_info(method_name)
type_name = info.fullname if info is not None else fallback.type.fullname
else:
type_name = fallback.type.fullname

if type_name:
return f"{type_name}.{method_name}"
Expand Down
6 changes: 6 additions & 0 deletions mypy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1177,6 +1177,12 @@ def add_invertible_flag(
)
# This undocumented feature exports limited line-level dependency information.
internals_group.add_argument("--export-ref-info", action="store_true", help=argparse.SUPPRESS)
add_invertible_flag(
"--use-method-hook-defining-class",
default=False,
help=argparse.SUPPRESS,
group=internals_group,
)

# Experimental parallel type-checking support.
internals_group.add_argument(
Expand Down
5 changes: 5 additions & 0 deletions mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class BuildType:
"strict_bytes",
"fixed_format_cache",
"untyped_calls_exclude",
"use_method_hook_defining_class",
"enable_incomplete_feature",
"install_types",
}
Expand Down Expand Up @@ -340,6 +341,10 @@ def __init__(self) -> None:

# Paths of user plugins
self.plugins: list[str] = []
# Temporary opt-in compatibility flag for plugin method hook fullnames.
# If True, inherited calls use the defining class (Base.method). If False,
# they use the call-site class (Derived.method).
self.use_method_hook_defining_class = False

# Per-module options (raw)
self.per_module_options: dict[str, dict[str, object]] = {}
Expand Down
9 changes: 6 additions & 3 deletions mypy/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,8 @@ def get_method_signature_hook(
may infer a better type for the method. The hook is also called for special
Python dunder methods except __init__ and __new__ (use get_function_hook to customize
class instantiation). This function is called with the method full name using
the class where it was _defined_. For example, in this code:
the class of the object on which the method is called, unless
use_method_hook_defining_class is enabled. For example, in this code:

from lib import Special

Expand All @@ -663,7 +664,8 @@ class Derived(Base):
x: Special
y = x[0]

this method is called with '__main__.Base.method', and then with
this method is called with '__main__.Derived.method' (or '__main__.Base.method'
with use_method_hook_defining_class), and then with
'lib.Special.__getitem__'.
"""
return None
Expand All @@ -672,7 +674,8 @@ def get_method_hook(self, fullname: str) -> Callable[[MethodContext], Type] | No
"""Adjust return type of a method call.

This is the same as get_function_hook(), but is called with the
method full name (again, using the class where the method is defined).
method full name (with the same class selection behavior as
get_method_signature_hook()).
"""
return None

Expand Down
17 changes: 17 additions & 0 deletions test-data/unit/check-custom-plugin.test
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,23 @@ plugins=<ROOT>/test-data/unit/plugins/fully_qualified_test_hook.py
[builtins fixtures/classmethod.pyi]
[typing fixtures/typing-typeddict.pyi]

[case testMethodSignatureHookUsesDefiningClass]
# flags: --config-file tmp/mypy.ini
from typing import Any

class Base:
def method(self, arg: Any) -> Any: ...

class Derived(Base): ...

var: Derived
reveal_type(var.method(42)) # N: Revealed type is "builtins.int"

[file mypy.ini]
\[mypy]
plugins=<ROOT>/test-data/unit/plugins/method_hook_defining_class.py
use_method_hook_defining_class = True

[case testDynamicClassPlugin]
# flags: --config-file tmp/mypy.ini
from mod import declarative_base, Column, Instr
Expand Down
25 changes: 25 additions & 0 deletions test-data/unit/plugins/method_hook_defining_class.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from __future__ import annotations

from typing import Callable

from mypy.plugin import MethodSigContext, Plugin
from mypy.types import CallableType


class DefiningClassPlugin(Plugin):
def get_method_signature_hook(
self, fullname: str
) -> Callable[[MethodSigContext], CallableType] | None:
if fullname == "__main__.Base.method":
return defining_class_hook
return None


def defining_class_hook(ctx: MethodSigContext) -> CallableType:
return ctx.default_signature.copy_modified(
ret_type=ctx.api.named_generic_type("builtins.int", [])
)


def plugin(version: str) -> type[DefiningClassPlugin]:
return DefiningClassPlugin
Loading