diff --git a/Lib/inspect.py b/Lib/inspect.py --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2028,19 +2028,19 @@ class Signature: return self._bound_arguments_cls(self, arguments) - def bind(self, *args, **kwargs): + def bind(__bind_self, *args, **kwargs): '''Get a BoundArguments object, that maps the passed `args` and `kwargs` to the function's signature. Raises `TypeError` if the passed arguments can not be bound. ''' - return self._bind(args, kwargs) + return __bind_self._bind(args, kwargs) - def bind_partial(self, *args, **kwargs): + def bind_partial(__bind_self, *args, **kwargs): '''Get a BoundArguments object, that partially maps the passed `args` and `kwargs` to the function's signature. Raises `TypeError` if the passed arguments can not be bound. ''' - return self._bind(args, kwargs, partial=True) + return __bind_self._bind(args, kwargs, partial=True) def __str__(self): result = [] diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -27,7 +27,7 @@ import inspect import pprint import sys -from functools import wraps +from functools import wraps, partial BaseExceptions = (BaseException,) @@ -66,6 +66,34 @@ DescriptorTypes = ( ) +def _get_signature_object(func, as_instance): + """ + Given an arbitrary, possibly callable object, try to create a suitable + signature object. + """ + old_func = func + if isinstance(func, type) and not as_instance: + # If it's a type and should be modelled as a type, use __init__. + try: + func = func.__init__ + except AttributeError: + return None + # Skip the `self` argument in __init__ + func = partial(func, None) + elif not isinstance(func, FunctionTypes): + # If we really want to model an instance of the passed type, + # __call__ should be looked up, not __init__. + try: + func = func.__call__ + except AttributeError: + return None + try: + return inspect.signature(func) + except ValueError: + # Certain callable types are not supported by inspect.signature() + return None + + def _getsignature(func, skipfirst, instance=False): if isinstance(func, type) and not instance: try: @@ -368,7 +396,7 @@ class NonCallableMock(Base): def __init__( self, spec=None, wraps=None, name=None, spec_set=None, parent=None, _spec_state=None, _new_name='', _new_parent=None, - **kwargs + _spec_as_instance=False, _eat_self=None, **kwargs ): if _new_parent is None: _new_parent = parent @@ -383,7 +411,11 @@ class NonCallableMock(Base): spec = spec_set spec_set = True - self._mock_add_spec(spec, spec_set) + self._mock_add_spec(spec, spec_set, _spec_as_instance) + + if _eat_self is None: + _eat_self = parent is not None + __dict__['_mock_eat_self'] = _eat_self __dict__['_mock_children'] = {} __dict__['_mock_wraps'] = wraps @@ -428,20 +460,23 @@ class NonCallableMock(Base): self._mock_add_spec(spec, spec_set) - def _mock_add_spec(self, spec, spec_set): + def _mock_add_spec(self, spec, spec_set, _spec_as_instance=False): _spec_class = None + _spec_signature = None if spec is not None and not _is_list(spec): if isinstance(spec, type): _spec_class = spec else: _spec_class = _get_class(spec) + _spec_signature = _get_signature_object(spec, _spec_as_instance) spec = dir(spec) __dict__ = self.__dict__ __dict__['_spec_class'] = _spec_class __dict__['_spec_set'] = spec_set + __dict__['_spec_signature'] = _spec_signature __dict__['_mock_methods'] = spec @@ -695,22 +730,44 @@ class NonCallableMock(Base): self._mock_children[name] = _deleted - def _format_mock_call_signature(self, args, kwargs): name = self._mock_name or 'mock' return _format_call_signature(name, args, kwargs) - def _format_mock_failure_message(self, args, kwargs): + def _format_mock_failure_message(self, args, kwargs, _call): message = 'Expected call: %s\nActual call: %s' expected_string = self._format_mock_call_signature(args, kwargs) - call_args = self.call_args - if len(call_args) == 3: - call_args = call_args[1:] - actual_string = self._format_mock_call_signature(*call_args) + if len(_call) == 3: + _call = _call[1:] + actual_string = self._format_mock_call_signature(*_call) return message % (expected_string, actual_string) + def _match_call(self, expected_args, expected_kwargs, _call): + def _fail(expected_args=expected_args): + msg = self._format_mock_failure_message( + expected_args, expected_kwargs, _call) + raise AssertionError(msg) + sig = self._spec_signature + args, kwargs = _call + if sig is not None: + if self._mock_eat_self: + # Need to skip the implicit self + expected_args = (None,) + expected_args + args = (None,) + args + try: + bound_expected = sig.bind(*expected_args, **expected_kwargs) + bound_actual = sig.bind(*args, **kwargs) + except TypeError: + _fail() + else: + if bound_expected != bound_actual: + _fail() + elif (args, kwargs) != (expected_args, expected_kwargs): + _fail() + + def assert_called_with(_mock_self, *args, **kwargs): """assert that the mock was called with the specified arguments. @@ -720,10 +777,7 @@ class NonCallableMock(Base): if self.call_args is None: expected = self._format_mock_call_signature(args, kwargs) raise AssertionError('Expected call: %s\nNot called' % (expected,)) - - if self.call_args != (args, kwargs): - msg = self._format_mock_failure_message(args, kwargs) - raise AssertionError(msg) + self._match_call(args, kwargs, self.call_args) def assert_called_once_with(_mock_self, *args, **kwargs): @@ -850,11 +904,12 @@ class CallableMixin(Base): self = _mock_self self.called = True self.call_count += 1 - self.call_args = _Call((args, kwargs), two=True) - self.call_args_list.append(_Call((args, kwargs), two=True)) - _new_name = self._mock_new_name _new_parent = self._mock_new_parent + + _call = _Call((args, kwargs), two=True) + self.call_args = _call + self.call_args_list.append(_call) self.mock_calls.append(_Call(('', args, kwargs))) seen = set() @@ -2031,6 +2086,8 @@ def create_autospec(spec, spec_set=False elif spec is None: # None we mock with a normal mock without a spec _kwargs = {} + if _kwargs and instance: + _kwargs['_spec_as_instance'] = True _kwargs.update(kwargs) @@ -2097,10 +2154,13 @@ def create_autospec(spec, spec_set=False if isinstance(spec, FunctionTypes): parent = mock.mock + skipfirst = _must_skip(spec, entry, is_type) + if not skipfirst: + kwargs['_eat_self'] = False new = MagicMock(parent=parent, name=entry, _new_name=entry, - _new_parent=parent, **kwargs) + _new_parent=parent, + **kwargs) mock._mock_children[entry] = new - skipfirst = _must_skip(spec, entry, is_type) _check_signature(original, new, skipfirst=skipfirst) # so functions created with _set_signature become instance attributes, @@ -2126,7 +2186,8 @@ def _must_skip(spec, entry, is_type): continue if isinstance(result, (staticmethod, classmethod)): return False - return is_type + if isinstance(getattr(result, '__get__', None), MethodWrapperTypes): + return is_type # shouldn't get here unless function is a dynamically provided attribute # XXXX untested behaviour @@ -2160,6 +2221,14 @@ FunctionTypes = ( type(ANY.__eq__), ) +MethodTypes = ( + type(ANY.__eq__), +) + +MethodWrapperTypes = ( + type(ANY.__eq__.__get__), +) + file_spec = None diff --git a/Lib/unittest/test/testmock/testhelpers.py b/Lib/unittest/test/testmock/testhelpers.py --- a/Lib/unittest/test/testmock/testhelpers.py +++ b/Lib/unittest/test/testmock/testhelpers.py @@ -337,9 +337,10 @@ class SpecSignatureTest(unittest.TestCas def test_basic(self): - for spec in (SomeClass, SomeClass()): - mock = create_autospec(spec) - self._check_someclass_mock(mock) + mock = create_autospec(SomeClass) + self._check_someclass_mock(mock) + mock = create_autospec(SomeClass()) + self._check_someclass_mock(mock) def test_create_autospec_return_value(self): @@ -779,6 +780,8 @@ class SpecSignatureTest(unittest.TestCas pass a = create_autospec(Foo) + a.f(10) + a.f.assert_called_with(10) a.f(self=10) a.f.assert_called_with(self=10) diff --git a/Lib/unittest/test/testmock/testmock.py b/Lib/unittest/test/testmock/testmock.py --- a/Lib/unittest/test/testmock/testmock.py +++ b/Lib/unittest/test/testmock/testmock.py @@ -273,6 +273,19 @@ class MockTest(unittest.TestCase): mock.assert_called_with(1, 2, 3, a='fish', b='nothing') + def test_assert_called_with_spec(self): + def f(a, b, c, d=None): + pass + + mock = Mock(spec=f) + + mock(1, b=2, c=3) + mock.assert_called_with(1, 2, 3) + mock.assert_called_with(a=1, b=2, c=3) + self.assertRaises(AssertionError, mock.assert_called_with, + 1, b=3, c=2) + + def test_assert_called_once_with(self): mock = Mock() mock()