Skip to content

Commit c20fd78

Browse files
ilevkivskyiJukkaL
authored andcommitted
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 c86480c commit c20fd78

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
@@ -2448,7 +2448,7 @@ def erase_override(t: Type) -> Type:
24482448
if not is_subtype(original_arg_type, erase_override(override_arg_type)):
24492449
context: Context = node
24502450
if isinstance(node, FuncDef) and not node.is_property:
2451-
arg_node = node.arguments[i + len(override.bound_args)]
2451+
arg_node = node.arguments[i + override.bound()]
24522452
if arg_node.line != -1:
24532453
context = arg_node
24542454
self.msg.argument_incompatible_with_supertype(

mypy/checkexpr.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4970,7 +4970,7 @@ def apply_type_arguments_to_callable(
49704970
tp.fallback,
49714971
name="tuple",
49724972
definition=tp.definition,
4973-
bound_args=tp.bound_args,
4973+
is_bound=tp.is_bound,
49744974
)
49754975
self.msg.incompatible_type_application(
49764976
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
@@ -916,7 +916,7 @@ def analyze_var(
916916
bound_items = []
917917
for ct in call_type.items if isinstance(call_type, UnionType) else [call_type]:
918918
p_ct = get_proper_type(ct)
919-
if isinstance(p_ct, FunctionLike) and not p_ct.is_type_obj():
919+
if isinstance(p_ct, FunctionLike) and (not p_ct.bound() or var.is_property):
920920
item = expand_and_bind_callable(p_ct, var, itype, name, mx, is_trivial_self)
921921
else:
922922
item = expand_without_binding(ct, var, itype, original_itype, mx)
@@ -1503,6 +1503,6 @@ def bind_self_fast(method: F, original_type: Type | None = None) -> F:
15031503
arg_types=method.arg_types[1:],
15041504
arg_kinds=method.arg_kinds[1:],
15051505
arg_names=method.arg_names[1:],
1506-
bound_args=[original_type],
1506+
is_bound=True,
15071507
)
15081508
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
@@ -1597,6 +1597,9 @@ def with_name(self, name: str) -> FunctionLike:
15971597
def get_name(self) -> str | None:
15981598
pass
15991599

1600+
def bound(self) -> bool:
1601+
return bool(self.items) and self.items[0].is_bound
1602+
16001603

16011604
class FormalArgument(NamedTuple):
16021605
name: str | None
@@ -1826,8 +1829,7 @@ class CallableType(FunctionLike):
18261829
# 'dict' and 'partial' for a `functools.partial` evaluation)
18271830
"from_type_type", # Was this callable generated by analyzing Type[...]
18281831
# instantiation?
1829-
"bound_args", # Bound type args, mostly unused but may be useful for
1830-
# tools that consume mypy ASTs
1832+
"is_bound", # Is this a bound method?
18311833
"def_extras", # Information about original definition we want to serialize.
18321834
# This is used for more detailed error messages.
18331835
"type_guard", # T, if -> TypeGuard[T] (ret_type is bool in this case).
@@ -1855,7 +1857,7 @@ def __init__(
18551857
implicit: bool = False,
18561858
special_sig: str | None = None,
18571859
from_type_type: bool = False,
1858-
bound_args: Sequence[Type | None] = (),
1860+
is_bound: bool = False,
18591861
def_extras: dict[str, Any] | None = None,
18601862
type_guard: Type | None = None,
18611863
type_is: Type | None = None,
@@ -1888,9 +1890,7 @@ def __init__(
18881890
self.from_type_type = from_type_type
18891891
self.from_concatenate = from_concatenate
18901892
self.imprecise_arg_kinds = imprecise_arg_kinds
1891-
if not bound_args:
1892-
bound_args = ()
1893-
self.bound_args = bound_args
1893+
self.is_bound = is_bound
18941894
if def_extras:
18951895
self.def_extras = def_extras
18961896
elif isinstance(definition, FuncDef):
@@ -1927,7 +1927,7 @@ def copy_modified(
19271927
implicit: Bogus[bool] = _dummy,
19281928
special_sig: Bogus[str | None] = _dummy,
19291929
from_type_type: Bogus[bool] = _dummy,
1930-
bound_args: Bogus[list[Type | None]] = _dummy,
1930+
is_bound: Bogus[bool] = _dummy,
19311931
def_extras: Bogus[dict[str, Any]] = _dummy,
19321932
type_guard: Bogus[Type | None] = _dummy,
19331933
type_is: Bogus[Type | None] = _dummy,
@@ -1952,7 +1952,7 @@ def copy_modified(
19521952
implicit=implicit if implicit is not _dummy else self.implicit,
19531953
special_sig=special_sig if special_sig is not _dummy else self.special_sig,
19541954
from_type_type=from_type_type if from_type_type is not _dummy else self.from_type_type,
1955-
bound_args=bound_args if bound_args is not _dummy else self.bound_args,
1955+
is_bound=is_bound if is_bound is not _dummy else self.is_bound,
19561956
def_extras=def_extras if def_extras is not _dummy else dict(self.def_extras),
19571957
type_guard=type_guard if type_guard is not _dummy else self.type_guard,
19581958
type_is=type_is if type_is is not _dummy else self.type_is,
@@ -2277,7 +2277,7 @@ def serialize(self) -> JsonDict:
22772277
"variables": [v.serialize() for v in self.variables],
22782278
"is_ellipsis_args": self.is_ellipsis_args,
22792279
"implicit": self.implicit,
2280-
"bound_args": [(None if t is None else t.serialize()) for t in self.bound_args],
2280+
"is_bound": self.is_bound,
22812281
"def_extras": dict(self.def_extras),
22822282
"type_guard": self.type_guard.serialize() if self.type_guard is not None else None,
22832283
"type_is": (self.type_is.serialize() if self.type_is is not None else None),
@@ -2300,7 +2300,7 @@ def deserialize(cls, data: JsonDict) -> CallableType:
23002300
variables=[cast(TypeVarLikeType, deserialize_type(v)) for v in data["variables"]],
23012301
is_ellipsis_args=data["is_ellipsis_args"],
23022302
implicit=data["implicit"],
2303-
bound_args=[(None if t is None else deserialize_type(t)) for t in data["bound_args"]],
2303+
is_bound=data["is_bound"],
23042304
def_extras=data["def_extras"],
23052305
type_guard=(
23062306
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
@@ -3599,3 +3599,45 @@ class Bar(Foo):
35993599

36003600
def foo(self, value: Union[int, str]) -> Union[int, str]:
36013601
return super().foo(value) # E: Call to abstract method "foo" of "Foo" with trivial body via super() is unsafe
3602+
3603+
[case testBoundMethodsAssignedInClassBody]
3604+
from typing import Callable
3605+
3606+
class A:
3607+
def f(self, x: int) -> str:
3608+
pass
3609+
@classmethod
3610+
def g(cls, x: int) -> str:
3611+
pass
3612+
@staticmethod
3613+
def h(x: int) -> str:
3614+
pass
3615+
attr: Callable[[int], str]
3616+
3617+
class C:
3618+
x1 = A.f
3619+
x2 = A.g
3620+
x3 = A().f
3621+
x4 = A().g
3622+
x5 = A.h
3623+
x6 = A().h
3624+
x7 = A().attr
3625+
3626+
reveal_type(C.x1) # N: Revealed type is "def (self: __main__.A, x: builtins.int) -> builtins.str"
3627+
reveal_type(C.x2) # N: Revealed type is "def (x: builtins.int) -> builtins.str"
3628+
reveal_type(C.x3) # N: Revealed type is "def (x: builtins.int) -> builtins.str"
3629+
reveal_type(C.x4) # N: Revealed type is "def (x: builtins.int) -> builtins.str"
3630+
reveal_type(C.x5) # N: Revealed type is "def (x: builtins.int) -> builtins.str"
3631+
reveal_type(C.x6) # N: Revealed type is "def (x: builtins.int) -> builtins.str"
3632+
reveal_type(C.x7) # N: Revealed type is "def (builtins.int) -> builtins.str"
3633+
3634+
reveal_type(C().x1) # E: Invalid self argument "C" to attribute function "x1" with type "Callable[[A, int], str]" \
3635+
# N: Revealed type is "def (x: builtins.int) -> builtins.str"
3636+
reveal_type(C().x2) # N: Revealed type is "def (x: builtins.int) -> builtins.str"
3637+
reveal_type(C().x3) # N: Revealed type is "def (x: builtins.int) -> builtins.str"
3638+
reveal_type(C().x4) # N: Revealed type is "def (x: builtins.int) -> builtins.str"
3639+
reveal_type(C().x5) # N: Revealed type is "def (x: builtins.int) -> builtins.str"
3640+
reveal_type(C().x6) # N: Revealed type is "def (x: builtins.int) -> builtins.str"
3641+
reveal_type(C().x7) # E: Invalid self argument "C" to attribute function "x7" with type "Callable[[int], str]" \
3642+
# N: Revealed type is "def () -> builtins.str"
3643+
[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)