Skip to content

Commit

Permalink
spy: handle AttributeError with __getattribute__
Browse files Browse the repository at this point in the history
I have noticed that `spy` did not work on functions that are not defined
in the class itself, but a parent class.  This patch fixes this.
  • Loading branch information
blueyed committed May 17, 2016
1 parent 5d57dd7 commit 645680a
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 4 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@
functions which receive a parameter named ``method``.
Thanks `@sagarchalise`_ for the report (`#31`_).

* Fix AttributeError with ``mocker.spy`` when spying on inherited methods
(`#42`_).

.. _@sagarchalise: https://github.com/sagarchalise
.. _@satyrius: https://github.com/satyrius
.. _#31: https://github.com/pytest-dev/pytest-mock/issues/31
.. _#32: https://github.com/pytest-dev/pytest-mock/issues/32
.. _#42: https://github.com/pytest-dev/pytest-mock/issues/42

0.10.1
------
Expand Down
12 changes: 8 additions & 4 deletions pytest_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,15 @@ def spy(self, obj, name):
# Can't use autospec classmethod or staticmethod objects
# see: https://bugs.python.org/issue23078
if inspect.isclass(obj):
# bypass class descriptor:
# Bypass class descriptor:
# http://stackoverflow.com/questions/14187973/python3-check-if-method-is-static
value = obj.__getattribute__(obj, name)
if isinstance(value, (classmethod, staticmethod)):
autospec = False
try:
value = obj.__getattribute__(obj, name)
except AttributeError:
pass
else:
if isinstance(value, (classmethod, staticmethod)):
autospec = False

result = self.patch.object(obj, name, side_effect=method,
autospec=autospec)
Expand Down
57 changes: 57 additions & 0 deletions test_pytest_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,27 @@ def bar(self, arg):
assert spy.call_args_list == calls


@skip_pypy
def test_instance_method_by_subclass_spy(mocker):
from pytest_mock import mock_module

class Base(object):

def bar(self, arg):
return arg * 2

class Foo(Base):
pass

spy = mocker.spy(Foo, 'bar')
foo = Foo()
other = Foo()
assert foo.bar(arg=10) == 20
assert other.bar(arg=10) == 20
calls = [mock_module.call(foo, arg=10), mock_module.call(other, arg=10)]
assert spy.call_args_list == calls


@skip_pypy
def test_class_method_spy(mocker):
class Foo(object):
Expand All @@ -225,6 +246,24 @@ def bar(cls, arg):
spy.assert_called_once_with(arg=10)


@skip_pypy
@pytest.mark.xfail(sys.version_info[0] == 2, reason='does not work on Python 2')
def test_class_method_subclass_spy(mocker):
class Base(object):

@classmethod
def bar(self, arg):
return arg * 2

class Foo(Base):
pass

spy = mocker.spy(Foo, 'bar')
assert Foo.bar(arg=10) == 20
Foo.bar.assert_called_once_with(arg=10)
spy.assert_called_once_with(arg=10)


@skip_pypy
def test_class_method_with_metaclass_spy(mocker):
class MetaFoo(type):
Expand Down Expand Up @@ -258,6 +297,24 @@ def bar(arg):
spy.assert_called_once_with(arg=10)


@skip_pypy
@pytest.mark.xfail(sys.version_info[0] == 2, reason='does not work on Python 2')
def test_static_method_subclass_spy(mocker):
class Base(object):

@staticmethod
def bar(arg):
return arg * 2

class Foo(Base):
pass

spy = mocker.spy(Foo, 'bar')
assert Foo.bar(arg=10) == 20
Foo.bar.assert_called_once_with(arg=10)
spy.assert_called_once_with(arg=10)


@contextmanager
def assert_traceback():
"""
Expand Down

0 comments on commit 645680a

Please sign in to comment.