-
-
Notifications
You must be signed in to change notification settings - Fork 147
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
Wrap assert methods of mock module to clean up traceback #27
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
import sys | ||
|
||
import pytest | ||
from distutils.util import strtobool | ||
|
||
if sys.version_info >= (3, 3): # pragma: no cover | ||
import unittest.mock as mock_module | ||
|
@@ -137,3 +138,98 @@ def mock(mocker): | |
warnings.warn('"mock" fixture has been deprecated, use "mocker" instead', | ||
DeprecationWarning) | ||
return mocker | ||
|
||
|
||
_mock_module_patches = [] | ||
_mock_module_originals = {} | ||
|
||
|
||
def assert_wrapper(method, *args, **kwargs): | ||
__tracebackhide__ = True | ||
try: | ||
method(*args, **kwargs) | ||
except AssertionError as e: | ||
raise AssertionError(*e.args) | ||
|
||
|
||
def wrap_assert_not_called(*args, **kwargs): | ||
__tracebackhide__ = True | ||
assert_wrapper(_mock_module_originals["assert_not_called"], | ||
*args, **kwargs) | ||
|
||
|
||
def wrap_assert_called_with(*args, **kwargs): | ||
__tracebackhide__ = True | ||
assert_wrapper(_mock_module_originals["assert_called_with"], | ||
*args, **kwargs) | ||
|
||
|
||
def wrap_assert_called_once_with(*args, **kwargs): | ||
__tracebackhide__ = True | ||
assert_wrapper(_mock_module_originals["assert_called_once_with"], | ||
*args, **kwargs) | ||
|
||
|
||
def wrap_assert_has_calls(*args, **kwargs): | ||
__tracebackhide__ = True | ||
assert_wrapper(_mock_module_originals["assert_has_calls"], | ||
*args, **kwargs) | ||
|
||
|
||
def wrap_assert_any_call(*args, **kwargs): | ||
__tracebackhide__ = True | ||
assert_wrapper(_mock_module_originals["assert_any_call"], | ||
*args, **kwargs) | ||
|
||
|
||
def wrap_assert_methods(config): | ||
""" | ||
Wrap assert methods of mock module so we can hide their traceback | ||
""" | ||
# Make sure we only do this once | ||
if _mock_module_originals: | ||
return | ||
|
||
wrappers = { | ||
'assert_not_called': wrap_assert_not_called, | ||
'assert_called_with': wrap_assert_called_with, | ||
'assert_called_once_with': wrap_assert_called_once_with, | ||
'assert_has_calls': wrap_assert_has_calls, | ||
'assert_any_call': wrap_assert_any_call, | ||
} | ||
for method, wrapper in wrappers.items(): | ||
try: | ||
original = getattr(mock_module.NonCallableMock, method) | ||
except AttributeError: | ||
continue | ||
_mock_module_originals[method] = original | ||
patcher = mock_module.patch.object( | ||
mock_module.NonCallableMock, method, wrapper) | ||
patcher.start() | ||
_mock_module_patches.append(patcher) | ||
config.add_cleanup(unwrap_assert_methods) | ||
|
||
|
||
def unwrap_assert_methods(): | ||
for patcher in _mock_module_patches: | ||
patcher.stop() | ||
_mock_module_patches[:] = [] | ||
_mock_module_originals.clear() | ||
|
||
|
||
def pytest_addoption(parser): | ||
parser.addini('mock_traceback_monkeypatch', | ||
'Monkeypatch the mock library to improve reporting of the ' | ||
'assert_called_... methods', | ||
default=True) | ||
|
||
|
||
def parse_ini_boolean(value): | ||
if value in (True, False): | ||
return value | ||
return bool(strtobool(value)) | ||
|
||
|
||
def pytest_configure(config): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think for completeness we should also implement There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I had that – but then the tests break, because the tests actually run a second pytest in the same process. That pytest will also call unconfigure, and the patching will be gone for all following tests. But see line 209 – that does exactly what you're asking for. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm a little uneasy of doing this wrapping automatically for everyone which uses the plugin... there are a tons of test suites, some of them doing already some deep dark magic on their on, who knows what this might break? I agree with you it is unlikely, but I think we should add an option to disable this. How about an ini option: [pytest]
mock_traceback_monkeypatch = false The default would be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I agree – it's quite agressive and I felt the pain of this while writing it. But as I can't see any other way to make sure that the patching is there when somebody uses mock in a test, I think a config option would probably be a good way to handle problems. I'll add one. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks! 😄 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Oops, missed that, thanks! |
||
if parse_ini_boolean(config.getini('mock_traceback_monkeypatch')): | ||
wrap_assert_methods(config) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't you just call the wrapped method directly, and let the
AssertionError
propagate? Like this:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would defeat the whole purpose :). Then the traceback would look exactly like it did before – the wrapper is hidden, but the
assert_not_called
frame is displayed.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh you are right, I was under the impression that having a local variable named
__tracebackhide__
would strip all frames below and including that frame, but it only hides the frame containing the variable so you have to raise a new exception at that point. Got it, thanks! 😄