From d8ecca5ebd929f1be49033f8a06eed97494af3da Mon Sep 17 00:00:00 2001 From: Joan Massich Date: Tue, 22 Aug 2017 13:48:29 +0200 Subject: [PATCH 1/4] Add test to design warns signature in TDD mimicking raises signature --- testing/test_recwarn.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 6895b1140..e4e7934cc 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -284,3 +284,25 @@ class TestWarns(object): ''') result = testdir.runpytest() result.stdout.fnmatch_lines(['*2 passed in*']) + + def test_match_regex(self): + with pytest.warns(UserWarning, match=r'must be \d+$'): + warnings.warn("value must be 42", UserWarning) + + with pytest.raises(AssertionError, match='pattern not found'): + with pytest.warns(UserWarning, match=r'must be \d+$'): + warnings.warn("this is not here", UserWarning) + + def test_one_from_multiple_warns(): + with warns(UserWarning, match=r'aaa'): + warnings.warn("cccccccccc", UserWarning) + warnings.warn("bbbbbbbbbb", UserWarning) + warnings.warn("aaaaaaaaaa", UserWarning) + + def test_none_of_multiple_warns(): + a, b, c = ('aaa', 'bbbbbbbbbb', 'cccccccccc') + expected_msg = "'{}' pattern not found in \['{}', '{}'\]".format(a, b, c) + with raises(AssertionError, match=expected_msg): + with warns(UserWarning, match=r'aaa'): + warnings.warn("bbbbbbbbbb", UserWarning) + warnings.warn("cccccccccc", UserWarning) From aa6a67044f1620797ac6dee3faa4e77368970c97 Mon Sep 17 00:00:00 2001 From: Joan Massich Date: Wed, 6 Sep 2017 01:13:08 +0200 Subject: [PATCH 2/4] Add match_regex functionality to warns --- _pytest/recwarn.py | 27 ++++++++++++++++++++++----- testing/test_recwarn.py | 18 ++++++++++-------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/_pytest/recwarn.py b/_pytest/recwarn.py index c9fa872c0..4f7efba99 100644 --- a/_pytest/recwarn.py +++ b/_pytest/recwarn.py @@ -8,6 +8,8 @@ import py import sys import warnings +import re + from _pytest.fixtures import yield_fixture from _pytest.outcomes import fail @@ -99,9 +101,11 @@ def warns(expected_warning, *args, **kwargs): >>> with warns(RuntimeWarning): ... warnings.warn("my warning", RuntimeWarning) """ - wcheck = WarningsChecker(expected_warning) + match_expr = None if not args: - return wcheck + if "match" in kwargs: + match_expr = kwargs.pop("match") + return WarningsChecker(expected_warning, match_expr=match_expr) elif isinstance(args[0], str): code, = args assert isinstance(code, str) @@ -109,12 +113,12 @@ def warns(expected_warning, *args, **kwargs): loc = frame.f_locals.copy() loc.update(kwargs) - with wcheck: + with WarningsChecker(expected_warning, match_expr=match_expr): code = _pytest._code.Source(code).compile() py.builtin.exec_(code, frame.f_globals, loc) else: func = args[0] - with wcheck: + with WarningsChecker(expected_warning, match_expr=match_expr): return func(*args[1:], **kwargs) @@ -174,7 +178,7 @@ class WarningsRecorder(warnings.catch_warnings): class WarningsChecker(WarningsRecorder): - def __init__(self, expected_warning=None): + def __init__(self, expected_warning=None, match_expr=None): super(WarningsChecker, self).__init__() msg = ("exceptions must be old-style classes or " @@ -189,6 +193,7 @@ class WarningsChecker(WarningsRecorder): raise TypeError(msg % type(expected_warning)) self.expected_warning = expected_warning + self.match_expr = match_expr def __exit__(self, *exc_info): super(WarningsChecker, self).__exit__(*exc_info) @@ -203,3 +208,15 @@ class WarningsChecker(WarningsRecorder): "The list of emitted warnings is: {1}.".format( self.expected_warning, [each.message for each in self])) + elif self.match_expr is not None: + for r in self: + if issubclass(r.category, self.expected_warning): + if re.compile(self.match_expr).search(str(r.message)): + break + else: + fail("DID NOT WARN. No warnings of type {0} matching" + " ('{1}') was emitted. The list of emitted warnings" + " is: {2}.".format( + self.expected_warning, + self.match_expr, + [each.message for each in self])) diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index e4e7934cc..ca4023f66 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -289,20 +289,22 @@ class TestWarns(object): with pytest.warns(UserWarning, match=r'must be \d+$'): warnings.warn("value must be 42", UserWarning) - with pytest.raises(AssertionError, match='pattern not found'): + with pytest.raises(pytest.fail.Exception): with pytest.warns(UserWarning, match=r'must be \d+$'): warnings.warn("this is not here", UserWarning) - def test_one_from_multiple_warns(): - with warns(UserWarning, match=r'aaa'): + with pytest.raises(pytest.fail.Exception): + with pytest.warns(FutureWarning, match=r'must be \d+$'): + warnings.warn("value must be 42", UserWarning) + + def test_one_from_multiple_warns(self): + with pytest.warns(UserWarning, match=r'aaa'): warnings.warn("cccccccccc", UserWarning) warnings.warn("bbbbbbbbbb", UserWarning) warnings.warn("aaaaaaaaaa", UserWarning) - def test_none_of_multiple_warns(): - a, b, c = ('aaa', 'bbbbbbbbbb', 'cccccccccc') - expected_msg = "'{}' pattern not found in \['{}', '{}'\]".format(a, b, c) - with raises(AssertionError, match=expected_msg): - with warns(UserWarning, match=r'aaa'): + def test_none_of_multiple_warns(self): + with pytest.raises(pytest.fail.Exception): + with pytest.warns(UserWarning, match=r'aaa'): warnings.warn("bbbbbbbbbb", UserWarning) warnings.warn("cccccccccc", UserWarning) From 80d165475b4c1ef1f70ed01db3b7b08b627cb91b Mon Sep 17 00:00:00 2001 From: Joan Massich Date: Thu, 7 Sep 2017 10:28:52 +0200 Subject: [PATCH 3/4] Add documentation --- _pytest/recwarn.py | 16 ++++++++++++++++ doc/en/warnings.rst | 15 ++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/_pytest/recwarn.py b/_pytest/recwarn.py index 4f7efba99..c9f86a483 100644 --- a/_pytest/recwarn.py +++ b/_pytest/recwarn.py @@ -100,6 +100,22 @@ def warns(expected_warning, *args, **kwargs): >>> with warns(RuntimeWarning): ... warnings.warn("my warning", RuntimeWarning) + + In the context manager form you may use the keyword argument ``match`` to assert + that the exception matches a text or regex:: + + >>> with warns(UserWarning, match='must be 0 or None'): + ... warnings.warn("value must be 0 or None", UserWarning) + + >>> with warns(UserWarning, match=r'must be \d+$'): + ... warnings.warn("value must be 42", UserWarning) + + >>> with warns(UserWarning, match=r'must be \d+$'): + ... warnings.warn("this is not here", UserWarning) + Traceback (most recent call last): + ... + Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted... + """ match_expr = None if not args: diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index c84277173..ac26068c4 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -168,7 +168,20 @@ which works in a similar manner to :ref:`raises `:: with pytest.warns(UserWarning): warnings.warn("my warning", UserWarning) -The test will fail if the warning in question is not raised. +The test will fail if the warning in question is not raised. The keyword +argument ``match`` to assert that the exception matches a text or regex:: + + >>> with warns(UserWarning, match='must be 0 or None'): + ... warnings.warn("value must be 0 or None", UserWarning) + + >>> with warns(UserWarning, match=r'must be \d+$'): + ... warnings.warn("value must be 42", UserWarning) + + >>> with warns(UserWarning, match=r'must be \d+$'): + ... warnings.warn("this is not here", UserWarning) + Traceback (most recent call last): + ... + Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted... You can also call ``pytest.warns`` on a function or code string:: From a0c6758202c518d7e6c1726a850e6b7e6e0a3a74 Mon Sep 17 00:00:00 2001 From: Joan Massich Date: Thu, 7 Sep 2017 10:51:14 +0200 Subject: [PATCH 4/4] Add changelog --- changelog/2708.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/2708.feature diff --git a/changelog/2708.feature b/changelog/2708.feature new file mode 100644 index 000000000..f6039ede9 --- /dev/null +++ b/changelog/2708.feature @@ -0,0 +1 @@ +Match ``warns`` signature to ``raises`` by adding ``match`` keyworkd. \ No newline at end of file