Skip to content

Commit

Permalink
use getattr_static in spy instead of __getattributes__ (#224)
Browse files Browse the repository at this point in the history
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
  • Loading branch information
yesthesoup and nicoddemus authored Jan 10, 2021
1 parent 2de3e9a commit 379b623
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 48 deletions.
36 changes: 36 additions & 0 deletions src/pytest_mock/_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from typing import Union

_mock_module = None


def get_mock_module(config):
"""
Import and return the actual "mock" module. By default this is
"unittest.mock", but the user can force to always use "mock" using
the mock_use_standalone_module ini option.
"""
global _mock_module
if _mock_module is None:
use_standalone_module = parse_ini_boolean(
config.getini("mock_use_standalone_module")
)
if use_standalone_module:
import mock

_mock_module = mock
else:
import unittest.mock

_mock_module = unittest.mock

return _mock_module


def parse_ini_boolean(value: Union[bool, str]) -> bool:
if isinstance(value, bool):
return value
if value.lower() == "true":
return True
if value.lower() == "false":
return False
raise ValueError("unknown string for bool: %r" % value)
58 changes: 12 additions & 46 deletions src/pytest_mock/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,9 @@

import pytest

_T = TypeVar("_T")


def _get_mock_module(config):
"""
Import and return the actual "mock" module. By default this is
"unittest.mock", but the user can force to always use "mock" using
the mock_use_standalone_module ini option.
"""
if not hasattr(_get_mock_module, "_module"):
use_standalone_module = parse_ini_boolean(
config.getini("mock_use_standalone_module")
)
if use_standalone_module:
import mock

_get_mock_module._module = mock
else:
_get_mock_module._module = unittest.mock
from ._util import get_mock_module, parse_ini_boolean

return _get_mock_module._module
_T = TypeVar("_T")


class PytestMockWarning(UserWarning):
Expand All @@ -54,7 +36,7 @@ class MockerFixture:
def __init__(self, config: Any) -> None:
self._patches = [] # type: List[Any]
self._mocks = [] # type: List[Any]
self.mock_module = mock_module = _get_mock_module(config)
self.mock_module = mock_module = get_mock_module(config)
self.patch = self._Patcher(
self._patches, self._mocks, mock_module
) # type: MockerFixture._Patcher
Expand Down Expand Up @@ -99,20 +81,14 @@ def spy(self, obj: object, name: str) -> unittest.mock.MagicMock:
:return: Spy object.
"""
method = getattr(obj, name)

autospec = inspect.ismethod(method) or inspect.isfunction(method)
# Can't use autospec classmethod or staticmethod objects
# see: https://bugs.python.org/issue23078
if inspect.isclass(obj):
# Bypass class descriptor:
# http://stackoverflow.com/questions/14187973/python3-check-if-method-is-static
try:
value = obj.__getattribute__(obj, name) # type:ignore
except AttributeError:
pass
else:
if isinstance(value, (classmethod, staticmethod)):
autospec = False
if inspect.isclass(obj) and isinstance(
inspect.getattr_static(obj, name), (classmethod, staticmethod)
):
# Can't use autospec classmethod or staticmethod objects before 3.7
# see: https://bugs.python.org/issue23078
autospec = False
else:
autospec = inspect.ismethod(method) or inspect.isfunction(method)

def wrapper(*args, **kwargs):
spy_obj.spy_return = None
Expand Down Expand Up @@ -518,7 +494,7 @@ def wrap_assert_methods(config: Any) -> None:
if _mock_module_originals:
return

mock_module = _get_mock_module(config)
mock_module = get_mock_module(config)

wrappers = {
"assert_called": wrap_assert_called,
Expand Down Expand Up @@ -594,16 +570,6 @@ def pytest_addoption(parser: Any) -> None:
)


def parse_ini_boolean(value: Union[bool, str]) -> bool:
if isinstance(value, bool):
return value
if value.lower() == "true":
return True
if value.lower() == "false":
return False
raise ValueError("unknown string for bool: %r" % value)


def pytest_configure(config: Any) -> None:
tb = config.getoption("--tb", default="auto")
if (
Expand Down
28 changes: 26 additions & 2 deletions tests/test_pytest_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,9 @@ def test_mock_patch_dict_resetall(mocker: MockerFixture) -> None:
],
)
def test_mocker_aliases(name: str, pytestconfig: Any) -> None:
from pytest_mock.plugin import _get_mock_module
from pytest_mock._util import get_mock_module

mock_module = _get_mock_module(pytestconfig)
mock_module = get_mock_module(pytestconfig)

mocker = MockerFixture(pytestconfig)
assert getattr(mocker, name) is getattr(mock_module, name)
Expand Down Expand Up @@ -268,6 +268,19 @@ def bar(self, arg):
assert str(spy.spy_exception) == "Error with {}".format(v)


def test_instance_method_spy_autospec_true(mocker: MockerFixture) -> None:
class Foo:
def bar(self, arg):
return arg * 2

foo = Foo()
spy = mocker.spy(foo, "bar")
with pytest.raises(
AttributeError, match="'function' object has no attribute 'fake_assert_method'"
):
spy.fake_assert_method(arg=5)


def test_spy_reset(mocker: MockerFixture) -> None:
class Foo(object):
def bar(self, x):
Expand Down Expand Up @@ -342,6 +355,17 @@ def bar(cls, arg):
assert spy.spy_return == 20


@skip_pypy
def test_class_method_spy_autospec_false(mocker: MockerFixture) -> None:
class Foo:
@classmethod
def bar(cls, arg):
return arg * 2

spy = mocker.spy(Foo, "bar")
spy.fake_assert_method()


@skip_pypy
def test_class_method_subclass_spy(mocker: MockerFixture) -> None:
class Base:
Expand Down

0 comments on commit 379b623

Please sign in to comment.