Skip to content

Move self argument checks to a later phase - after decorator application, if any #19490

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 4 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
95 changes: 60 additions & 35 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1369,49 +1369,19 @@ def check_func_def(
)

# Store argument types.
found_self = False
if isinstance(defn, FuncDef) and not defn.is_decorated:
found_self = self.require_correct_self_argument(typ, defn)
for i in range(len(typ.arg_types)):
arg_type = typ.arg_types[i]
if (
isinstance(defn, FuncDef)
and ref_type is not None
and i == 0
and defn.has_self_or_cls_argument
and typ.arg_kinds[0] not in [nodes.ARG_STAR, nodes.ARG_STAR2]
):
if defn.is_class or defn.name == "__new__":
ref_type = mypy.types.TypeType.make_normalized(ref_type)
if not is_same_type(arg_type, ref_type):
# This level of erasure matches the one in checkmember.check_self_arg(),
# better keep these two checks consistent.
erased = get_proper_type(erase_typevars(erase_to_bound(arg_type)))
if not is_subtype(ref_type, erased, ignore_type_params=True):
if (
isinstance(erased, Instance)
and erased.type.is_protocol
or isinstance(erased, TypeType)
and isinstance(erased.item, Instance)
and erased.item.type.is_protocol
):
# We allow the explicit self-type to be not a supertype of
# the current class if it is a protocol. For such cases
# the consistency check will be performed at call sites.
msg = None
elif typ.arg_names[i] in {"self", "cls"}:
msg = message_registry.ERASED_SELF_TYPE_NOT_SUPERTYPE.format(
erased.str_with_options(self.options),
ref_type.str_with_options(self.options),
)
else:
msg = message_registry.MISSING_OR_INVALID_SELF_TYPE
if msg:
self.fail(msg, defn)
elif isinstance(arg_type, TypeVarType):
if isinstance(arg_type, TypeVarType):
# Refuse covariant parameter type variables
# TODO: check recursively for inner type variables
if (
arg_type.variance == COVARIANT
and defn.name not in ("__init__", "__new__", "__post_init__")
and not is_private(defn.name) # private methods are not inherited
and (i != 0 or not found_self)
):
ctx: Context = arg_type
if ctx.line < 0:
Expand Down Expand Up @@ -1561,6 +1531,60 @@ def check_func_def(

self.binder = old_binder

def require_correct_self_argument(self, func: Type, defn: FuncDef) -> bool:
func = get_proper_type(func)
if not isinstance(func, CallableType):
return False

with self.scope.push_function(defn):
# We temporary push the definition to get the self type as
# visible from *inside* of this function/method.
ref_type: Type | None = self.scope.active_self_type()
if ref_type is None:
return False

if not defn.has_self_or_cls_argument or (
func.arg_kinds and func.arg_kinds[0] in [nodes.ARG_STAR, nodes.ARG_STAR2]
):
return False

if not func.arg_types:
self.fail(
'Method must have at least one argument. Did you forget the "self" argument?', defn
)
return False

arg_type = func.arg_types[0]
if defn.is_class or defn.name == "__new__":
ref_type = mypy.types.TypeType.make_normalized(ref_type)
if is_same_type(arg_type, ref_type):
return True

# This level of erasure matches the one in checkmember.check_self_arg(),
# better keep these two checks consistent.
erased = get_proper_type(erase_typevars(erase_to_bound(arg_type)))
if not is_subtype(ref_type, erased, ignore_type_params=True):
if (
isinstance(erased, Instance)
and erased.type.is_protocol
or isinstance(erased, TypeType)
and isinstance(erased.item, Instance)
and erased.item.type.is_protocol
):
# We allow the explicit self-type to be not a supertype of
# the current class if it is a protocol. For such cases
# the consistency check will be performed at call sites.
msg = None
elif func.arg_names[0] in {"self", "cls"}:
msg = message_registry.ERASED_SELF_TYPE_NOT_SUPERTYPE.format(
erased.str_with_options(self.options), ref_type.str_with_options(self.options)
)
else:
msg = message_registry.MISSING_OR_INVALID_SELF_TYPE
if msg:
self.fail(msg, defn)
return True

def is_var_redefined_in_outer_context(self, v: Var, after_line: int) -> bool:
"""Can the variable be assigned to at module top level or outer function?

Expand Down Expand Up @@ -5298,6 +5322,7 @@ def visit_decorator_inner(
)
if non_trivial_decorator:
self.check_untyped_after_decorator(sig, e.func)
self.require_correct_self_argument(sig, e.func)
sig = set_callable_name(sig, e.func)
e.var.type = sig
e.var.is_ready = True
Expand Down
7 changes: 1 addition & 6 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1072,12 +1072,7 @@ def prepare_method_signature(self, func: FuncDef, info: TypeInfo, has_self_type:
if func.has_self_or_cls_argument:
if func.name in ["__init_subclass__", "__class_getitem__"]:
func.is_class = True
if not func.arguments:
self.fail(
'Method must have at least one argument. Did you forget the "self" argument?',
func,
)
elif isinstance(functype, CallableType):
if func.arguments and isinstance(functype, CallableType):
self_type = get_proper_type(functype.arg_types[0])
if isinstance(self_type, AnyType):
if has_self_type:
Expand Down
73 changes: 72 additions & 1 deletion test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -3261,7 +3261,10 @@ b.bad = 'a' # E: Incompatible types in assignment (expression has type "str", v
from typing import Any

class Test:
def __setattr__() -> None: ... # E: Method must have at least one argument. Did you forget the "self" argument? # E: Invalid signature "Callable[[], None]" for "__setattr__"
def __setattr__() -> None: ... \
# E: Invalid signature "Callable[[], None]" for "__setattr__" \
# E: Method must have at least one argument. Did you forget the "self" argument?

t = Test()
t.crash = 'test' # E: Attribute function "__setattr__" with type "Callable[[], None]" does not accept self argument \
# E: "Test" has no attribute "crash"
Expand Down Expand Up @@ -7742,6 +7745,74 @@ class Foo:
def bad(): # E: Method must have at least one argument. Did you forget the "self" argument?
self.x = 0 # E: Name "self" is not defined

[case testMethodSelfArgumentChecks]
from typing import Callable, ParamSpec, TypeVar

T = TypeVar("T")
P = ParamSpec("P")

def to_number_1(fn: Callable[[], int]) -> int:
return 0

def to_number_2(fn: Callable[[int], int]) -> int:
return 0

def to_same_callable(fn: Callable[P, T]) -> Callable[P, T]:
return fn

class A:
def undecorated() -> None: ... # E: Method must have at least one argument. Did you forget the "self" argument?

def undecorated_not_self(x: int) -> None: ... # E: Self argument missing for a non-static method (or an invalid type for self)

def undecorated_not_self_2(self: int) -> None: ... # E: The erased type of self "builtins.int" is not a supertype of its class "__main__.A"

@to_number_1
def fn1() -> int:
return 0

@to_number_1 # E: Argument 1 to "to_number_1" has incompatible type "Callable[[int], int]"; expected "Callable[[], int]"
def fn2(_x: int) -> int:
return 0

@to_number_2 # E: Argument 1 to "to_number_2" has incompatible type "Callable[[], int]"; expected "Callable[[int], int]"
def fn3() -> int:
return 0

@to_number_2
def fn4(_x: int) -> int:
return 0

@to_number_2 # E: Argument 1 to "to_number_2" has incompatible type "Callable[[str], int]"; expected "Callable[[int], int]"
def fn5(_x: str) -> int:
return 0

@to_same_callable
def g1() -> None: ... # E: Method must have at least one argument. Did you forget the "self" argument?

@to_same_callable
def g2(x: int) -> None: ... # E: Self argument missing for a non-static method (or an invalid type for self)

@to_same_callable
def g3(self: int) -> None: ... # E: The erased type of self "builtins.int" is not a supertype of its class "__main__.A"

reveal_type(A().fn1) # N: Revealed type is "builtins.int"
reveal_type(A().fn2) # N: Revealed type is "builtins.int"
reveal_type(A().fn3) # N: Revealed type is "builtins.int"
reveal_type(A().fn4) # N: Revealed type is "builtins.int"
reveal_type(A().fn5) # N: Revealed type is "builtins.int"

reveal_type(A().g1) # E: Attribute function "g1" with type "Callable[[], None]" does not accept self argument \
# N: Revealed type is "def ()"
reveal_type(A().g2) # E: Invalid self argument "A" to attribute function "g2" with type "Callable[[int], None]" \
# N: Revealed type is "def ()"
reveal_type(A().g3) # E: Invalid self argument "A" to attribute function "g3" with type "Callable[[int], None]" \
# N: Revealed type is "def ()"



[builtins fixtures/tuple.pyi]

[case testTypeAfterAttributeAccessWithDisallowAnyExpr]
# flags: --disallow-any-expr

Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/fine-grained.test
Original file line number Diff line number Diff line change
Expand Up @@ -1720,7 +1720,7 @@ from typing import Iterator, Callable, List, Optional
from a import f
import a

def dec(f: Callable[['A'], Optional[Iterator[int]]]) -> Callable[[int], int]: pass
def dec(f: Callable[['A'], Optional[Iterator[int]]]) -> Callable[['A', int], int]: pass

class A:
@dec
Expand Down
16 changes: 0 additions & 16 deletions test-data/unit/semanal-errors.test
Original file line number Diff line number Diff line change
Expand Up @@ -537,13 +537,6 @@ def f(y: t): x = t
main:4: error: "t" is a type variable and only valid in type context
main:5: error: "t" is a type variable and only valid in type context

[case testMissingSelf]
import typing
class A:
def f(): pass
[out]
main:3: error: Method must have at least one argument. Did you forget the "self" argument?

[case testInvalidBaseClass]
import typing
class A(B): pass
Expand All @@ -558,15 +551,6 @@ def f() -> None: super().y
main:2: error: "super" used outside class
main:3: error: "super" used outside class

[case testMissingSelfInMethod]
import typing
class A:
def f() -> None: pass
def g(): pass
[out]
main:3: error: Method must have at least one argument. Did you forget the "self" argument?
main:4: error: Method must have at least one argument. Did you forget the "self" argument?

[case testMultipleMethodDefinition]
import typing
class A:
Expand Down