8000 Mock patch fix autospec by claudiubelu · Pull Request #1983 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

Mock patch fix autospec #1983

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Adds signature checking for mock autospec object method calls
Mock can accept an spec object / class as argument, making sure
that accessing attributes that do not exist in the spec will cause an
AttributeError to be raised, but there is no guarantee that the spec's
methods signatures are respected in any way. This creates the possibility
to have faulty code with passing unittests and assertions.

Example:

import mock

class Something(object):
    def foo(self, a, b, c, d):
        pass

m = mock.Mock(spec=Something)
m.foo()

Adds the autospec argument to Mock, and its mock_add_spec method.

Passes the spec's attribute with the same name to the child mock (spec-ing
the child), if the mock's autospec is True.

Sets _mock_check_sig if the given spec is callable.

Adds unit tests to validate the fact that the autospec method signatures are
respected.
  • Loading branch information
claudiubelu committed Jun 7, 2017
commit 654dfca2a005ffc43b74fa5c5a7d0b58ce453fbb
39 changes: 32 additions & 7 deletions Lib/unittest/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ def checksig(_mock_self, *args, **kwargs):
_copy_func_details(func, checksig)
type(mock)._mock_check_sig = checksig

return sig


def _copy_func_details(func, funcopy):
# we explicitly don't copy func.__dict__ into this copy as it would
Expand Down Expand Up @@ -372,7 +374,8 @@ def __new__(cls, *args, **kw):
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
Expand All @@ -382,10 +385,15 @@ def __init__(
__dict__['_mock_name'] = name
__dict__['_mock_new_name'] = _new_name
__dict__['_mock_new_parent'] = _new_parent
__dict__['_autospec'] = autospec

if spec_set is not None:
spec = spec_set
spec_set = True
if autospec is not None:
# autospec is even stricter than spec_set.
spec = autospec
autospec = True
if _eat_self is None:
_eat_self = parent is not None

Expand Down Expand Up @@ -426,12 +434,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__['_autospec'] = autospec
self._mock_add_spec(spec, spec_set)


Expand All @@ -445,9 +459,9 @@ def _mock_add_spec(self, spec, spec_set, _spec_as_instance=False,
_spec_class = spec
else:
_spec_class = _get_class(spec)
res = _get_signature_object(spec,
_spec_as_instance, _eat_self)
_spec_signature = res and res[1]

_spec_signature = _check_signature(spec, self, _eat_self,
_spec_as_instance)

spec = dir(spec)

Expand Down Expand Up @@ -592,9 +606,20 @@ def __getattr__(self, name):
# execution?
wraps = getattr(self._mock_wraps, name)

kwargs = {}
if self.__dict__.get('_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

Expand Down
49 changes: 49 additions & 0 deletions Lib/unittest/test/testmock/testmock.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,48 @@ def test_only_allowed_methods_exist(self):
)


def _check_autospeced_something(self, something):
for method_name in ['meth', 'cmeth', 'smeth']:
mock_method = getattr(something, method_name)

# 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)

# assert that AttributeError is raised if the method does not exist.
self.assertRaises(AttributeError, getattr, something, 'foolish')


def test_mock_autospec_all_members(self):
for spec in [Something, Something()]:
mock = Mock(autospec=spec)
self._check_autospeced_something(mock)


def test_mock_spec_function(self):
def foo(lish):
pass

mock = Mock(spec=foo)

mock(sentinel.lish)
mock.assert_called_once_with(sentinel.lish)
self.assertRaises(TypeError, mock)
self.assertRaises(TypeError, sentinel.foo, sentinel.lish)
self.assertRaises(TypeError, mock, foo=sentinel.foo)


def test_from_spec(self):
class Something(object):
x = 3
Expand Down Expand Up @@ -1376,6 +1418,13 @@ 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 = Mock()
mock.mock_add_spec(spec, autospec=True)
self._check_autospeced_something(mock)


def test_adding_child_mock(self):
for Klass in NonCallableMock, Mock, MagicMock, NonCallableMagicMock:
mock = Klass()
Expand Down
0