Skip to content

Commit e554aca

Browse files
authored
Merge branch 'master' into rm-3.4
2 parents e25817f + 7bddcd5 commit e554aca

File tree

5 files changed

+150
-6
lines changed

5 files changed

+150
-6
lines changed

.github/workflows/main.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
strategy:
1111
fail-fast: false
1212
matrix:
13-
python: ["2.7", "3.5", "3.6", "3.7"]
13+
python: ["2.7", "3.5", "3.6", "3.7", "3.8"]
1414
os: [ubuntu-latest, windows-latest]
1515
include:
1616
- python: "2.7"
@@ -21,6 +21,8 @@ jobs:
2121
tox_env: "py36"
2222
- python: "3.7"
2323
tox_env: "py37"
24+
- python: "3.8"
25+
tox_env: "py38"
2426

2527
steps:
2628
- uses: actions/checkout@v1

CHANGELOG.rst

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,39 @@
1-
1.11.2 (unreleased)
1+
1.13.0 (2019-12-05)
2+
-------------------
3+
4+
* The object returned by ``mocker.spy`` now also tracks any side effect
5+
of the spied method/function.
6+
7+
1.12.1 (2019-11-20)
8+
-------------------
9+
10+
* Fix error if ``mocker.patch`` is used in code where the source file
11+
is not available, for example stale ``.pyc`` files (`#169`_).
12+
13+
.. _#169: https://github.com/pytest-dev/pytest-mock/issues/169#issuecomment-555729265
14+
15+
1.12.0 (2019-11-19)
16+
-------------------
17+
18+
* Now all patch functions also raise a ``ValueError`` when used
19+
as a context-manager. Thanks `@AlexGascon`_ for the PR (`#168`_).
20+
21+
.. _@AlexGascon: https://github.com/AlexGascon
22+
.. _#168: https://github.com/pytest-dev/pytest-mock/pull/168
23+
24+
1.11.2 (2019-10-19)
225
-------------------
326

427
* The *pytest introspection follows* message is no longer shown
528
if there is no pytest introspection (`#154`_).
629
Thanks `@The-Compiler`_ for the report.
730

31+
* ``mocker`` now raises a ``ValueError`` when used as a context-manager.
32+
Thanks `@binarymason`_ for the PR (`#165`_).
33+
834
.. _#154: https://github.com/pytest-dev/pytest-mock/issues/154
35+
.. _#165: https://github.com/pytest-dev/pytest-mock/pull/165
36+
.. _@binarymason: https://github.com/binarymason
937

1038
1.11.1 (2019-10-04)
1139
-------------------

README.rst

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ of a test:
3535
.. |ci| image:: https://github.com/pytest-dev/pytest-mock/workflows/build/badge.svg
3636
:target: https://github.com/pytest-dev/pytest-mock/actions
3737

38-
.. |coverage| image:: http://img.shields.io/coveralls/pytest-dev/pytest-mock.svg
39-
:target: https://coveralls.io/r/pytest-dev/pytest-mock
38+
.. |coverage| image:: https://coveralls.io/repos/github/pytest-dev/pytest-mock/badge.svg?branch=master
39+
:target: https://coveralls.io/github/pytest-dev/pytest-mock?branch=master
4040

4141
.. |python| image:: https://img.shields.io/pypi/pyversions/pytest-mock.svg
4242
:target: https://pypi.python.org/pypi/pytest-mock/
@@ -103,6 +103,9 @@ features with it, like retrieving call count. It also works for class and static
103103
Since version ``1.11``, it is also possible to query the ``return_value`` attribute
104104
to observe what the spied function/method returned.
105105

106+
Since version ``1.13``, it is also possible to query the ``side_effect`` attribute
107+
to observe any exception thrown by the spied function/method.
108+
106109
Stub
107110
----
108111

src/pytest_mock/plugin.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,13 @@ def spy(self, obj, name):
113113

114114
@w
115115
def wrapper(*args, **kwargs):
116-
r = method(*args, **kwargs)
117-
result.return_value = r
116+
try:
117+
r = method(*args, **kwargs)
118+
except Exception as e:
119+
result.side_effect = e
120+
raise
121+
else:
122+
result.return_value = r
118123
return r
119124

120125
result = self.patch.object(obj, name, side_effect=wrapper, autospec=autospec)
@@ -147,13 +152,30 @@ def _start_patch(self, mock_func, *args, **kwargs):
147152
module, registering the patch to stop it later and returns the
148153
mock object resulting from the mock call.
149154
"""
155+
self._enforce_no_with_context(inspect.stack())
150156
p = mock_func(*args, **kwargs)
151157
mocked = p.start()
152158
self._patches.append(p)
153159
if hasattr(mocked, "reset_mock"):
154160
self._mocks.append(mocked)
155161
return mocked
156162

163+
def _enforce_no_with_context(self, stack):
164+
"""raises a ValueError if mocker is used in a with context"""
165+
caller = stack[2]
166+
frame = caller[0]
167+
info = inspect.getframeinfo(frame)
168+
if info.code_context is None:
169+
# no source code available (#169)
170+
return
171+
code_context = " ".join(info.code_context).strip()
172+
173+
if code_context.startswith("with mocker."):
174+
raise ValueError(
175+
"Using mocker in a with context is not supported. "
176+
"https://github.com/pytest-dev/pytest-mock#note-about-usage-as-context-manager"
177+
)
178+
157179
def object(self, *args, **kwargs):
158180
"""API to mock.patch.object"""
159181
return self._start_patch(self.mock_module.patch.object, *args, **kwargs)

tests/test_pytest_mock.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,25 @@ def bar(self, arg):
243243
assert spy.return_value == 20
244244

245245

246+
def test_instance_method_spy_exception(mocker):
247+
excepted_message = "foo"
248+
249+
class Foo(object):
250+
def bar(self, arg):
251+
raise Exception(excepted_message)
252+
253+
foo = Foo()
254+
other = Foo()
255+
spy = mocker.spy(foo, "bar")
256+
257+
with pytest.raises(Exception) as exc_info:
258+
foo.bar(10)
259+
assert str(exc_info.value) == excepted_message
260+
261+
foo.bar.assert_called_once_with(arg=10)
262+
assert spy.side_effect == exc_info.value
263+
264+
246265
@skip_pypy
247266
def test_instance_method_by_class_spy(mocker):
248267
class Foo(object):
@@ -717,3 +736,73 @@ def test_get_random_number(mocker):
717736
result = testdir.runpytest_subprocess()
718737
result.stdout.fnmatch_lines("* 1 passed in *")
719738
assert "RuntimeError" not in result.stderr.str()
739+
740+
741+
def test_abort_patch_object_context_manager(mocker):
742+
class A(object):
743+
def doIt(self):
744+
return False
745+
746+
a = A()
747+
748+
with pytest.raises(ValueError) as excinfo:
749+
with mocker.patch.object(a, "doIt", return_value=True):
750+
assert a.doIt() == True
751+
752+
expected_error_msg = (
753+
"Using mocker in a with context is not supported. "
754+
"https://github.com/pytest-dev/pytest-mock#note-about-usage-as-context-manager"
755+
)
756+
757+
assert str(excinfo.value) == expected_error_msg
758+
759+
760+
def test_abort_patch_context_manager(mocker):
761+
with pytest.raises(ValueError) as excinfo:
762+
with mocker.patch("some_package"):
763+
pass
764+
765+
expected_error_msg = (
766+
"Using mocker in a with context is not supported. "
767+
"https://github.com/pytest-dev/pytest-mock#note-about-usage-as-context-manager"
768+
)
769+
770+
assert str(excinfo.value) == expected_error_msg
771+
772+
773+
def test_abort_patch_context_manager_with_stale_pyc(testdir):
774+
"""Ensure we don't trigger an error in case the frame where mocker.patch is being
775+
used doesn't have a 'context' (#169)"""
776+
import compileall
777+
778+
py_fn = testdir.makepyfile(
779+
c="""
780+
class C:
781+
x = 1
782+
783+
def check(mocker):
784+
mocker.patch.object(C, "x", 2)
785+
assert C.x == 2
786+
"""
787+
)
788+
testdir.syspathinsert()
789+
790+
testdir.makepyfile(
791+
"""
792+
from c import check
793+
def test_foo(mocker):
794+
check(mocker)
795+
"""
796+
)
797+
result = testdir.runpytest()
798+
result.stdout.fnmatch_lines("* 1 passed *")
799+
800+
kwargs = {"legacy": True} if sys.version_info[0] >= 3 else {}
801+
assert compileall.compile_file(str(py_fn), **kwargs)
802+
803+
pyc_fn = str(py_fn) + "c"
804+
assert os.path.isfile(pyc_fn)
805+
806+
py_fn.remove()
807+
result = testdir.runpytest()
808+
result.stdout.fnmatch_lines("* 1 passed *")

0 commit comments

Comments
 (0)