Skip to content
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

use getattr_static in spy instead of __getattributes__ #224

Merged
merged 4 commits into from Jan 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 36 additions & 0 deletions 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)
58 changes: 12 additions & 46 deletions src/pytest_mock/plugin.py
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
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