Skip to content
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

Warn instead of raising exception in context manager #221

Merged
merged 6 commits into from Jan 4, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions src/pytest_mock/__init__.py
Expand Up @@ -5,6 +5,7 @@
__all__ = [
"MockerFixture",
"MockFixture",
"PytestMockWarning",
"pytest_addoption",
"pytest_configure",
"session_mocker",
Expand Down
58 changes: 53 additions & 5 deletions src/pytest_mock/plugin.py
Expand Up @@ -13,6 +13,8 @@
import asyncio
import functools
import inspect
import warnings
import sys

import pytest

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


class PytestMockWarning(UserWarning):
"""Base class for all warnings emitted by pytest-mock."""


class MockerFixture:
"""
Fixture that provides the same interface to functions in the mock module,
Expand Down Expand Up @@ -169,7 +175,7 @@ def __init__(self, patches, mocks, mock_module):
self.mock_module = mock_module

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

Expand All @@ -206,6 +220,37 @@ def object(
new = self.mock_module.DEFAULT
return self._start_patch(
self.mock_module.patch.object,
True,
target,
attribute,
new=new,
spec=spec,
create=create,
spec_set=spec_set,
autospec=autospec,
new_callable=new_callable,
**kwargs
)

def context_manager(
self,
target: object,
attribute: str,
new: object = DEFAULT,
spec: Optional[object] = None,
create: bool = False,
spec_set: Optional[object] = None,
autospec: Optional[object] = None,
new_callable: object = None,
**kwargs: Any
) -> unittest.mock.MagicMock:
"""This is equivalent to mock.patch.object except that the returned mock
does not issue a warning when used as a context manager."""
if new is self.DEFAULT:
new = self.mock_module.DEFAULT
return self._start_patch(
self.mock_module.patch.object,
False,
target,
attribute,
new=new,
Expand All @@ -230,6 +275,7 @@ def multiple(
"""API to mock.patch.multiple"""
return self._start_patch(
self.mock_module.patch.multiple,
True,
target,
spec=spec,
create=create,
Expand All @@ -249,6 +295,7 @@ def dict(
"""API to mock.patch.dict"""
return self._start_patch(
self.mock_module.patch.dict,
True,
in_dict,
values=values,
clear=clear,
Expand Down Expand Up @@ -328,6 +375,7 @@ def __call__(
new = self.mock_module.DEFAULT
return self._start_patch(
self.mock_module.patch,
True,
target,
new=new,
spec=spec,
Expand Down
58 changes: 42 additions & 16 deletions tests/test_pytest_mock.py
@@ -1,12 +1,13 @@
import os
import platform
import re
import sys
from contextlib import contextmanager
from typing import Callable, Any, Tuple, Generator, Type
from unittest.mock import MagicMock

import pytest
from pytest_mock import MockerFixture
from pytest_mock import MockerFixture, PytestMockWarning

pytest_plugins = "pytester"

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


def test_abort_patch_object_context_manager(mocker: MockerFixture) -> None:
def test_warn_patch_object_context_manager(mocker: MockerFixture) -> None:
class A:
def doIt(self):
return False

a = A()

with pytest.raises(ValueError) as excinfo:
with mocker.patch.object(a, "doIt", return_value=True):
assert a.doIt() is True

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

assert str(excinfo.value) == expected_error_msg
with pytest.warns(
PytestMockWarning, match=re.escape(expected_warning_msg)
) as warn_record:
with mocker.patch.object(a, "doIt", return_value=True):
assert a.doIt() is True

assert warn_record[0].filename == __file__

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

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

assert str(excinfo.value) == expected_error_msg
with pytest.warns(
PytestMockWarning, match=re.escape(expected_warning_msg)
) as warn_record:
with mocker.patch("json.loads"):
pass

assert warn_record[0].filename == __file__


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


def test_patch_context_manager_with_context_manager(mocker: MockerFixture) -> None:
"""Test that no warnings are issued when an object patched with
patch.context_manager is used as a context manager (#221)"""

class A:
def doIt(self):
return False

a = A()

with pytest.warns(None) as warn_record:
with mocker.patch.context_manager(a, "doIt", return_value=True):
assert a.doIt() is True

assert len(warn_record) == 0


def test_abort_patch_context_manager_with_stale_pyc(testdir: Any) -> None:
"""Ensure we don't trigger an error in case the frame where mocker.patch is being
used doesn't have a 'context' (#169)"""
Expand Down