Skip to content

Fix handling of non-method callable attribute #3227

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

Closed
wants to merge 17 commits into from
Closed
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
53 changes: 16 additions & 37 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -807,51 +807,30 @@ def is_trivial_body(self, block: Block) -> bool:
(isinstance(stmt, ExpressionStmt) and
isinstance(stmt.expr, EllipsisExpr)))

def check_reverse_op_method(self, defn: FuncItem, typ: CallableType,
method: str) -> None:
def check_reverse_op_method(self, defn: FuncItem, typ: CallableType, method: str) -> None:
"""Check a reverse operator method such as __radd__."""

# This used to check for some very obscure scenario. It now
# just decides whether it's worth calling
# check_overlapping_op_methods().
# Decides whether it's worth calling check_overlapping_op_methods().

if method in ('__eq__', '__ne__'):
# These are defined for all objects => can't cause trouble.
return
if len(typ.arg_types) != 2:
# Plausibly the method could have too few arguments, which would result
# in an error elsewhere.
return

# With 'Any' or 'object' return type we are happy, since any possible
# return value is valid.
ret_type = typ.ret_type
if isinstance(ret_type, AnyType):
other_method = nodes.normal_from_reverse_op[method]
arg_type = typ.arg_types[1]
# TODO: arg_type = arg_type.fallback, e.g. for TupleType
if not (isinstance(arg_type, (Instance, UnionType))
and arg_type.has_readable_member(other_method)):
return
if isinstance(ret_type, Instance):
if ret_type.type.fullname() == 'builtins.object':
return
# Plausibly the method could have too few arguments, which would result
# in an error elsewhere.
if len(typ.arg_types) <= 2:
# TODO check self argument kind

# Check for the issue described above.
arg_type = typ.arg_types[1]
other_method = nodes.normal_from_reverse_op[method]
if isinstance(arg_type, Instance):
if not arg_type.type.has_readable_member(other_method):
return
elif isinstance(arg_type, AnyType):
return
elif isinstance(arg_type, UnionType):
if not arg_type.has_readable_member(other_method):
return
else:
return

typ2 = self.expr_checker.analyze_external_member_access(
other_method, arg_type, defn)
self.check_overlapping_op_methods(
typ, method, defn.info,
typ2, other_method, cast(Instance, arg_type),
defn)
typ2 = self.expr_checker.analyze_external_member_access(other_method, arg_type, defn)
for t in union_items(arg_type):
assert isinstance(t, Instance)
self.check_overlapping_op_methods(typ, method, defn.info,
typ2, other_method, t, defn)

def check_overlapping_op_methods(self,
reverse_type: CallableType,
Expand Down
40 changes: 16 additions & 24 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from mypy.plugin import Plugin, AttributeContext
from mypy import messages
from mypy import subtypes
from mypy import meet
MYPY = False
if MYPY: # import for forward declaration only
import mypy.checker
Expand Down Expand Up @@ -314,7 +315,9 @@ def analyze_var(name: str, var: Var, itype: Instance, info: TypeInfo, node: Cont
# methods: the former to the instance, the latter to the
# class.
functype = t
check_method_type(functype, itype, var.is_classmethod, node, msg)
# Use meet to simulate dispatch - e.g. reduce Union[A, B] to A on dispatch to A
dispatched_type = meet.meet_types(original_type, itype)
check_self_arg(functype, dispatched_type, var.is_classmethod, node, name, msg)
signature = bind_self(functype, original_type, var.is_classmethod)
if var.is_property:
# A property cannot have an overloaded type => the cast
Expand Down Expand Up @@ -370,33 +373,22 @@ def lookup_member_var_or_accessor(info: TypeInfo, name: str,
return None


def check_method_type(functype: FunctionLike, itype: Instance, is_classmethod: bool,
context: Context, msg: MessageBuilder) -> None:
def check_self_arg(functype: FunctionLike, original_type: Type, is_classmethod: bool,
context: Context, name: str, msg: MessageBuilder) -> None:
"""Check that the the most precise type of the self argument is compatible
with the declared type of each of the overloads.
"""
for item in functype.items():
if not item.arg_types or item.arg_kinds[0] not in (ARG_POS, ARG_STAR):
# No positional first (self) argument (*args is okay).
msg.invalid_method_type(item, context)
elif not is_classmethod:
# Check that self argument has type 'Any' or valid instance type.
selfarg = item.arg_types[0]
# If this is a method of a tuple class, correct for the fact that
# we passed to typ.fallback in analyze_member_access. See #1432.
if isinstance(selfarg, TupleType):
selfarg = selfarg.fallback
if not subtypes.is_subtype(selfarg, itype):
msg.invalid_method_type(item, context)
msg.fail('Attribute function with type %s does not accept self argument'
% msg.format(item), context)
else:
# Check that cls argument has type 'Any' or valid class type.
# (This is sufficient for the current treatment of @classmethod,
# but probably needs to be revisited when we implement Type[C]
# or advanced variants of it like Type[<args>, C].)
clsarg = item.arg_types[0]
if isinstance(clsarg, CallableType) and clsarg.is_type_obj():
if not subtypes.is_equivalent(clsarg.ret_type, itype):
msg.invalid_class_method_type(item, context)
else:
if not subtypes.is_equivalent(clsarg, AnyType(TypeOfAny.special_form)):
msg.invalid_class_method_type(item, context)
selfarg = item.arg_types[0]
if is_classmethod:
original_type = TypeType.make_normalized(original_type)
if not subtypes.is_subtype(original_type, erase_to_bound(selfarg)):
msg.invalid_method_type(name, original_type, item, is_classmethod, context)


def analyze_class_attribute_access(itype: Instance,
Expand Down
10 changes: 5 additions & 5 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -856,11 +856,11 @@ def cannot_determine_type(self, name: str, context: Context) -> None:
def cannot_determine_type_in_base(self, name: str, base: str, context: Context) -> None:
self.fail("Cannot determine type of '%s' in base class '%s'" % (name, base), context)

def invalid_method_type(self, sig: CallableType, context: Context) -> None:
self.fail('Invalid method type', context)

def invalid_class_method_type(self, sig: CallableType, context: Context) -> None:
self.fail('Invalid class method type', context)
def invalid_method_type(self, name: str, arg: Type, sig: CallableType, is_classmethod: bool,
context: Context) -> None:
kind = 'class attribute function' if is_classmethod else 'attribute function'
self.fail('Invalid self argument %s to %s "%s" with type %s'
% (self.format(arg), kind, name, self.format(sig)), context)

def incompatible_conditional_function_def(self, defn: FuncDef) -> None:
self.fail('All conditional function variants must have identical '
Expand Down
3 changes: 3 additions & 0 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,9 @@ def deserialize(cls, data: Union[JsonDict, str]) -> 'Instance':
def copy_modified(self, *, args: List[Type]) -> 'Instance':
return Instance(self.type, args, self.line, self.column, self.erased)

def has_readable_member(self, name: str) -> bool:
return self.type.has_readable_member(name)


class TypeVarType(Type):
"""A type variable type.
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -2105,7 +2105,7 @@ class B:
a = A
bad = lambda: 42

B().bad() # E: Invalid method type
B().bad() # E: Attribute function with type "Callable[[], int]" does not accept self argument
reveal_type(B.a) # E: Revealed type is 'def () -> __main__.A'
reveal_type(B().a) # E: Revealed type is 'def () -> __main__.A'
reveal_type(B().a()) # E: Revealed type is '__main__.A'
Expand Down
6 changes: 3 additions & 3 deletions test-data/unit/check-functions.test
Original file line number Diff line number Diff line change
Expand Up @@ -507,8 +507,8 @@ class A:
f = x # type: Callable[[], None]
g = x # type: Callable[[B], None]
a = None # type: A
a.f() # E: Invalid method type
a.g() # E: Invalid method type
a.f() # E: Attribute function with type "Callable[[], None]" does not accept self argument
a.g() # E: Invalid self argument "A" to attribute function "g" with type "Callable[[B], None]"

[case testMethodWithDynamicallyTypedMethodAsDataAttribute]
from typing import Any, Callable
Expand Down Expand Up @@ -568,7 +568,7 @@ class A(Generic[t]):
ab = None # type: A[B]
ac = None # type: A[C]
ab.f()
ac.f() # E: Invalid method type
ac.f() # E: Invalid self argument "A[C]" to attribute function "f" with type "Callable[[A[B]], None]"

[case testPartiallyTypedSelfInMethodDataAttribute]
from typing import Any, TypeVar, Generic, Callable
Expand Down
31 changes: 27 additions & 4 deletions test-data/unit/check-selftype.test
Original file line number Diff line number Diff line change
Expand Up @@ -354,16 +354,21 @@ class E:
[case testSelfTypeProperty]
from typing import TypeVar

T = TypeVar('T', bound='A')
Q = TypeVar('Q')
T = TypeVar('T', bound='X')

class A:
class X:
@property
def member(self: T) -> T:
pass
def __members__(self: Q) -> Q: return self

class A(X):
@property
def member(self: T) -> T: return self

class B(A):
pass

reveal_type(X().__members__) # E: Revealed type is '__main__.X*'
reveal_type(A().member) # E: Revealed type is '__main__.A*'
reveal_type(B().member) # E: Revealed type is '__main__.B*'

Expand All @@ -376,3 +381,21 @@ class A:
# def g(self: None) -> None: ... see in check-python2.test
[out]
main:3: error: Self argument missing for a non-static method (or an invalid type for self)

[case testUnionPropertyField]
from typing import Union

class A:
x: int

class B:
@property
def x(self) -> int: return 1

class C:
@property
def x(self) -> int: return 1

ab: Union[A, B, C]
reveal_type(ab.x) # E: Revealed type is 'builtins.int'
[builtins fixtures/property.pyi]
2 changes: 1 addition & 1 deletion typeshed
Submodule typeshed updated 53 files
+0 −6 CONTRIBUTING.md
+2 −2 stdlib/2/ConfigParser.pyi
+21 −21 stdlib/2/__builtin__.pyi
+1 −1 stdlib/2/_io.pyi
+0 −1 stdlib/2/ast.pyi
+80 −48 stdlib/2/exceptions.pyi
+1 −1 stdlib/2/functools.pyi
+2 −3 stdlib/2/heapq.pyi
+2 −2 stdlib/2/itertools.pyi
+0 −2 stdlib/2/os/__init__.pyi
+1 −1 stdlib/2/symbol.pyi
+0 −2 stdlib/2and3/argparse.pyi
+0 −23 stdlib/2and3/chunk.pyi
+0 −17 stdlib/2and3/codeop.pyi
+5 −5 stdlib/2and3/ftplib.pyi
+1 −1 stdlib/2and3/logging/handlers.pyi
+1 −1 stdlib/2and3/socket.pyi
+1 −11 stdlib/2and3/traceback.pyi
+6 −9 stdlib/3.4/asyncio/streams.pyi
+4 −24 stdlib/3.4/asyncio/tasks.pyi
+1 −6 stdlib/3.4/enum.pyi
+29 −30 stdlib/3/builtins.pyi
+7 −32 stdlib/3/configparser.pyi
+2 −2 stdlib/3/fcntl.pyi
+1 −1 stdlib/3/functools.pyi
+2 −2 stdlib/3/heapq.pyi
+2 −4 stdlib/3/io.pyi
+57 −21 stdlib/3/multiprocessing/__init__.pyi
+3 −18 stdlib/3/multiprocessing/context.pyi
+4 −3 stdlib/3/multiprocessing/managers.pyi
+8 −21 stdlib/3/multiprocessing/pool.pyi
+6 −18 stdlib/3/os/__init__.pyi
+1 −4 stdlib/3/os/path.pyi
+2 −2 stdlib/3/queue.pyi
+1 −11 stdlib/3/resource.pyi
+4 −11 stdlib/3/shlex.pyi
+29 −40 stdlib/3/smtplib.pyi
+0 −22 stdlib/3/ssl.pyi
+4 −3 stdlib/3/subprocess.pyi
+26 −24 stdlib/3/sys.pyi
+1 −1 stdlib/3/unittest/__init__.pyi
+1 −1 tests/mypy_selftest.py
+1 −1 third_party/2/six/__init__.pyi
+1 −1 third_party/2and3/jinja2/__init__.pyi
+1 −1 third_party/2and3/jinja2/environment.pyi
+1 −2 third_party/2and3/jinja2/utils.pyi
+2 −2 third_party/2and3/pynamodb/attributes.pyi
+1 −1 third_party/2and3/requests/adapters.pyi
+1 −3 third_party/2and3/requests/auth.pyi
+1 −1 third_party/2and3/requests/models.pyi
+0 −7 third_party/2and3/typing_extensions.pyi
+2 −8 third_party/3/lxml/etree.pyi
+1 −1 third_party/3/six/__init__.pyi