Skip to content

Commit

Permalink
Merge branch 'master' into merge
Browse files Browse the repository at this point in the history
Conflicts:
	src/_pytest/main.py
  • Loading branch information
blueyed committed Feb 22, 2020
2 parents 831395d + 706ea86 commit 5178e3d
Show file tree
Hide file tree
Showing 7 changed files with 68 additions and 25 deletions.
3 changes: 3 additions & 0 deletions changelog/6752.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
When :py:func:`pytest.raises` is used as a function (as opposed to a context manager),
a `match` keyword argument is now passed through to the tested function. Previously
it was swallowed and ignored (regression in pytest 5.1.0).
33 changes: 20 additions & 13 deletions src/_pytest/capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,15 @@
import pytest
from _pytest.compat import CaptureAndPassthroughIO
from _pytest.compat import CaptureIO
from _pytest.compat import TYPE_CHECKING
from _pytest.config import Config
from _pytest.fixtures import FixtureRequest

if TYPE_CHECKING:
from typing_extensions import Literal

_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]

patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}


Expand Down Expand Up @@ -66,6 +72,18 @@ def pytest_load_initial_conftests(early_config: Config):
sys.stderr.write(err)


def _get_multicapture(method: "_CaptureMethod") -> "MultiCapture":
if method == "fd":
return MultiCapture(out=True, err=True, Capture=FDCapture)
elif method == "sys":
return MultiCapture(out=True, err=True, Capture=SysCapture)
elif method == "no":
return MultiCapture(out=False, err=False, in_=False)
elif method == "tee-sys":
return MultiCapture(out=True, err=True, in_=False, Capture=TeeSysCapture)
raise ValueError("unknown capturing method: {!r}".format(method))


class CaptureManager:
"""
Capture plugin, manages that the appropriate capture method is enabled/disabled during collection and each
Expand All @@ -79,7 +97,7 @@ class CaptureManager:
case special handling is needed to ensure the fixtures take precedence over the global capture.
"""

def __init__(self, method) -> None:
def __init__(self, method: "_CaptureMethod") -> None:
self._method = method
self._global_capturing = None
self._capture_fixture = None # type: Optional[CaptureFixture]
Expand All @@ -89,17 +107,6 @@ def __repr__(self):
self._method, self._global_capturing, self._capture_fixture
)

def _getcapture(self, method):
if method == "fd":
return MultiCapture(out=True, err=True, Capture=FDCapture)
elif method == "sys":
return MultiCapture(out=True, err=True, Capture=SysCapture)
elif method == "no":
return MultiCapture(out=False, err=False, in_=False)
elif method == "tee-sys":
return MultiCapture(out=True, err=True, in_=False, Capture=TeeSysCapture)
raise ValueError("unknown capturing method: %r" % method) # pragma: no cover

def is_capturing(self):
if self.is_globally_capturing():
return "global"
Expand All @@ -114,7 +121,7 @@ def is_globally_capturing(self):

def start_global_capturing(self):
assert self._global_capturing is None
self._global_capturing = self._getcapture(self._method)
self._global_capturing = _get_multicapture(self._method)
self._global_capturing.start_capturing()

def stop_global_capturing(self):
Expand Down
6 changes: 5 additions & 1 deletion src/_pytest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

if TYPE_CHECKING:
from typing import Type
from typing_extensions import Literal

from _pytest.python import Package

Expand Down Expand Up @@ -300,7 +301,9 @@ def _in_venv(path):
return any([fname.basename in activates for fname in bindir.listdir()])


def pytest_ignore_collect(path, config):
def pytest_ignore_collect(
path: py.path.local, config: Config
) -> "Optional[Literal[True]]":
ignore_paths = config._getconftest_pathlist("collect_ignore", path=path.dirpath())
ignore_paths = ignore_paths or []
excludeopt = config.getoption("ignore")
Expand All @@ -324,6 +327,7 @@ def pytest_ignore_collect(path, config):
allow_in_venv = config.getoption("collect_in_virtualenv")
if not allow_in_venv and _in_venv(path):
return True
return None


def pytest_collection_modifyitems(items, config):
Expand Down
13 changes: 8 additions & 5 deletions src/_pytest/python_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,18 +557,16 @@ def raises( # noqa: F811
expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]],
func: Callable,
*args: Any,
match: Optional[str] = ...,
**kwargs: Any
) -> Optional[_pytest._code.ExceptionInfo[_E]]:
) -> _pytest._code.ExceptionInfo[_E]:
... # pragma: no cover


def raises( # noqa: F811
expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]],
*args: Any,
match: Optional[Union[str, "Pattern"]] = None,
**kwargs: Any
) -> Union["RaisesContext[_E]", Optional[_pytest._code.ExceptionInfo[_E]]]:
) -> Union["RaisesContext[_E]", _pytest._code.ExceptionInfo[_E]]:
r"""
Assert that a code block/function call raises ``expected_exception``
or raise a failure exception otherwise.
Expand All @@ -579,8 +577,12 @@ def raises( # noqa: F811
string that may contain `special characters`__, the pattern can
first be escaped with ``re.escape``.
__ https://docs.python.org/3/library/re.html#regular-expression-syntax
(This is only used when ``pytest.raises`` is used as a context manager,
and passed through to the function otherwise.
When using ``pytest.raises`` as a function, you can use:
``pytest.raises(Exc, func, match="passed on").match("my pattern")``.)
__ https://docs.python.org/3/library/re.html#regular-expression-syntax
.. currentmodule:: _pytest._code
Expand Down Expand Up @@ -684,6 +686,7 @@ def raises( # noqa: F811
message = "DID NOT RAISE {}".format(expected_exception)

if not args:
match = kwargs.pop("match", None)
if kwargs:
msg = "Unexpected keyword arguments passed to pytest.raises: "
msg += ", ".join(sorted(kwargs))
Expand Down
25 changes: 21 additions & 4 deletions testing/python/raises.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
import sys

import pytest
Expand Down Expand Up @@ -154,7 +155,7 @@ def test_no_raise_message(self):
else:
assert False, "Expected pytest.raises.Exception"

@pytest.mark.parametrize("method", ["function", "with"])
@pytest.mark.parametrize("method", ["function", "function_match", "with"])
def test_raises_cyclic_reference(self, method):
"""
Ensure pytest.raises does not leave a reference cycle (#1965).
Expand All @@ -175,6 +176,8 @@ def __call__(self):

if method == "function":
pytest.raises(ValueError, t)
elif method == "function_match":
pytest.raises(ValueError, t).match("^$")
else:
with pytest.raises(ValueError):
t()
Expand All @@ -184,7 +187,7 @@ def __call__(self):

assert refcount == len(gc.get_referrers(t))

def test_raises_match(self):
def test_raises_match(self) -> None:
msg = r"with base \d+"
with pytest.raises(ValueError, match=msg):
int("asdf")
Expand All @@ -194,13 +197,27 @@ def test_raises_match(self):
int("asdf")

msg = "with base 16"
expr = r"Pattern '{}' does not match \"invalid literal for int\(\) with base 10: 'asdf'\"".format(
expr = "Pattern {!r} does not match \"invalid literal for int() with base 10: 'asdf'\"".format(
msg
)
with pytest.raises(AssertionError, match=expr):
with pytest.raises(AssertionError, match=re.escape(expr)):
with pytest.raises(ValueError, match=msg):
int("asdf", base=10)

# "match" without context manager.
pytest.raises(ValueError, int, "asdf").match("invalid literal")
with pytest.raises(AssertionError) as excinfo:
pytest.raises(ValueError, int, "asdf").match(msg)
assert str(excinfo.value) == expr

pytest.raises(TypeError, int, match="invalid")

def tfunc(match):
raise ValueError("match={}".format(match))

pytest.raises(ValueError, tfunc, match="asdf").match("match=asdf")
pytest.raises(ValueError, tfunc, match="").match("match=")

def test_match_failure_string_quoting(self):
with pytest.raises(AssertionError) as excinfo:
with pytest.raises(AssertionError, match="'foo"):
Expand Down
9 changes: 9 additions & 0 deletions testing/test_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@

import pytest
from _pytest import capture
from _pytest.capture import _get_multicapture
from _pytest.capture import CaptureManager
from _pytest.capture import MultiCapture
from _pytest.config import ExitCode

# note: py.io capture tests where copied from
Expand Down Expand Up @@ -1567,3 +1569,10 @@ def test_encodedfile_writelines(tmpfile: BinaryIO) -> None:
tmpfile.close()
with pytest.raises(ValueError):
ef.read()


def test__get_multicapture() -> None:
assert isinstance(_get_multicapture("fd"), MultiCapture)
pytest.raises(ValueError, _get_multicapture, "unknown").match(
r"^unknown capturing method: 'unknown'"
)
4 changes: 2 additions & 2 deletions testing/test_terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,9 @@ class TestMore(BaseTests): pass
"> if self.fail: assert 0",
"E assert 0",
"",
"tests/test_p1.py:5: AssertionError",
"tests/test_p1.py:5: assert 0",
"*= short test summary info =*",
"FAILED tests/test_p3.py::TestMore::test_p1 - assert 0",
"FAILED tests/test_p3.py::TestMore::test_p1 (tests/test_p1.py:5) - assert 0",
"*= 1 failed in *",
]
)
Expand Down

0 comments on commit 5178e3d

Please sign in to comment.