From f4a4c4444cc10381a764e4357263bc3e16b1f177 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 17 Feb 2020 14:38:26 +0100 Subject: [PATCH 1/7] Handle `match` with `pytest.raises()` Fixes https://github.com/pytest-dev/pytest/issues/6752. --- changelog/6752.bugfix.rst | 1 + src/_pytest/python_api.py | 9 +++++++++ testing/python/raises.py | 15 ++++++++++++--- 3 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 changelog/6752.bugfix.rst diff --git a/changelog/6752.bugfix.rst b/changelog/6752.bugfix.rst new file mode 100644 index 00000000000..cd849e60408 --- /dev/null +++ b/changelog/6752.bugfix.rst @@ -0,0 +1 @@ +Handle ``match`` with :py:func:`pytest.raises` used as a function. diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 24145016ce6..7f3c531e1ec 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -710,6 +710,15 @@ def raises( # noqa: F811 except expected_exception as e: # We just caught the exception - there is a traceback. assert e.__traceback__ is not None + if match: + excinfo = _pytest._code.ExceptionInfo.from_exc_info( + (type(e), e, e.__traceback__) + ) + excinfo.match(match) + try: + return excinfo + finally: + del excinfo return _pytest._code.ExceptionInfo.from_exc_info( (type(e), e, e.__traceback__) ) diff --git a/testing/python/raises.py b/testing/python/raises.py index a53b2137a5f..d0ed00375c8 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -1,3 +1,4 @@ +import re import sys import pytest @@ -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). @@ -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() @@ -194,13 +197,19 @@ 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 + def test_match_failure_string_quoting(self): with pytest.raises(AssertionError) as excinfo: with pytest.raises(AssertionError, match="'foo"): From fa78e27862801c56c3590c026f25a627017e27ec Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 17 Feb 2020 15:18:17 +0100 Subject: [PATCH 2/7] pass match to func --- changelog/6752.bugfix.rst | 3 ++- src/_pytest/python_api.py | 18 ++++++------------ testing/python/raises.py | 14 +++++++++++--- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/changelog/6752.bugfix.rst b/changelog/6752.bugfix.rst index cd849e60408..d498417c508 100644 --- a/changelog/6752.bugfix.rst +++ b/changelog/6752.bugfix.rst @@ -1 +1,2 @@ -Handle ``match`` with :py:func:`pytest.raises` used as a function. +Handle ``match`` with :py:func:`pytest.raises` used as a function, passing it to the function. +Previously it was ignored. diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 7f3c531e1ec..40571079adb 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -557,7 +557,6 @@ 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]]: ... # pragma: no cover @@ -566,7 +565,6 @@ def raises( # noqa: F811 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]]]: r""" @@ -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("my pattern")``.) + __ https://docs.python.org/3/library/re.html#regular-expression-syntax .. currentmodule:: _pytest._code @@ -693,6 +695,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)) @@ -710,15 +713,6 @@ def raises( # noqa: F811 except expected_exception as e: # We just caught the exception - there is a traceback. assert e.__traceback__ is not None - if match: - excinfo = _pytest._code.ExceptionInfo.from_exc_info( - (type(e), e, e.__traceback__) - ) - excinfo.match(match) - try: - return excinfo - finally: - del excinfo return _pytest._code.ExceptionInfo.from_exc_info( (type(e), e, e.__traceback__) ) diff --git a/testing/python/raises.py b/testing/python/raises.py index d0ed00375c8..7e7d875e11f 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -177,7 +177,7 @@ def __call__(self): if method == "function": pytest.raises(ValueError, t) elif method == "function_match": - pytest.raises(ValueError, t, match="^$") + pytest.raises(ValueError, t).match("^$") else: with pytest.raises(ValueError): t() @@ -205,11 +205,19 @@ def test_raises_match(self): int("asdf", base=10) # "match" without context manager. - pytest.raises(ValueError, int, "asdf", match="invalid literal") + pytest.raises(ValueError, int, "asdf").match("invalid literal") with pytest.raises(AssertionError) as excinfo: - pytest.raises(ValueError, int, "asdf", match=msg) + 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)) + + excinfo = pytest.raises(ValueError, tfunc, match="asdf").match("match=asdf") + excinfo = 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"): From c06d038d65b1b4c0d679462cbf907f8f39cca8ae Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 17 Feb 2020 15:37:03 +0100 Subject: [PATCH 3/7] update doc --- changelog/6752.bugfix.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/changelog/6752.bugfix.rst b/changelog/6752.bugfix.rst index d498417c508..4fb6ab63ea6 100644 --- a/changelog/6752.bugfix.rst +++ b/changelog/6752.bugfix.rst @@ -1,2 +1,3 @@ -Handle ``match`` with :py:func:`pytest.raises` used as a function, passing it to the function. -Previously it was ignored. +Pass `match` keyword argument to the tested function with +:py:func:`pytest.raises` used as a function - previously it was ignored +(regression in pytest 5.1.0). From 8dd5ae74d172013cd351176f2d5207fc8c3493f0 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 17 Feb 2020 15:39:02 +0100 Subject: [PATCH 4/7] no unicode for Windows pydoc --- src/_pytest/python_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 40571079adb..b8a6a9ed736 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -580,7 +580,7 @@ def raises( # noqa: F811 (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("my pattern")``.) + ``pytest.raises(Exc, func, match="passed on").match("my pattern")``.) __ https://docs.python.org/3/library/re.html#regular-expression-syntax From bdf40d172b7fb5b39f4cbdfd21010dcee46cff7d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 22 Feb 2020 13:59:49 +0100 Subject: [PATCH 5/7] update changelog --- changelog/6752.bugfix.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/changelog/6752.bugfix.rst b/changelog/6752.bugfix.rst index 4fb6ab63ea6..510ea8d7dc9 100644 --- a/changelog/6752.bugfix.rst +++ b/changelog/6752.bugfix.rst @@ -1,3 +1,3 @@ -Pass `match` keyword argument to the tested function with -:py:func:`pytest.raises` used as a function - previously it was ignored -(regression in pytest 5.1.0). +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). From 37a283bfffbebb552d508d7ec46eeabc79334ede Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 22 Feb 2020 14:03:31 +0100 Subject: [PATCH 6/7] fix typing for raises-func: does not return None --- src/_pytest/python_api.py | 2 +- testing/python/raises.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index b8a6a9ed736..ba113e5740c 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -558,7 +558,7 @@ def raises( # noqa: F811 func: Callable, *args: Any, **kwargs: Any -) -> Optional[_pytest._code.ExceptionInfo[_E]]: +) -> _pytest._code.ExceptionInfo[_E]: ... # pragma: no cover diff --git a/testing/python/raises.py b/testing/python/raises.py index 7e7d875e11f..6c607464d54 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -187,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") @@ -215,8 +215,8 @@ def test_raises_match(self): def tfunc(match): raise ValueError("match={}".format(match)) - excinfo = pytest.raises(ValueError, tfunc, match="asdf").match("match=asdf") - excinfo = pytest.raises(ValueError, tfunc, match="").match("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: From edf4e6abe2aaa694723fdc02aacc9bd66ab2c86c Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 22 Feb 2020 14:24:30 +0100 Subject: [PATCH 7/7] fixup! fix typing for raises-func: does not return None --- src/_pytest/python_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index ba113e5740c..d3692412def 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -566,7 +566,7 @@ def raises( # noqa: F811 expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]], *args: Any, **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.