Skip to content

Commit 20cea74

Browse files
committed
Move self argument checks to a later phase (after decorator application, if any)
1 parent a0665e1 commit 20cea74

File tree

3 files changed

+120
-41
lines changed

3 files changed

+120
-41
lines changed

mypy/checker.py

Lines changed: 60 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1369,49 +1369,19 @@ def check_func_def(
13691369
)
13701370

13711371
# Store argument types.
1372+
found_self = False
1373+
if isinstance(defn, FuncDef) and not defn.is_decorated:
1374+
found_self = self.require_correct_self_argument(typ, defn)
13721375
for i in range(len(typ.arg_types)):
13731376
arg_type = typ.arg_types[i]
1374-
if (
1375-
isinstance(defn, FuncDef)
1376-
and ref_type is not None
1377-
and i == 0
1378-
and defn.has_self_or_cls_argument
1379-
and typ.arg_kinds[0] not in [nodes.ARG_STAR, nodes.ARG_STAR2]
1380-
):
1381-
if defn.is_class or defn.name == "__new__":
1382-
ref_type = mypy.types.TypeType.make_normalized(ref_type)
1383-
if not is_same_type(arg_type, ref_type):
1384-
# This level of erasure matches the one in checkmember.check_self_arg(),
1385-
# better keep these two checks consistent.
1386-
erased = get_proper_type(erase_typevars(erase_to_bound(arg_type)))
1387-
if not is_subtype(ref_type, erased, ignore_type_params=True):
1388-
if (
1389-
isinstance(erased, Instance)
1390-
and erased.type.is_protocol
1391-
or isinstance(erased, TypeType)
1392-
and isinstance(erased.item, Instance)
1393-
and erased.item.type.is_protocol
1394-
):
1395-
# We allow the explicit self-type to be not a supertype of
1396-
# the current class if it is a protocol. For such cases
1397-
# the consistency check will be performed at call sites.
1398-
msg = None
1399-
elif typ.arg_names[i] in {"self", "cls"}:
1400-
msg = message_registry.ERASED_SELF_TYPE_NOT_SUPERTYPE.format(
1401-
erased.str_with_options(self.options),
1402-
ref_type.str_with_options(self.options),
1403-
)
1404-
else:
1405-
msg = message_registry.MISSING_OR_INVALID_SELF_TYPE
1406-
if msg:
1407-
self.fail(msg, defn)
1408-
elif isinstance(arg_type, TypeVarType):
1377+
if isinstance(arg_type, TypeVarType):
14091378
# Refuse covariant parameter type variables
14101379
# TODO: check recursively for inner type variables
14111380
if (
14121381
arg_type.variance == COVARIANT
14131382
and defn.name not in ("__init__", "__new__", "__post_init__")
14141383
and not is_private(defn.name) # private methods are not inherited
1384+
and (i != 0 or not found_self)
14151385
):
14161386
ctx: Context = arg_type
14171387
if ctx.line < 0:
@@ -1561,6 +1531,60 @@ def check_func_def(
15611531

15621532
self.binder = old_binder
15631533

1534+
def require_correct_self_argument(self, func: Type, defn: FuncDef) -> bool:
1535+
func = get_proper_type(func)
1536+
if not isinstance(func, CallableType):
1537+
return False
1538+
1539+
with self.scope.push_function(defn):
1540+
# We temporary push the definition to get the self type as
1541+
# visible from *inside* of this function/method.
1542+
ref_type: Type | None = self.scope.active_self_type()
1543+
if ref_type is None:
1544+
return False
1545+
1546+
if not defn.has_self_or_cls_argument or (
1547+
func.arg_kinds and func.arg_kinds[0] in [nodes.ARG_STAR, nodes.ARG_STAR2]
1548+
):
1549+
return False
1550+
1551+
if not func.arg_types:
1552+
self.fail(
1553+
'Method must have at least one argument. Did you forget the "self" argument?', defn
1554+
)
1555+
return False
1556+
1557+
arg_type = func.arg_types[0]
1558+
if defn.is_class or defn.name == "__new__":
1559+
ref_type = mypy.types.TypeType.make_normalized(ref_type)
1560+
if is_same_type(arg_type, ref_type):
1561+
return True
1562+
1563+
# This level of erasure matches the one in checkmember.check_self_arg(),
1564+
# better keep these two checks consistent.
1565+
erased = get_proper_type(erase_typevars(erase_to_bound(arg_type)))
1566+
if not is_subtype(ref_type, erased, ignore_type_params=True):
1567+
if (
1568+
isinstance(erased, Instance)
1569+
and erased.type.is_protocol
1570+
or isinstance(erased, TypeType)
1571+
and isinstance(erased.item, Instance)
1572+
and erased.item.type.is_protocol
1573+
):
1574+
# We allow the explicit self-type to be not a supertype of
1575+
# the current class if it is a protocol. For such cases
1576+
# the consistency check will be performed at call sites.
1577+
msg = None
1578+
elif func.arg_names[0] in {"self", "cls"}:
1579+
msg = message_registry.ERASED_SELF_TYPE_NOT_SUPERTYPE.format(
1580+
erased.str_with_options(self.options), ref_type.str_with_options(self.options)
1581+
)
1582+
else:
1583+
msg = message_registry.MISSING_OR_INVALID_SELF_TYPE
1584+
if msg:
1585+
self.fail(msg, defn)
1586+
return True
1587+
15641588
def is_var_redefined_in_outer_context(self, v: Var, after_line: int) -> bool:
15651589
"""Can the variable be assigned to at module top level or outer function?
15661590
@@ -5298,6 +5322,7 @@ def visit_decorator_inner(
52985322
)
52995323
if non_trivial_decorator:
53005324
self.check_untyped_after_decorator(sig, e.func)
5325+
self.require_correct_self_argument(sig, e.func)
53015326
sig = set_callable_name(sig, e.func)
53025327
e.var.type = sig
53035328
e.var.is_ready = True

mypy/semanal.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,12 +1072,7 @@ def prepare_method_signature(self, func: FuncDef, info: TypeInfo, has_self_type:
10721072
if func.has_self_or_cls_argument:
10731073
if func.name in ["__init_subclass__", "__class_getitem__"]:
10741074
func.is_class = True
1075-
if not func.arguments:
1076-
self.fail(
1077-
'Method must have at least one argument. Did you forget the "self" argument?',
1078-
func,
1079-
)
1080-
elif isinstance(functype, CallableType):
1075+
if func.arguments and isinstance(functype, CallableType):
10811076
self_type = get_proper_type(functype.arg_types[0])
10821077
if isinstance(self_type, AnyType):
10831078
if has_self_type:

test-data/unit/check-functions.test

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3708,3 +3708,62 @@ foo(*args) # E: Argument 1 to "foo" has incompatible type "*list[object]"; expe
37083708
kwargs: dict[str, object]
37093709
foo(**kwargs) # E: Argument 1 to "foo" has incompatible type "**dict[str, object]"; expected "P"
37103710
[builtins fixtures/dict.pyi]
3711+
3712+
[case testMethodDecoratorSelfChecks]
3713+
from typing import Callable, ParamSpec, TypeVar
3714+
3715+
T = TypeVar("T")
3716+
P = ParamSpec("P")
3717+
3718+
def to_number_1(fn: Callable[[], int]) -> int:
3719+
return 0
3720+
3721+
def to_number_2(fn: Callable[[int], int]) -> int:
3722+
return 0
3723+
3724+
def to_same_callable(fn: Callable[P, T]) -> Callable[P, T]:
3725+
return fn
3726+
3727+
class A:
3728+
@to_number_1
3729+
def fn1() -> int:
3730+
return 0
3731+
3732+
@to_number_1 # E: Argument 1 to "to_number_1" has incompatible type "Callable[[int], int]"; expected "Callable[[], int]"
3733+
def fn2(_x: int) -> int:
3734+
return 0
3735+
3736+
@to_number_2 # E: Argument 1 to "to_number_2" has incompatible type "Callable[[], int]"; expected "Callable[[int], int]"
3737+
def fn3() -> int:
3738+
return 0
3739+
3740+
@to_number_2
3741+
def fn4(_x: int) -> int:
3742+
return 0
3743+
3744+
@to_number_2 # E: Argument 1 to "to_number_2" has incompatible type "Callable[[str], int]"; expected "Callable[[int], int]"
3745+
def fn5(_x: str) -> int:
3746+
return 0
3747+
3748+
@to_same_callable
3749+
def g1() -> None: ... # E: Method must have at least one argument. Did you forget the "self" argument?
3750+
3751+
@to_same_callable
3752+
def g2(x: int) -> None: ... # E: Self argument missing for a non-static method (or an invalid type for self)
3753+
3754+
@to_same_callable
3755+
def g3(self: int) -> None: ... # E: The erased type of self "builtins.int" is not a supertype of its class "__main__.A"
3756+
3757+
reveal_type(A().fn1) # N: Revealed type is "builtins.int"
3758+
reveal_type(A().fn2) # N: Revealed type is "builtins.int"
3759+
reveal_type(A().fn3) # N: Revealed type is "builtins.int"
3760+
reveal_type(A().fn4) # N: Revealed type is "builtins.int"
3761+
reveal_type(A().fn5) # N: Revealed type is "builtins.int"
3762+
3763+
reveal_type(A().g1) # E: Attribute function "g1" with type "Callable[[], None]" does not accept self argument \
3764+
# N: Revealed type is "def ()"
3765+
reveal_type(A().g2) # E: Invalid self argument "A" to attribute function "g2" with type "Callable[[int], None]" \
3766+
# N: Revealed type is "def ()"
3767+
reveal_type(A().g3) # E: Invalid self argument "A" to attribute function "g3" with type "Callable[[int], None]" \
3768+
# N: Revealed type is "def ()"
3769+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)