Skip to content

Add mock_use_standalone_module ini option and lazy-load of mock module #69

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

Merged
merged 2 commits into from
Nov 3, 2016
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)