Skip to content

Commit b6f2ea3

Browse files
authored
Handle assignment of bound methods in class bodies (#19233)
Fixes #18438 Fixes #19146 Surprisingly, a very small change is sufficient to replicate Python runtime behavior for all the important cases (see `checkmember.py`). I also replace the `bound_args` argument of `CallableType`, that was mostly unused, with a flag (as suggested by @JukkaL) and make sure it is properly set/preserved everywhere.
1 parent faac780 commit b6f2ea3

File tree

12 files changed

+110
-21
lines changed

12 files changed

+110
-21
lines changed

mypy/checker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2449,7 +2449,7 @@ def erase_override(t: Type) -> Type:
24492449
if not is_subtype(original_arg_type, erase_override(override_arg_type)):
24502450
context: Context = node
24512451
if isinstance(node, FuncDef) and not node.is_property:
2452-
arg_node = node.arguments[i + len(override.bound_args)]
2452+
arg_node = node.arguments[i + override.bound()]
24532453
if arg_node.line != -1:
24542454
context = arg_node
24552455
self.msg.argument_incompatible_with_supertype(

mypy/checkexpr.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4975,7 +4975,7 @@ def apply_type_arguments_to_callable(
49754975
tp.fallback,
49764976
name="tuple",
49774977
definition=tp.definition,
4978-
bound_args=tp.bound_args,
4978+
is_bound=tp.is_bound,
49794979
)
49804980
self.msg.incompatible_type_application(
49814981
min_arg_count, len(type_vars), len(args), ctx

mypy/checkmember.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -921,7 +921,7 @@ def analyze_var(
921921
bound_items = []
922922
for ct in call_type.items if isinstance(call_type, UnionType) else [call_type]:
923923
p_ct = get_proper_type(ct)
924-
if isinstance(p_ct, FunctionLike) and not p_ct.is_type_obj():
924+
if isinstance(p_ct, FunctionLike) and (not p_ct.bound() or var.is_property):
925925
item = expand_and_bind_callable(p_ct, var, itype, name, mx, is_trivial_self)
926926
else:
927927
item = expand_without_binding(ct, var, itype, original_itype, mx)
@@ -1498,6 +1498,6 @@ def bind_self_fast(method: F, original_type: Type | None = None) -> F:
14981498
arg_types=func.arg_types[1:],
14991499
arg_kinds=func.arg_kinds[1:],
15001500
arg_names=func.arg_names[1:],
1501-
bound_args=[original_type],
1501+
is_bound=True,
15021502
)
15031503
return cast(F, res)

mypy/fixup.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -271,9 +271,6 @@ def visit_callable_type(self, ct: CallableType) -> None:
271271
ct.ret_type.accept(self)
272272
for v in ct.variables:
273273
v.accept(self)
274-
for arg in ct.bound_args:
275-
if arg:
276-
arg.accept(self)
277274
if ct.type_guard is not None:
278275
ct.type_guard.accept(self)
279276
if ct.type_is is not None:

mypy/messages.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -644,8 +644,8 @@ def incompatible_argument(
644644
callee_name = callable_name(callee)
645645
if callee_name is not None:
646646
name = callee_name
647-
if callee.bound_args and callee.bound_args[0] is not None:
648-
base = format_type(callee.bound_args[0], self.options)
647+
if object_type is not None:
648+
base = format_type(object_type, self.options)
649649
else:
650650
base = extract_type(name)
651651

mypy/server/astdiff.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,7 @@ def visit_callable_type(self, typ: CallableType) -> SnapshotItem:
460460
typ.is_type_obj(),
461461
typ.is_ellipsis_args,
462462
snapshot_types(typ.variables),
463+
typ.is_bound,
463464
)
464465

465466
def normalize_callable_variables(self, typ: CallableType) -> CallableType:

mypy/typeops.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ def type_object_type(info: TypeInfo, named_type: Callable[[str], Instance]) -> P
185185
arg_kinds=[ARG_STAR, ARG_STAR2],
186186
arg_names=["_args", "_kwds"],
187187
ret_type=any_type,
188+
is_bound=True,
188189
fallback=named_type("builtins.function"),
189190
)
190191
return class_callable(sig, info, fallback, None, is_new=False)
@@ -479,7 +480,7 @@ class B(A): pass
479480
arg_kinds=func.arg_kinds[1:],
480481
arg_names=func.arg_names[1:],
481482
variables=variables,
482-
bound_args=[original_type],
483+
is_bound=True,
483484
)
484485
return cast(F, res)
485486

mypy/types.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1605,6 +1605,9 @@ def with_name(self, name: str) -> FunctionLike:
16051605
def get_name(self) -> str | None:
16061606
pass
16071607

1608+
def bound(self) -> bool:
1609+
return bool(self.items) and self.items[0].is_bound
1610+
16081611

16091612
class FormalArgument(NamedTuple):
16101613
name: str | None
@@ -1834,8 +1837,7 @@ class CallableType(FunctionLike):
18341837
# 'dict' and 'partial' for a `functools.partial` evaluation)
18351838
"from_type_type", # Was this callable generated by analyzing Type[...]
18361839
# instantiation?
1837-
"bound_args", # Bound type args, mostly unused but may be useful for
1838-
# tools that consume mypy ASTs
1840+
"is_bound", # Is this a bound method?
18391841
"def_extras", # Information about original definition we want to serialize.
18401842
# This is used for more detailed error messages.
18411843
"type_guard", # T, if -> TypeGuard[T] (ret_type is bool in this case).
@@ -1863,7 +1865,7 @@ def __init__(
18631865
implicit: bool = False,
18641866
special_sig: str | None = None,
18651867
from_type_type: bool = False,
1866-
bound_args: Sequence[Type | None] = (),
1868+
is_bound: bool = False,
18671869
def_extras: dict[str, Any] | None = None,
18681870
type_guard: Type | None = None,
18691871
type_is: Type | None = None,
@@ -1896,9 +1898,7 @@ def __init__(
18961898
self.from_type_type = from_type_type
18971899
self.from_concatenate = from_concatenate
18981900
self.imprecise_arg_kinds = imprecise_arg_kinds
1899-
if not bound_args:
1900-
bound_args = ()
1901-
self.bound_args = bound_args
1901+
self.is_bound = is_bound
19021902
if def_extras:
19031903
self.def_extras = def_extras
19041904
elif isinstance(definition, FuncDef):
@@ -1935,7 +1935,7 @@ def copy_modified(
19351935
implicit: Bogus[bool] = _dummy,
19361936
special_sig: Bogus[str | None] = _dummy,
19371937
from_type_type: Bogus[bool] = _dummy,
1938-
bound_args: Bogus[list[Type | None]] = _dummy,
1938+
is_bound: Bogus[bool] = _dummy,
19391939
def_extras: Bogus[dict[str, Any]] = _dummy,
19401940
type_guard: Bogus[Type | None] = _dummy,
19411941
type_is: Bogus[Type | None] = _dummy,
@@ -1960,7 +1960,7 @@ def copy_modified(
19601960
implicit=implicit if implicit is not _dummy else self.implicit,
19611961
special_sig=special_sig if special_sig is not _dummy else self.special_sig,
19621962
from_type_type=from_type_type if from_type_type is not _dummy else self.from_type_type,
1963-
bound_args=bound_args if bound_args is not _dummy else self.bound_args,
1963+
is_bound=is_bound if is_bound is not _dummy else self.is_bound,
19641964
def_extras=def_extras if def_extras is not _dummy else dict(self.def_extras),
19651965
type_guard=type_guard if type_guard is not _dummy else self.type_guard,
19661966
type_is=type_is if type_is is not _dummy else self.type_is,
@@ -2285,7 +2285,7 @@ def serialize(self) -> JsonDict:
22852285
"variables": [v.serialize() for v in self.variables],
22862286
"is_ellipsis_args": self.is_ellipsis_args,
22872287
"implicit": self.implicit,
2288-
"bound_args": [(None if t is None else t.serialize()) for t in self.bound_args],
2288+
"is_bound": self.is_bound,
22892289
"def_extras": dict(self.def_extras),
22902290
"type_guard": self.type_guard.serialize() if self.type_guard is not None else None,
22912291
"type_is": (self.type_is.serialize() if self.type_is is not None else None),
@@ -2308,7 +2308,7 @@ def deserialize(cls, data: JsonDict) -> CallableType:
23082308
variables=[cast(TypeVarLikeType, deserialize_type(v)) for v in data["variables"]],
23092309
is_ellipsis_args=data["is_ellipsis_args"],
23102310
implicit=data["implicit"],
2311-
bound_args=[(None if t is None else deserialize_type(t)) for t in data["bound_args"]],
2311+
is_bound=data["is_bound"],
23122312
def_extras=data["def_extras"],
23132313
type_guard=(
23142314
deserialize_type(data["type_guard"]) if data["type_guard"] is not None else None

test-data/unit/check-classes.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4292,7 +4292,7 @@ int.__eq__(3, 4)
42924292
[builtins fixtures/args.pyi]
42934293
[out]
42944294
main:33: error: Too few arguments for "__eq__" of "int"
4295-
main:33: error: Unsupported operand types for == ("int" and "type[int]")
4295+
main:33: error: Unsupported operand types for == ("type[int]" and "type[int]")
42964296

42974297
[case testDupBaseClasses]
42984298
class A:

test-data/unit/check-functions.test

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3591,3 +3591,45 @@ class Bar(Foo):
35913591

35923592
def foo(self, value: Union[int, str]) -> Union[int, str]:
35933593
return super().foo(value) # E: Call to abstract method "foo" of "Foo" with trivial body via super() is unsafe
3594+
3595+
[case testBoundMethodsAssignedInClassBody]
3596+
from typing import Callable
3597+
3598+
class A:
3599+
def f(self, x: int) -> str:
3600+
pass
3601+
@classmethod
3602+
def g(cls, x: int) -> str:
3603+
pass
3604+
@staticmethod
3605+
def h(x: int) -> str:
3606+
pass
3607+
attr: Callable[[int], str]
3608+
3609+
class C:
3610+
x1 = A.f
3611+
x2 = A.g
3612+
x3 = A().f
3613+
x4 = A().g
3614+
x5 = A.h
3615+
x6 = A().h
3616+
x7 = A().attr
3617+
3618+
reveal_type(C.x1) # N: Revealed type is "def (self: __main__.A, x: builtins.int) -> builtins.str"
3619+
reveal_type(C.x2) # N: Revealed type is "def (x: builtins.int) -> builtins.str"
3620+
reveal_type(C.x3) # N: Revealed type is "def (x: builtins.int) -> builtins.str"
3621+
reveal_type(C.x4) # N: Revealed type is "def (x: builtins.int) -> builtins.str"
3622+
reveal_type(C.x5) # N: Revealed type is "def (x: builtins.int) -> builtins.str"
3623+
reveal_type(C.x6) # N: Revealed type is "def (x: builtins.int) -> builtins.str"
3624+
reveal_type(C.x7) # N: Revealed type is "def (builtins.int) -> builtins.str"
3625+
3626+
reveal_type(C().x1) # E: Invalid self argument "C" to attribute function "x1" with type "Callable[[A, int], str]" \
3627+
# N: Revealed type is "def (x: builtins.int) -> builtins.str"
3628+
reveal_type(C().x2) # N: Revealed type is "def (x: builtins.int) -> builtins.str"
3629+
reveal_type(C().x3) # N: Revealed type is "def (x: builtins.int) -> builtins.str"
3630+
reveal_type(C().x4) # N: Revealed type is "def (x: builtins.int) -> builtins.str"
3631+
reveal_type(C().x5) # N: Revealed type is "def (x: builtins.int) -> builtins.str"
3632+
reveal_type(C().x6) # N: Revealed type is "def (x: builtins.int) -> builtins.str"
3633+
reveal_type(C().x7) # E: Invalid self argument "C" to attribute function "x7" with type "Callable[[int], str]" \
3634+
# N: Revealed type is "def () -> builtins.str"
3635+
[builtins fixtures/classmethod.pyi]

test-data/unit/check-incremental.test

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6862,3 +6862,27 @@ if int():
68626862
[out]
68636863
[out2]
68646864
main:6: error: Incompatible types in assignment (expression has type "str", variable has type "int")
6865+
6866+
[case testMethodMakeBoundIncremental]
6867+
from a import A
6868+
a = A()
6869+
a.f()
6870+
[file a.py]
6871+
class B:
6872+
def f(self, s: A) -> int: ...
6873+
6874+
def f(s: A) -> int: ...
6875+
6876+
class A:
6877+
f = f
6878+
[file a.py.2]
6879+
class B:
6880+
def f(self, s: A) -> int: ...
6881+
6882+
def f(s: A) -> int: ...
6883+
6884+
class A:
6885+
f = B().f
6886+
[out]
6887+
[out2]
6888+
main:3: error: Too few arguments

test-data/unit/fine-grained.test

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11217,3 +11217,27 @@ class A:
1121711217
[out]
1121811218
==
1121911219
main:3: error: Property "f" defined in "A" is read-only
11220+
11221+
[case testMethodMakeBoundFineGrained]
11222+
from a import A
11223+
a = A()
11224+
a.f()
11225+
[file a.py]
11226+
class B:
11227+
def f(self, s: A) -> int: ...
11228+
11229+
def f(s: A) -> int: ...
11230+
11231+
class A:
11232+
f = f
11233+
[file a.py.2]
11234+
class B:
11235+
def f(self, s: A) -> int: ...
11236+
11237+
def f(s: A) -> int: ...
11238+
11239+
class A:
11240+
f = B().f
11241+
[out]
11242+
==
11243+
main:3: error: Too few arguments

0 commit comments

Comments
 (0)