diff --git a/Lib/test/test_unittest/testmock/testmock.py b/Lib/test/test_unittest/testmock/testmock.py index 764585ec5d54688..ab2bc5aa8b82e55 100644 --- a/Lib/test/test_unittest/testmock/testmock.py +++ b/Lib/test/test_unittest/testmock/testmock.py @@ -1,9 +1,11 @@ +import asyncio import copy +import functools import re import sys import tempfile -from test.support import ALWAYS_EQ +from test.support import ALWAYS_EQ, requires_working_socket import unittest from test.test_unittest.testmock.support import is_instance from unittest import mock @@ -29,7 +31,8 @@ def next(self): class Something(object): - def meth(self, a, b, c, d=None): pass + def meth(self, a, b, c, d=None): + return a, b, c, d @classmethod def cmeth(cls, a, b, c, d=None): pass @@ -38,6 +41,16 @@ def cmeth(cls, a, b, c, d=None): pass def smeth(a, b, c, d=None): pass +class SomethingAsync(object): + async def meth(self, a, b, c, d=None): pass + + @classmethod + async def cmeth(cls, a, b, c, d=None): pass + + @staticmethod + async def smeth(a, b, c, d=None): pass + + class SomethingElse(object): def __init__(self): self._instance = None @@ -55,7 +68,21 @@ class Typos(): set_spec = None -def something(a): pass +def something(a): + return a + + +def something_two_args(a, b): + return a, b + + +class SomethingWithProps(object): + def __init__(self, a, b, c=None): + self.a = a + self.b = b + self.c = c + + def meth(self, x, y): pass class MockTest(unittest.TestCase): @@ -371,6 +398,17 @@ def test_reset_mock(self): "children incorrectly cleared") self.assertFalse(mock.something.called, "child not reset") + def test_mock_autospec_reset_mock(self): + mock_something = Mock(autospec=Something) + mock_something.meth(sentinel.a, sentinel.b, sentinel.c) + + mock_something.reset_mock() + + self.assertEqual(mock_something.meth.call_count, 0) + # the child mock is preserved by reset_mock(), so signature checking + # is still enforced afterwards. + self.assertRaises(TypeError, mock_something.meth) + mock_something.meth(sentinel.a, sentinel.b, sentinel.c) def test_reset_mock_recursion(self): mock = Mock() @@ -695,6 +733,212 @@ def test_attributes(mock): test_attributes(Mock(spec=Something())) + def _check_autospeced_something(self, something): + # assert that AttributeError is raised if the method does not exist. + self.assertRaises(AttributeError, getattr, something, 'foolish') + + self._check_autospeced_something_method(something.meth) + self._check_autospeced_something_method(something.cmeth) + self._check_autospeced_something_method(something.smeth) + + + def _check_autospeced_something_method(self, mock_method): + # check that the methods are callable with correct args. + mock_method(sentinel.a, sentinel.b, sentinel.c) + mock_method(sentinel.a, sentinel.b, sentinel.c, d=sentinel.d) + mock_method.assert_has_calls([ + call(sentinel.a, sentinel.b, sentinel.c), + call(sentinel.a, sentinel.b, sentinel.c, d=sentinel.d)]) + + # assert that TypeError is raised if the method signature is not + # respected. + self.assertRaises(TypeError, mock_method) + self.assertRaises(TypeError, mock_method, sentinel.a) + self.assertRaises(TypeError, mock_method, a=sentinel.a) + self.assertRaises(TypeError, mock_method, sentinel.a, sentinel.b, + sentinel.c, e=sentinel.e) + + + def test_mock_autospec_all_members(self): + for spec in [Something, Something()]: + mock_something = Mock(autospec=spec) + self._check_autospeced_something(mock_something) + + + def test_mock_autospec_all_members_wraps(self): + something = Something() + mock_something = Mock(autospec=something, wraps=something) + self._check_autospeced_something(mock_something) + + + def _check_autospeced_something_async(self, something): + # assert that AttributeError is raised if the method does not exist. + self.assertRaises(AttributeError, getattr, something, 'foolish') + + self._check_autospeced_something_method_async(something.meth) + self._check_autospeced_something_method_async(something.cmeth) + self._check_autospeced_something_method_async(something.smeth) + + + def _check_autospeced_something_method_async(self, mock_method): + # check that the methods are callable with correct args. + asyncio.run(mock_method(sentinel.a, sentinel.b, sentinel.c)) + asyncio.run(mock_method(sentinel.a, sentinel.b, sentinel.c, + d=sentinel.d)) + mock_method.assert_has_calls([ + call(sentinel.a, sentinel.b, sentinel.c), + call(sentinel.a, sentinel.b, sentinel.c, d=sentinel.d)]) + + # assert that TypeError is raised if the method signature is not + # respected. + self.assertRaises(TypeError, mock_method) + self.assertRaises(TypeError, mock_method, sentinel.a) + self.assertRaises(TypeError, mock_method, a=sentinel.a) + self.assertRaises(TypeError, mock_method, sentinel.a, sentinel.b, + sentinel.c, e=sentinel.e) + + + @requires_working_socket() + def test_mock_autospec_all_members_async(self): + for spec in [SomethingAsync, SomethingAsync()]: + mock_something = AsyncMock(autospec=spec) + self._check_autospeced_something_async(mock_something) + + + @requires_working_socket() + def test_mock_autospec_all_members_wraps_async(self): + something = SomethingAsync() + mock_something = AsyncMock(autospec=something, wraps=something) + self._check_autospeced_something_async(mock_something) + + + def test_mock_autospec_function(self): + mock_func = Mock(autospec=something, wraps=something) + + result = mock_func(sentinel.a) + self.assertEqual(result, sentinel.a) + + self.assertRaises(TypeError, mock_func) + self.assertRaises(TypeError, mock_func, sentinel.a, sentinel.b) + + + def test_mock_autospec_partial_function(self): + partial_something = functools.partial(something_two_args, sentinel.a) + + mock_func = Mock(autospec=partial_something, wraps=partial_something) + + result = mock_func(sentinel.b) + self.assertEqual(result, (sentinel.a, sentinel.b)) + + self.assertRaises(TypeError, mock_func) + self.assertRaises(TypeError, mock_func, sentinel.b, sentinel.c) + + + def test_mock_autospec_partial_method(self): + obj = Something() + partial_meth = functools.partial(obj.meth, sentinel.a, sentinel.b) + + mock_meth = Mock(autospec=partial_meth, wraps=partial_meth) + + result = mock_meth(sentinel.c) + self.assertEqual(result, (sentinel.a, sentinel.b, sentinel.c, None)) + + self.assertRaises(TypeError, mock_meth) + self.assertRaises(TypeError, mock_meth, sentinel.d, e=sentinel.e) + + + def test_mock_autospec_side_effect(self): + def side_effect(a, b, c, d=None): + return (a, b, c, d) + + mock_something = Mock(autospec=Something) + mock_something.meth.side_effect = side_effect + + result = mock_something.meth(sentinel.a, sentinel.b, sentinel.c) + self.assertEqual(result, (sentinel.a, sentinel.b, sentinel.c, None)) + + # signature checking is enforced before the side_effect runs. + self.assertRaises(TypeError, mock_something.meth) + self.assertRaises(TypeError, mock_something.meth, sentinel.a) + + + def test_mock_autospec_class_init_signature(self): + mock_class = Mock(autospec=SomethingWithProps) + + mock_class(sentinel.a, sentinel.b) + mock_class(sentinel.a, sentinel.b, sentinel.c) + + self.assertRaises(TypeError, mock_class) + self.assertRaises(TypeError, mock_class, sentinel.a) + self.assertRaises(TypeError, mock_class, sentinel.a, sentinel.b, + sentinel.c, e=sentinel.e) + + + def test_mock_autospec_with_spec_list_for_init_only_attributes(self): + # SomethingWithProps only assigns 'a' and 'b' as instance attributes + # in __init__, so they are absent from dir(SomethingWithProps) and + # would normally be rejected by autospec. + mock_something = Mock( + autospec=SomethingWithProps, spec=['a', 'b', 'c']) + + # the extra attributes from `spec` are accessible. + mock_something.a + mock_something.b + + # autospec is still applied: methods are still signature-checked. + mock_something.meth(sentinel.x, sentinel.y) + self.assertRaises(TypeError, mock_something.meth) + + # attributes that are neither on the spec class nor in the extra + # `spec` list are still rejected. + self.assertRaises(AttributeError, getattr, mock_something, 'foolish') + + + def test_mock_autospec_nested(self): + class Inner(object): + def meth(self, a, b): pass + + class Outer(object): + inner = Inner() + + mock_outer = Mock(autospec=Outer) + self.assertIsInstance(mock_outer.inner, Mock) + + mock_outer.inner.meth(sentinel.a, sentinel.b) + self.assertRaises(TypeError, mock_outer.inner.meth) + self.assertRaises(TypeError, mock_outer.inner.meth, sentinel.a) + + + def test_mock_autospec_magic_methods(self): + for Klass in MagicMock, NonCallableMagicMock: + mock = Klass(autospec=int) + int(mock) + + mock.__int__.return_value = 4 + self.assertEqual(int(mock), 4) + + # attributes that don't exist on the spec are still rejected. + self.assertRaises(AttributeError, getattr, mock, 'foo') + + + def test_mock_autospec_call_signature(self): + class Caller(object): + def __init__(self, a): pass + def __call__(self, x): pass + + # autospeccing the class checks the constructor signature. + mock_cls = MagicMock(autospec=Caller) + mock_cls(a=sentinel.a) + self.assertRaises(TypeError, mock_cls) + self.assertRaises(TypeError, mock_cls, sentinel.a, sentinel.b) + + # autospeccing an instance checks the __call__ signature instead. + mock_instance = MagicMock(autospec=Caller(sentinel.a)) + mock_instance(x=sentinel.x) + self.assertRaises(TypeError, mock_instance) + self.assertRaises(TypeError, mock_instance, sentinel.x, sentinel.y) + + def test_wraps_calls(self): real = Mock() @@ -1974,6 +2218,21 @@ def test_mock_add_spec_magic_methods(self): self.assertRaises(TypeError, lambda: mock['foo']) + def test_mock_add_spec_autospec_all_members(self): + for spec in [Something, Something()]: + mock_something = Mock() + mock_something.mock_add_spec(spec, autospec=True) + self._check_autospeced_something(mock_something) + + + @requires_working_socket() + def test_mock_add_spec_autospec_all_members_async(self): + for spec in [SomethingAsync, SomethingAsync()]: + mock_something = Mock() + mock_something.mock_add_spec(spec, autospec=True) + self._check_autospeced_something_async(mock_something) + + def test_adding_child_mock(self): for Klass in (NonCallableMock, Mock, MagicMock, NonCallableMagicMock, AsyncMock): @@ -2170,6 +2429,19 @@ def test_attach_mock_return_value(self): self.assertEqual(m.mock_calls, call().foo().call_list()) + def test_mock_autospec_attach_mock(self): + m = Mock() + child = Mock(autospec=Something) + m.attach_mock(child, 'child') + + child.meth(sentinel.a, sentinel.b, sentinel.c) + m.assert_has_calls( + [call.child.meth(sentinel.a, sentinel.b, sentinel.c)]) + + # signature checking survives being attached to another mock. + self.assertRaises(TypeError, child.meth) + + def test_attach_mock_patch_autospec(self): parent = Mock() @@ -2419,6 +2691,13 @@ def test_property_not_called_with_spec_mock(self): self.assertIsNone(obj._instance, msg='after mock') self.assertEqual('object', obj.instance) + def test_mock_autospec_property_not_called(self): + obj = SomethingElse() + self.assertIsNone(obj._instance, msg='before mock') + mock = Mock(autospec=obj) + self.assertIsNone(obj._instance) + self.assertEqual('object', obj.instance) + def test_decorated_async_methods_with_spec_mock(self): class Foo(): @classmethod diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 49011faaa51eaed..a95d23989e64a97 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -107,6 +107,11 @@ 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 accounts for a partial's bound + # arguments; going through func.__call__ (a builtin method-wrapper) + # would lose that and fail to produce a signature at all. + 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__. @@ -136,6 +141,8 @@ def checksig(self, /, *args, **kwargs): type(mock)._mock_check_sig = checksig type(mock).__signature__ = sig + return func, sig + def _copy_func_details(func, funcopy): # we explicitly don't copy func.__dict__ into this copy as it would @@ -463,7 +470,8 @@ def __new__( def __init__( self, spec=None, wraps=None, name=None, spec_set=None, parent=None, _spec_state=None, _new_name='', _new_parent=None, - _spec_as_instance=False, _eat_self=None, unsafe=False, **kwargs + _spec_as_instance=False, _eat_self=None, unsafe=False, + autospec=None, **kwargs ): if _new_parent is None: _new_parent = parent @@ -474,14 +482,29 @@ def __init__( __dict__['_mock_new_name'] = _new_name __dict__['_mock_new_parent'] = _new_parent __dict__['_mock_sealed'] = False + __dict__['_mock_autospec'] = autospec if spec_set is not None: spec = spec_set spec_set = True + + extra_spec_props = None + if autospec is not None: + # autospec is even stricter than spec_set. + # an explicit spec given as a list of attribute names is preserved + # as additional allowed attributes (e.g.: instance attributes set + # in __init__, which won't show up via autospec). + if spec is not None and _is_list(spec): + extra_spec_props = spec + spec = autospec + autospec = True + if _eat_self is None: _eat_self = parent is not None self._mock_add_spec(spec, spec_set, _spec_as_instance, _eat_self) + if extra_spec_props is not None: + self._mock_extend_spec_methods(extra_spec_props) __dict__['_mock_children'] = {} __dict__['_mock_wraps'] = wraps @@ -520,12 +543,18 @@ def attach_mock(self, mock, attribute): setattr(self, attribute, mock) - def mock_add_spec(self, spec, spec_set=False): + def mock_add_spec(self, spec, spec_set=False, autospec=None): """Add a spec to a mock. `spec` can either be an object or a list of strings. Only attributes on the `spec` can be fetched as attributes from the mock. - If `spec_set` is True then only attributes on the spec can be set.""" + If `spec_set` is True then only attributes on the spec can be set. + If `autospec` is True then only attributes on the spec can be accessed + and set, and if a method in the `spec` is called, it's signature is + checked. + """ + if autospec is not None: + self.__dict__['_mock_autospec'] = autospec self._mock_add_spec(spec, spec_set) @@ -543,8 +572,13 @@ def _mock_add_spec(self, spec, spec_set, _spec_as_instance=False, _spec_class = spec else: _spec_class = type(spec) - res = _get_signature_object(spec, - _spec_as_instance, _eat_self) + + if self.__dict__.get('_mock_autospec') is None: + res = _get_signature_object(spec, _spec_as_instance, _eat_self) + else: + res = _check_signature(spec, self, _eat_self, + _spec_as_instance) + _spec_signature = res and res[1] spec_list = dir(spec) @@ -712,9 +746,20 @@ def __getattr__(self, name): # execution? wraps = getattr(self._mock_wraps, name) + kwargs = {} + if self.__dict__.get('_mock_autospec') is not None: + # get the mock's spec attribute with the same name and + # pass it to the child. + spec_class = self.__dict__.get('_spec_class') + spec = getattr(spec_class, name, None) + is_type = isinstance(spec_class, type) + eat_self = _must_skip(spec_class, name, is_type) + kwargs['_eat_self'] = eat_self + kwargs['autospec'] = spec + result = self._get_child_mock( parent=self, name=name, wraps=wraps, _new_name=name, - _new_parent=self + _new_parent=self, **kwargs ) self._mock_children[name] = result diff --git a/Misc/NEWS.d/next/Library/2017-11-20-06-36-00.bpo-30587.1SsAy9.rst b/Misc/NEWS.d/next/Library/2017-11-20-06-36-00.bpo-30587.1SsAy9.rst new file mode 100644 index 000000000000000..105b8319e37f4bc --- /dev/null +++ b/Misc/NEWS.d/next/Library/2017-11-20-06-36-00.bpo-30587.1SsAy9.rst @@ -0,0 +1,3 @@ +mock: Added autospec argument to the constructors Mock constructors and mock_add_spec. +Passing the autospec argument will also check the method signatures of the mocked +methods.