For example:
from typing import Any, Optional
class Foo(object):
def __init__(self):
# type: () -> None
self.x = 0
def get_x(self, a=None, b=None):
# type: (Optional[int], Optional[int]) -> int
return self.x + (a or 0) + (b or 0)
def get_x_patch(self, *args, **kwargs):
# type: (Foo, *Any, **Any) -> int
return 42
Foo.get_x = get_x_patch
Results:
test_method_assignment.py:19: error: Cannot assign to a method
This assignment should be legal as any call to get_x will be able to call get_x_patch. It looks like https://github.com/python/mypy/commit/3ce8d6a5c72f49e120b354619ab44523fee4de0e explicitly disallowed all method assignments, but there's not a ton of context behind it.
I know monkeypatching is generally frowned upon, but is unfortunately a very popular part of Python. I can always mark those lines as ignored, but I'd rather be able to test that the patch is compatible with the underlying method with mypy.
_I swear_, this is a duplicate, but I can't find the issue # yet...
@kirbyfan64 Yeah...I poked around and couldn't find anything. Happy to close this if it is!
I think @sixolet might be interesting in this...
The mypy callable type representation isn't expressive enough to to check assignments to methods precisely. In particular, at least bound methods and unbound function objects should be treated differently. Consider this example:
class A:
def f(self): pass
def g(self): pass
def h(self): pass
A.f = h # rvalue has one argument (unbound)
A().f() # okay
A.f = A().g # rvalue has no arguments (bound)
A().f() # okay
When we have value with an annotated callable type, such as Callable[[A], None], mypy can't decide whether this is a bound or unbound function method/function. Static methods and class methods might complicate this further.
I'm pretty sure this is already broken in other contexts, but we may want to resolve this eventually. Some random ideas:
Callable type, the value of the flag would be unknown and these couldn't be used for monkey patching (unless the argument types are ...).Callable types. Either assume that they are unbound, or optimistically assume that they have a compatible 'unboundedness' for any particular context. This is unsafe. This is basically how things work right now, though perhaps not consistently.Callable), bound (for example, BoundMethod) and unbound (for example, UnboundCallable).Option (3) doesn't seem worth the added complexity, to be honest, as it's always possible to fall back to Callable[..., X].
So is the problem that when we see
class C:
f = None # type: Callable[[int, int], int]
we don't know whether that defines an instance variable or a class variable? Maybe we can use ClassVar (introduced by PEP 526 into the typing module)? E.g.
class C:
f = None # type: ClassVar[Callable[[int, int], int]]
and if ClassVar is not used assume f refers to an instance variable.
Of course initializations inside __init__ are unambiguous. And so are method definitions (with or without @staticmethod or @classmethod).
I do think mypy ought to be fully aware of bound and unbound methods.
But perhaps the original problem is due to something else? The immediate problem seems to be that we don't try to match *args, **kwds against a=None, b=None?
Isn't this a duplicate of https://github.com/python/mypy/issues/708?
But maybe it makes sense to keep this open, since this issue contains some additional discussion.
Not really -- IIUC this seems about monkey-patching a class, whereas #708 is about assigning to function attributes.
A case where I keep running into that issue is when writing unit tests and trying to replace methods with MagicMock().
Typically, class Foo is defined and tested somewhere and class FooBar uses (an instance of) Foo, but in order to unit test FooBar I don't really need/want to make actual calls to Foo methods (which can either take a long time to compute, or require some setup (eg, networking) that isn't here for unit test, …) So, I heavily Mock() the methods which allow to test that the correct calls are issued and thus test FooBar.
It does feel bad to add a bunch a # type: ignore on all these mocks :-(
I prefer setattr over using # type: ignore. Mypy won't complain about it. This is something we could discuss in the common issues section in the docs.
What's the state of this (about monkey patching a method)? It seems like it needed discussion, has that happened offline?
The has been no progress recently. The workarounds discussed above (setattr or # type: ignore) are still the recommended ways to deal with this.
Cool, thanks for the update. 5:)
I think that I am running into this. The code that causes the mypy error is FileDownloader.download = classmethod(lambda a, filename: open(f'tests/fixtures/{filename}', 'rb'))
The error is error: Cannot assign to a method
version is mypy==0.620
Hi, any progress on this?
In my case I'm not even monkey-patching (at least, I don't feel like it is), I'm trying to take a function as a parameter of __init__ and use it as a wrapper. Like this (note simplified example, so it might not make entire sense):
Adapter = Callable[[S_co], T_contra]
class AdaptingRunner():
adapter: Adapter
runner: Runner[T_contra]
def __init__(self, adapter: Adapter, runner: Runner[T_contra]):
self.adapter = adapter # Error here
self.runner = runner
def run(self, obj: S_co):
return self.runner.run(self.adapter(ob))
If I remove adapter: Adapter, everything is fine, but if I declare it, then I get the referenced error.
Most helpful comment
Hi, any progress on this?