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
40 changes: 40 additions & 0 deletions Lib/test/test_unittest/testmock/testhelpers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import functools
import inspect
import time
import types
Expand Down Expand Up @@ -954,6 +955,45 @@ def __getattr__(self, attribute):
self.assertNotHasAttr(autospec, '__name__')


def test_autospec_partial_function_signature_enforced(self):
# A mock created from a functools.partial object enforces the
# partial's effective, post-partial-application signature: "method"
# is already bound to "POST", so only "url" and "payload" remain.
def send_request(method, url, payload=None):
return method, url, payload

send_post_request = partial(send_request, "POST")
send_post_request("https://example.com")
send_post_request("https://example.com", payload="data")

mocked = create_autospec(send_post_request)
mocked("https://example.com")
mocked("https://example.com", payload="data")
self.assertRaises(TypeError, mocked)
self.assertRaises(TypeError, mocked, "a", "b", "c")
self.assertRaises(TypeError, mocked, "https://example.com", foo="lish")


def test_autospec_partialmethod_self_skipped(self):
# _must_skip() didn't recognize functools.partialmethod class
# attributes, so `self` was never dropped from the enforced
# signature when autospeccing a whole class.
class Foo:
def method(self, a, b, c=3):
return a, b, c
partial_method = functools.partialmethod(method, 1)

real = Foo()
real.partial_method(2, 3)

mocked = create_autospec(Foo)
instance = mocked()
instance.partial_method(2)
instance.partial_method(2, c=4)
self.assertRaises(TypeError, instance.partial_method)
self.assertRaises(TypeError, instance.partial_method, 2, 3, 4)


def test_autospec_signature_staticmethod(self):
class Foo:
@staticmethod
Expand Down
15 changes: 13 additions & 2 deletions 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,14 @@ 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, partial):
# inspect.signature() already knows how to compute the effective,
# post-partial-application signature of a functools.partial object
# directly. Don't fall through to the generic func.__call__ lookup
# below: partial.__call__ is a C slot wrapper whose own signature is
# just the unenforceable "(*args, **kwargs)" of the functools.partial
# constructor, not of the wrapped callable.
pass
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 @@ -2917,9 +2925,12 @@ def _must_skip(spec, entry, is_type):
continue
if isinstance(result, (staticmethod, classmethod)):
return False
elif isinstance(result, FunctionTypes):
elif isinstance(result, (FunctionTypes, partialmethod)):
# Normal method => skip if looked up on type
# (if looked up on instance, self is already skipped)
# functools.partialmethod resolves to a plain function carrying
# `self` when looked up via getattr() on the type, see
# functools.partialmethod.__get__.
return is_type
else:
return False
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Fix :func:`unittest.mock.create_autospec` (and ``mock.patch(...,
autospec=True)``) computing the wrong signature for :class:`functools.partial`
and :class:`functools.partialmethod` targets. A ``functools.partial`` mock
previously accepted calls with any number of arguments, since its
effective, post-partial-application signature was never consulted. A
``functools.partialmethod`` class attribute, when autospecced as part of a
whole class, previously had its ``self`` parameter checked against the
partialmethod's own pre-bound argument instead of being skipped, so calls
were validated against the wrong signature shape.
Loading