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 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
104 changes: 69 additions & 35 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1374,49 +1374,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 @@ -1566,6 +1536,69 @@ 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

# Do not report errors for untyped methods in classes nested in untyped funcs.
if not (
self.options.check_untyped_defs
or len(self.dynamic_funcs) < 2
or not self.dynamic_funcs[-2]
or not defn.is_dynamic()
):
return bool(func.arg_types)

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 @@ -5303,6 +5336,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
230 changes: 229 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,231 @@ 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 testMethodSelfArgumentChecksConcatenate]
from typing import Callable, ParamSpec, TypeVar
from typing_extensions import Concatenate

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

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

def remove_first(fn: Callable[Concatenate[T, P], R]) -> Callable[P, R]:
...

def add_correct_first(fn: Callable[P, R]) -> Callable[Concatenate["C", P], R]:
...

def add_wrong_first(fn: Callable[P, R]) -> Callable[Concatenate[int, P], R]:
...

class A:
@to_same_callable # E: Argument 1 to "to_same_callable" has incompatible type "Callable[[], int]"; expected "Callable[[T], int]"
def fn1() -> int:
return 0

@to_same_callable
def fn2(_x: int) -> int: # E: Self argument missing for a non-static method (or an invalid type for self)
return 0

@to_same_callable
def fn3(self, _x: int) -> int:
return 0

reveal_type(A().fn1) # N: Revealed type is "def () -> builtins.int"
reveal_type(A().fn2) # E: Invalid self argument "A" to attribute function "fn2" with type "Callable[[int], int]" \
# N: Revealed type is "def () -> builtins.int"
reveal_type(A().fn3) # N: Revealed type is "def (_x: builtins.int) -> builtins.int"

class B:
@remove_first # E: Argument 1 to "remove_first" has incompatible type "Callable[[], int]"; expected "Callable[[T], int]"
def fn1() -> int: # E: Method must have at least one argument. Did you forget the "self" argument?
return 0

@remove_first
def fn2(_x: int) -> int: # E: Method must have at least one argument. Did you forget the "self" argument?
return 0

@remove_first
def fn3(self, _x: int) -> int: # E: Self argument missing for a non-static method (or an invalid type for self)
return 0

@remove_first
def fn4(self, new_self: 'B') -> int:
return 0

reveal_type(B().fn1) # E: Attribute function "fn1" with type "Callable[[], int]" does not accept self argument \
# N: Revealed type is "def () -> builtins.int"
reveal_type(B().fn2) # E: Attribute function "fn2" with type "Callable[[], int]" does not accept self argument \
# N: Revealed type is "def () -> builtins.int"
reveal_type(B().fn3) # E: Invalid self argument "B" to attribute function "fn3" with type "Callable[[int], int]" \
# N: Revealed type is "def () -> builtins.int"
reveal_type(B().fn4) # N: Revealed type is "def () -> builtins.int"

class C:
@add_correct_first
def fn1() -> int:
return 0

@add_correct_first
def fn2(_x: int) -> int:
return 0

@add_correct_first
def fn3(self, _x: int) -> int:
return 0

reveal_type(C().fn1) # N: Revealed type is "def () -> builtins.int"
reveal_type(C().fn2) # N: Revealed type is "def (_x: builtins.int) -> builtins.int"
reveal_type(C().fn3) # N: Revealed type is "def (self: __main__.C, _x: builtins.int) -> builtins.int"

class D:
@add_wrong_first
def fn1() -> int: # E: Self argument missing for a non-static method (or an invalid type for self)
return 0

@add_wrong_first
def fn2(_x: int) -> int: # E: Self argument missing for a non-static method (or an invalid type for self)
return 0

@add_wrong_first
def fn3(self, _x: int) -> int: # E: Self argument missing for a non-static method (or an invalid type for self)
return 0

reveal_type(D().fn1) # E: Invalid self argument "D" to attribute function "fn1" with type "Callable[[int], int]" \
# N: Revealed type is "def () -> builtins.int"
reveal_type(D().fn2) # E: Invalid self argument "D" to attribute function "fn2" with type "Callable[[int, int], int]" \
# N: Revealed type is "def (_x: builtins.int) -> builtins.int"
reveal_type(D().fn3) # E: Invalid self argument "D" to attribute function "fn3" with type "Callable[[int, D, int], int]" \
# N: Revealed type is "def (self: __main__.D, _x: builtins.int) -> builtins.int"
[builtins fixtures/tuple.pyi]

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

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

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

def unchecked():
class Bad:
def fn() -> None: ... # E: Method must have at least one argument. Did you forget the "self" argument?
def fn2(x: int) -> None: ... # E: Self argument missing for a non-static method (or an invalid type for self)

# TODO: would be nice to make this error, but now we see the func
# being decorated as Any, not as a callable
@to_same_callable
def gaaa() -> None: ...
@to_same_callable
def gaaa2(x: int) -> None: ...

class Ok:
def fn(): ...
def fn2(x): ...

@to_same_callable
def g(): ...
@to_same_callable
def g2(x): ...

def checked() -> None:
class Bad:
def fn() -> None: ... # E: Method must have at least one argument. Did you forget the "self" argument?
def fn2(x: int) -> None: ... # E: Self argument missing for a non-static method (or an invalid type for self)

@to_same_callable
def g() -> 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)

class AlsoBad:
def fn(): ... # E: Method must have at least one argument. Did you forget the "self" argument?
def fn2(x): ...

@to_same_callable
def g(): ... # E: Method must have at least one argument. Did you forget the "self" argument?
@to_same_callable
def g2(x): ...

class Ok:
def fn(): ... # E: Method must have at least one argument. Did you forget the "self" argument?
def fn2(x): ...

@to_same_callable
def g(): ... # E: Method must have at least one argument. Did you forget the "self" argument?
@to_same_callable
def g2(x): ...
[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
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy