Skip to content
Open
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
25 changes: 25 additions & 0 deletions Lib/test/test_unittest/testmock/testpatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# E-mail: fuzzyman AT voidspace DOT org DOT uk
# http://www.voidspace.org.uk/python/mock/

import functools
import os
import sys
from collections import OrderedDict
Expand Down Expand Up @@ -1076,6 +1077,30 @@ def class_method(cls, a, b=10, *, c): pass
self.assertRaises(TypeError, method, 1, 2, 3, c=4)


def test_autospec_partialmethod(self):
# autospeccing a functools.partialmethod attribute used to
# produce a NonCallableMagicMock (the attribute has no __call__ of
# its own), making the mock entirely uncallable. It should behave
# like autospeccing any other bound method, with `self` and the
# partialmethod's own pre-bound arguments excluded from the
# enforced signature.
class Foo:
def method(self, a, b, c=3):
return a, b, c
partial_method = functools.partialmethod(method, 1)

Foo().partial_method(2)

with patch.object(Foo, 'partial_method', autospec=True) as method:
self.assertNotIsInstance(method, NonCallableMagicMock)
foo = Foo()
foo.partial_method(2)
foo.partial_method(2, c=4)
self.assertRaises(TypeError, foo.partial_method)
self.assertRaises(TypeError, foo.partial_method, 2, 3, 4)
self.assertRaises(TypeError, foo.partial_method, 2, foo="lish")


def test_autospec_with_new(self):
patcher = patch('%s.function' % __name__, new=3, autospec=True)
self.assertRaises(TypeError, patcher.start)
Expand Down
23 changes: 22 additions & 1 deletion Lib/unittest/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
from dataclasses import fields, is_dataclass
from types import CodeType, ModuleType, MethodType
from unittest.util import safe_repr
from functools import wraps, partial
from functools import partialmethod, wraps, partial
from threading import RLock


Expand Down Expand Up @@ -107,6 +107,20 @@ def _get_signature_object(func, as_instance, eat_self):
eat_self = True
# Use the original decorated method to extract the correct function signature
func = func.__func__
elif isinstance(func, partialmethod):
# partialmethod always consumes `self` itself: real access binds it via the
# descriptor protocol (instance access resolves `self` before applying before
# applying the partialmethod's own bound args). When a mock stands in for the
# raw class attribute directly, there is no descriptor rebinding at all, so no
# caller ever supplies `self` either. Build the effective signature by dropping
# `self` from the wrapped function's signature and applying the partialmethod's
# own pre-bound positional / keyword arguments to what remains.
inner_sig = inspect.signature(func.func, annotation_format=Format.FORWARDREF)
params = list(inner_sig.parameters.values())[1:]
stub = lambda *args, **kwargs: None
stub.__signature__ = inner_sig.replace(parameters=params)
func = partial(stub, *func.args, **func.keywords)
eat_self = False
elif not isinstance(func, FunctionTypes):
# If we really want to model an instance of the passed type,
# __call__ should be looked up, not __init__.
Expand Down Expand Up @@ -155,6 +169,13 @@ def _callable(obj):
return True
if isinstance(obj, (staticmethod, classmethod, MethodType)):
return _callable(obj.__func__)
if isinstance(obj, partialmethod):
# partialmethod objects have no __call__ of their own, they are non-data
# descriptors that only become callable once resolved via the descriptor
# protocol (instance /class attribute access). Judge callability from the
# wrapped function instead, so autospeccing a partialmethod attribute doesn't
# produce an uncallable mock.
return _callable(obj.func)
if getattr(obj, '__call__', None) is not None:
return True
return False
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Fix :func:`unittest.mock.create_autospec` (and ``mock.patch(...,
autospec=True)``) producing a completely uncallable
:class:`~unittest.mock.NonCallableMagicMock` when the autospecced attribute
is a :class:`functools.partialmethod`. The mock is now callable and enforces
the partial method's effective signature (``self`` and the partialmethod's
own pre-bound arguments excluded).
Loading