Skip to content

Commit f623fa8

Browse files
iforapsynicoddemus
andauthored
Warn instead of raising exception in context manager (#221)
Co-authored-by: Bruno Oliveira <[email protected]>
1 parent 5f6cab7 commit f623fa8

File tree

3 files changed

+96
-21
lines changed

3 files changed

+96
-21
lines changed

src/pytest_mock/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
__all__ = [
66
"MockerFixture",
77
"MockFixture",
8+
"PytestMockWarning",
89
"pytest_addoption",
910
"pytest_configure",
1011
"session_mocker",

src/pytest_mock/plugin.py

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import asyncio
1414
import functools
1515
import inspect
16+
import warnings
17+
import sys
1618

1719
import pytest
1820

@@ -39,6 +41,10 @@ def _get_mock_module(config):
3941
return _get_mock_module._module
4042

4143

44+
class PytestMockWarning(UserWarning):
45+
"""Base class for all warnings emitted by pytest-mock."""
46+
47+
4248
class MockerFixture:
4349
"""
4450
Fixture that provides the same interface to functions in the mock module,
@@ -169,7 +175,7 @@ def __init__(self, patches, mocks, mock_module):
169175
self.mock_module = mock_module
170176

171177
def _start_patch(
172-
self, mock_func: Any, *args: Any, **kwargs: Any
178+
self, mock_func: Any, warn_on_mock_enter: bool, *args: Any, **kwargs: Any
173179
) -> unittest.mock.MagicMock:
174180
"""Patches something by calling the given function from the mock
175181
module, registering the patch to stop it later and returns the
@@ -182,10 +188,18 @@ def _start_patch(
182188
self._mocks.append(mocked)
183189
# check if `mocked` is actually a mock object, as depending on autospec or target
184190
# parameters `mocked` can be anything
185-
if hasattr(mocked, "__enter__"):
186-
mocked.__enter__.side_effect = ValueError(
187-
"Using mocker in a with context is not supported. "
188-
"https://github.com/pytest-dev/pytest-mock#note-about-usage-as-context-manager"
191+
if hasattr(mocked, "__enter__") and warn_on_mock_enter:
192+
if sys.version_info >= (3, 8):
193+
depth = 5
194+
else:
195+
depth = 4
196+
mocked.__enter__.side_effect = lambda: warnings.warn(
197+
"Mocks returned by pytest-mock do not need to be used as context managers. "
198+
"The mocker fixture automatically undoes mocking at the end of a test. "
199+
"This warning can be ignored if it was triggered by mocking a context manager. "
200+
"https://github.com/pytest-dev/pytest-mock#note-about-usage-as-context-manager",
201+
PytestMockWarning,
202+
stacklevel=depth,
189203
)
190204
return mocked
191205

@@ -206,6 +220,37 @@ def object(
206220
new = self.mock_module.DEFAULT
207221
return self._start_patch(
208222
self.mock_module.patch.object,
223+
True,
224+
target,
225+
attribute,
226+
new=new,
227+
spec=spec,
228+
create=create,
229+
spec_set=spec_set,
230+
autospec=autospec,
231+
new_callable=new_callable,
232+
**kwargs
233+
)
234+
235+
def context_manager(
236+
self,
237+
target: builtins.object,
238+
attribute: str,
239+
new: builtins.object = DEFAULT,
240+
spec: Optional[builtins.object] = None,
241+
create: bool = False,
242+
spec_set: Optional[builtins.object] = None,
243+
autospec: Optional[builtins.object] = None,
244+
new_callable: builtins.object = None,
245+
**kwargs: Any
246+
) -> unittest.mock.MagicMock:
247+
"""This is equivalent to mock.patch.object except that the returned mock
248+
does not issue a warning when used as a context manager."""
249+
if new is self.DEFAULT:
250+
new = self.mock_module.DEFAULT
251+
return self._start_patch(
252+
self.mock_module.patch.object,
253+
False,
209254
target,
210255
attribute,
211256
new=new,
@@ -230,6 +275,7 @@ def multiple(
230275
"""API to mock.patch.multiple"""
231276
return self._start_patch(
232277
self.mock_module.patch.multiple,
278+
True,
233279
target,
234280
spec=spec,
235281
create=create,
@@ -249,6 +295,7 @@ def dict(
249295
"""API to mock.patch.dict"""
250296
return self._start_patch(
251297
self.mock_module.patch.dict,
298+
True,
252299
in_dict,
253300
values=values,
254301
clear=clear,
@@ -328,6 +375,7 @@ def __call__(
328375
new = self.mock_module.DEFAULT
329376
return self._start_patch(
330377
self.mock_module.patch,
378+
True,
331379
target,
332380
new=new,
333381
spec=spec,

tests/test_pytest_mock.py

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import os
22
import platform
3+
import re
34
import sys
45
from contextlib import contextmanager
56
from typing import Callable, Any, Tuple, Generator, Type
67
from unittest.mock import MagicMock
78

89
import pytest
9-
from pytest_mock import MockerFixture
10+
from pytest_mock import MockerFixture, PytestMockWarning
1011

1112
pytest_plugins = "pytester"
1213

@@ -806,36 +807,44 @@ def test_get_random_number(mocker):
806807
assert "RuntimeError" not in result.stderr.str()
807808

808809

809-
def test_abort_patch_object_context_manager(mocker: MockerFixture) -> None:
810+
def test_warn_patch_object_context_manager(mocker: MockerFixture) -> None:
810811
class A:
811812
def doIt(self):
812813
return False
813814

814815
a = A()
815816

816-
with pytest.raises(ValueError) as excinfo:
817-
with mocker.patch.object(a, "doIt", return_value=True):
818-
assert a.doIt() is True
819-
820-
expected_error_msg = (
821-
"Using mocker in a with context is not supported. "
817+
expected_warning_msg = (
818+
"Mocks returned by pytest-mock do not need to be used as context managers. "
819+
"The mocker fixture automatically undoes mocking at the end of a test. "
820+
"This warning can be ignored if it was triggered by mocking a context manager. "
822821
"https://github.com/pytest-dev/pytest-mock#note-about-usage-as-context-manager"
823822
)
824823

825-
assert str(excinfo.value) == expected_error_msg
824+
with pytest.warns(
825+
PytestMockWarning, match=re.escape(expected_warning_msg)
826+
) as warn_record:
827+
with mocker.patch.object(a, "doIt", return_value=True):
828+
assert a.doIt() is True
826829

830+
assert warn_record[0].filename == __file__
827831

828-
def test_abort_patch_context_manager(mocker: MockerFixture) -> None:
829-
with pytest.raises(ValueError) as excinfo:
830-
with mocker.patch("json.loads"):
831-
pass
832832

833-
expected_error_msg = (
834-
"Using mocker in a with context is not supported. "
833+
def test_warn_patch_context_manager(mocker: MockerFixture) -> None:
834+
expected_warning_msg = (
835+
"Mocks returned by pytest-mock do not need to be used as context managers. "
836+
"The mocker fixture automatically undoes mocking at the end of a test. "
837+
"This warning can be ignored if it was triggered by mocking a context manager. "
835838
"https://github.com/pytest-dev/pytest-mock#note-about-usage-as-context-manager"
836839
)
837840

838-
assert str(excinfo.value) == expected_error_msg
841+
with pytest.warns(
842+
PytestMockWarning, match=re.escape(expected_warning_msg)
843+
) as warn_record:
844+
with mocker.patch("json.loads"):
845+
pass
846+
847+
assert warn_record[0].filename == __file__
839848

840849

841850
def test_context_manager_patch_example(mocker: MockerFixture) -> None:
@@ -858,6 +867,23 @@ def my_func():
858867
assert isinstance(my_func(), mocker.MagicMock)
859868

860869

870+
def test_patch_context_manager_with_context_manager(mocker: MockerFixture) -> None:
871+
"""Test that no warnings are issued when an object patched with
872+
patch.context_manager is used as a context manager (#221)"""
873+
874+
class A:
875+
def doIt(self):
876+
return False
877+
878+
a = A()
879+
880+
with pytest.warns(None) as warn_record:
881+
with mocker.patch.context_manager(a, "doIt", return_value=True):
882+
assert a.doIt() is True
883+
884+
assert len(warn_record) == 0
885+
886+
861887
def test_abort_patch_context_manager_with_stale_pyc(testdir: Any) -> None:
862888
"""Ensure we don't trigger an error in case the frame where mocker.patch is being
863889
used doesn't have a 'context' (#169)"""

0 commit comments

Comments
 (0)