From 645680aa557e8aa06544df761d8cd4c95ab72845 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 17 May 2016 14:44:52 +0200 Subject: [PATCH] spy: handle AttributeError with __getattribute__ 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. --- CHANGELOG.rst | 4 ++++ pytest_mock.py | 12 ++++++---- test_pytest_mock.py | 57 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8f1c383..174c261 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 ------ diff --git a/pytest_mock.py b/pytest_mock.py index 63e2a82..bc6b411 100644 --- a/pytest_mock.py +++ b/pytest_mock.py @@ -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) diff --git a/test_pytest_mock.py b/test_pytest_mock.py index 703d165..fc5a6d0 100644 --- a/test_pytest_mock.py +++ b/test_pytest_mock.py @@ -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): @@ -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): @@ -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(): """