diff --git a/src/pytest_mock/_util.py b/src/pytest_mock/_util.py new file mode 100644 index 0000000..d3a732a --- /dev/null +++ b/src/pytest_mock/_util.py @@ -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) diff --git a/src/pytest_mock/plugin.py b/src/pytest_mock/plugin.py index 3be8667..158b87d 100644 --- a/src/pytest_mock/plugin.py +++ b/src/pytest_mock/plugin.py @@ -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): @@ -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 @@ -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 @@ -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, @@ -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 ( diff --git a/tests/test_pytest_mock.py b/tests/test_pytest_mock.py index f9fa520..3d2c5ba 100644 --- a/tests/test_pytest_mock.py +++ b/tests/test_pytest_mock.py @@ -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) @@ -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): @@ -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: