Skip to content

Commit

Permalink
Merge pull request #69 from nicoddemus/use-standalone-mock
Browse files Browse the repository at this point in the history
Add mock_use_standalone_module ini option and lazy-load of mock module
  • Loading branch information
nicoddemus authored Nov 3, 2016
2 parents 5dfb9d7 + 786f534 commit a4cfee7
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 52 deletions.
25 changes: 23 additions & 2 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
1.3
---
1.4.0
-----

* New configuration variable, ``mock_use_standalone_module`` (defaults to ``False``). This forces
the plugin to import ``mock`` instead of ``unittest.mock`` on Python 3. This is useful to import
and use a newer version than the one available in the Python distribution.

* Previously the plugin would first try to import ``mock`` and fallback to ``unittest.mock``
in case of an ``ImportError``, but this behavior has been removed because it could hide
hard to debug import errors (`#68`_).

* Now ``mock`` (Python 2) and ``unittest.mock`` (Python 3) are lazy-loaded to make it possible to
implement the new ``mock_use_standlone_module`` configuration option. As a consequence of this
the undocumented ``pytest_mock.mock_module`` variable, which pointed to the actual mock module
being used by the plugin, has been removed.

* `DEFAULT <https://docs.python.org/3/library/unittest.mock.html#default>`_ is now available from
the ``mocker`` fixture.

.. _#68: https://github.com/pytest-dev/pytest-mock/issues/68

1.3.0
-----

* Add support for Python 3.6. Thanks `@hackebrot`_ for the report (`#59`_).

Expand Down
19 changes: 19 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ Some objects from the ``mock`` module are accessible directly from ``mocker`` fo
* `MagicMock <https://docs.python.org/3/library/unittest.mock.html#unittest.mock.MagicMock>`_
* `PropertyMock <https://docs.python.org/3/library/unittest.mock.html#unittest.mock.PropertyMock>`_
* `ANY <https://docs.python.org/3/library/unittest.mock.html#any>`_
* `DEFAULT <https://docs.python.org/3/library/unittest.mock.html#default>`_ *(Version 1.4)*
* `call <https://docs.python.org/3/library/unittest.mock.html#call>`_ *(Version 1.1)*
* `sentinel <https://docs.python.org/3/library/unittest.mock.html#sentinel>`_ *(Version 1.2)*
* `mock_open <https://docs.python.org/3/library/unittest.mock.html#mock-open>`_
Expand Down Expand Up @@ -153,6 +154,24 @@ anyway plus it generates confusing messages on Python 3.5 due to exception chain
.. _advanced assertions: https://pytest.org/latest/assert.html


Use standalone "mock" package
-----------------------------

*New in version 1.4.0.*

Python 3 users might want to use a newest version of the ``mock`` package as published on PyPI
than the one that comes with the Python distribution.

.. code-block:: ini
[pytest]
mock_use_standalone_module = true
This will force the plugin to import ``mock`` instead of the ``unittest.mock`` module bundled with
Python 3.3+. Note that this option is only used in Python 3+, as Python 2 users only have the option
to use the ``mock`` package from PyPI anyway.


Requirements
============

Expand Down
71 changes: 46 additions & 25 deletions pytest_mock.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,50 @@
import inspect
import sys

import pytest

try:
import mock as mock_module
except ImportError:
import unittest.mock as mock_module

from _pytest_mock_version import version
__version__ = version


def _get_mock_module(config):
"""
Import and return the actual "mock" module. By default this is "mock" for Python 2 and
"unittest.mock" for Python 3, but the user can force to always use "mock" on Python 3 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 sys.version_info[0] == 2 or use_standalone_module:
import mock
_get_mock_module._module = mock
else:
import unittest.mock
_get_mock_module._module = unittest.mock

return _get_mock_module._module


class MockFixture(object):
"""
Fixture that provides the same interface to functions in the mock module,
ensuring that they are uninstalled at the end of each test.
"""

Mock = mock_module.Mock
MagicMock = mock_module.MagicMock
PropertyMock = mock_module.PropertyMock
call = mock_module.call
ANY = mock_module.ANY
sentinel = mock_module.sentinel
mock_open = mock_module.mock_open

def __init__(self):
def __init__(self, config):
self._patches = [] # list of mock._patch objects
self._mocks = [] # list of MagicMock objects
self.patch = self._Patcher(self._patches, self._mocks)
# temporary fix: this should be at class level, but is blowing
# up in Python 3.6
self._mock_module = mock_module = _get_mock_module(config)
self.patch = self._Patcher(self._patches, self._mocks, mock_module)
# aliases for convenience
self.Mock = mock_module.Mock
self.MagicMock = mock_module.MagicMock
self.PropertyMock = mock_module.PropertyMock
self.call = mock_module.call
self.ANY = mock_module.ANY
self.DEFAULT = mock_module.DEFAULT
self.sentinel = mock_module.sentinel
self.mock_open = mock_module.mock_open
self.sentinel = mock_module.sentinel
self.mock_open = mock_module.mock_open

Expand Down Expand Up @@ -90,17 +104,18 @@ def stub(self, name=None):
:rtype: mock.MagicMock
:return: Stub object.
"""
return mock_module.MagicMock(spec=lambda *args, **kwargs: None, name=name)
return self._mock_module.MagicMock(spec=lambda *args, **kwargs: None, name=name)

class _Patcher(object):
"""
Object to provide the same interface as mock.patch, mock.patch.object,
etc. We need this indirection to keep the same API of the mock package.
"""

def __init__(self, patches, mocks):
def __init__(self, patches, mocks, mock_module):
self._patches = patches
self._mocks = mocks
self._mock_module = mock_module

def _start_patch(self, mock_func, *args, **kwargs):
"""Patches something by calling the given function from the mock
Expand All @@ -115,29 +130,29 @@ def _start_patch(self, mock_func, *args, **kwargs):

def object(self, *args, **kwargs):
"""API to mock.patch.object"""
return self._start_patch(mock_module.patch.object, *args, **kwargs)
return self._start_patch(self._mock_module.patch.object, *args, **kwargs)

def multiple(self, *args, **kwargs):
"""API to mock.patch.multiple"""
return self._start_patch(mock_module.patch.multiple, *args,
return self._start_patch(self._mock_module.patch.multiple, *args,
**kwargs)

def dict(self, *args, **kwargs):
"""API to mock.patch.dict"""
return self._start_patch(mock_module.patch.dict, *args, **kwargs)
return self._start_patch(self._mock_module.patch.dict, *args, **kwargs)

def __call__(self, *args, **kwargs):
"""API to mock.patch"""
return self._start_patch(mock_module.patch, *args, **kwargs)
return self._start_patch(self._mock_module.patch, *args, **kwargs)


@pytest.yield_fixture
def mocker():
def mocker(pytestconfig):
"""
return an object that has the same interface to the `mock` module, but
takes care of automatically undoing all patches after each test method.
"""
result = MockFixture()
result = MockFixture(pytestconfig)
yield result
result.stopall()

Expand Down Expand Up @@ -209,6 +224,8 @@ def wrap_assert_methods(config):
if _mock_module_originals:
return

mock_module = _get_mock_module(config)

wrappers = {
'assert_not_called': wrap_assert_not_called,
'assert_called_with': wrap_assert_called_with,
Expand Down Expand Up @@ -247,6 +264,10 @@ def pytest_addoption(parser):
'Monkeypatch the mock library to improve reporting of the '
'assert_called_... methods',
default=True)
parser.addini('mock_use_standalone_module',
'Use standalone "mock" (from PyPI) instead of builtin "unittest.mock" '
'on Python 3',
default=False)


def parse_ini_boolean(value):
Expand Down
66 changes: 41 additions & 25 deletions test_pytest_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,8 @@ def mock_using_patch(mocker):


def mock_using_patch_multiple(mocker):
from pytest_mock import mock_module

r = mocker.patch.multiple('os', remove=mock_module.DEFAULT,
listdir=mock_module.DEFAULT)
r = mocker.patch.multiple('os', remove=mocker.DEFAULT,
listdir=mocker.DEFAULT)
return r['remove'], r['listdir']


Expand Down Expand Up @@ -133,10 +131,12 @@ def test_deprecated_mock(mock, tmpdir):


@pytest.mark.parametrize('name', ['MagicMock', 'PropertyMock', 'Mock', 'call', 'ANY', 'sentinel', 'mock_open'])
def test_mocker_aliases(name):
from pytest_mock import mock_module, MockFixture
def test_mocker_aliases(name, pytestconfig):
from pytest_mock import _get_mock_module, MockFixture

mock_module = _get_mock_module(pytestconfig)

mocker = MockFixture()
mocker = MockFixture(pytestconfig)
assert getattr(mocker, name) is getattr(mock_module, name)


Expand Down Expand Up @@ -203,8 +203,6 @@ def bar(self, arg):

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

class Foo(object):

def bar(self, arg):
Expand All @@ -215,13 +213,12 @@ def bar(self, arg):
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)]
calls = [mocker.call(foo, arg=10), mocker.call(other, arg=10)]
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):

Expand All @@ -236,7 +233,7 @@ class Foo(Base):
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)]
calls = [mocker.call(foo, arg=10), mocker.call(other, arg=10)]
assert spy.call_args_list == calls


Expand Down Expand Up @@ -424,12 +421,11 @@ def test_assert_any_call_wrapper(mocker):


def test_assert_has_calls(mocker):
from pytest_mock import mock_module
stub = mocker.stub()
stub("foo")
stub.assert_has_calls([mock_module.call("foo")])
stub.assert_has_calls([mocker.call("foo")])
with assert_traceback():
stub.assert_has_calls([mock_module.call("bar")])
stub.assert_has_calls([mocker.call("bar")])


def test_monkeypatch_ini(mocker, testdir):
Expand All @@ -447,11 +443,7 @@ def test_foo(mocker):
[pytest]
mock_traceback_monkeypatch = false
""")
if hasattr(testdir, 'runpytest_subprocess'):
result = testdir.runpytest_subprocess()
else:
# pytest 2.7.X
result = testdir.runpytest()
result = runpytest_subprocess(testdir)
assert result.ret == 0


Expand Down Expand Up @@ -487,14 +479,38 @@ def test_foo(mocker):
stub(1, greet='hello')
stub.assert_called_once_with(1, greet='hey')
""")
if hasattr(testdir, 'runpytest_subprocess'):
result = testdir.runpytest_subprocess('--tb=native')
else:
# pytest 2.7.X
result = testdir.runpytest('--tb=native')
result = runpytest_subprocess(testdir, '--tb=native')
assert result.ret == 1
assert 'During handling of the above exception' not in result.stdout.str()
assert 'Differing items:' not in result.stdout.str()
traceback_lines = [x for x in result.stdout.str().splitlines()
if 'Traceback (most recent call last)' in x]
assert len(traceback_lines) == 1 # make sure there are no duplicated tracebacks (#44)


@pytest.mark.skipif(sys.version_info[0] < 3, reason='Py3 only')
def test_standalone_mock(testdir):
"""Check that the "mock_use_standalone" is being used.
"""
testdir.makepyfile("""
def test_foo(mocker):
pass
""")
testdir.makeini("""
[pytest]
mock_use_standalone_module = true
""")
result = runpytest_subprocess(testdir)
assert result.ret == 3
result.stderr.fnmatch_lines([
"*No module named 'mock'*",
])


def runpytest_subprocess(testdir, *args):
"""Testdir.runpytest_subprocess only available in pytest-2.8+"""
if hasattr(testdir, 'runpytest_subprocess'):
return testdir.runpytest_subprocess(*args)
else:
# pytest 2.7.X
return testdir.runpytest(*args)

0 comments on commit a4cfee7

Please sign in to comment.