From aeb92accb2521f1f1ccc4faaf9e6f6bd36e04d8b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 10 Oct 2018 08:00:59 -0700 Subject: [PATCH 1/3] Implement pytest.deprecated_call with pytest.warns --- changelog/4102.feature.rst | 1 + src/_pytest/recwarn.py | 44 +++++--------------------------------- testing/test_recwarn.py | 21 ++++++++++++------ 3 files changed, 20 insertions(+), 46 deletions(-) create mode 100644 changelog/4102.feature.rst diff --git a/changelog/4102.feature.rst b/changelog/4102.feature.rst new file mode 100644 index 00000000000..b7916c841bc --- /dev/null +++ b/changelog/4102.feature.rst @@ -0,0 +1 @@ +Reimplement ``pytest.deprecated_call`` using ``pytest.warns`` so it supports the ``match='...'`` keyword argument. diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 7a058669718..c0121f7c1b4 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -43,45 +43,10 @@ def deprecated_call(func=None, *args, **kwargs): in which case it will ensure calling ``func(*args, **kwargs)`` produces one of the warnings types above. """ - if not func: - return _DeprecatedCallContext() - else: - __tracebackhide__ = True - with _DeprecatedCallContext(): - return func(*args, **kwargs) - - -class _DeprecatedCallContext(object): - """Implements the logic to capture deprecation warnings as a context manager.""" - - def __enter__(self): - self._captured_categories = [] - self._old_warn = warnings.warn - self._old_warn_explicit = warnings.warn_explicit - warnings.warn_explicit = self._warn_explicit - warnings.warn = self._warn - - def _warn_explicit(self, message, category, *args, **kwargs): - self._captured_categories.append(category) - - def _warn(self, message, category=None, *args, **kwargs): - if isinstance(message, Warning): - self._captured_categories.append(message.__class__) - else: - self._captured_categories.append(category) - - def __exit__(self, exc_type, exc_val, exc_tb): - warnings.warn_explicit = self._old_warn_explicit - warnings.warn = self._old_warn - - if exc_type is None: - deprecation_categories = (DeprecationWarning, PendingDeprecationWarning) - if not any( - issubclass(c, deprecation_categories) for c in self._captured_categories - ): - __tracebackhide__ = True - msg = "Did not produce DeprecationWarning or PendingDeprecationWarning" - raise AssertionError(msg) + __tracebackhide__ = True + if func is not None: + args = (func,) + args + return warns((DeprecationWarning, PendingDeprecationWarning), *args, **kwargs) def warns(expected_warning, *args, **kwargs): @@ -116,6 +81,7 @@ def warns(expected_warning, *args, **kwargs): Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted... """ + __tracebackhide__ = True match_expr = None if not args: if "match" in kwargs: diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 82bd66c555c..897f4c5e8ca 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -76,9 +76,8 @@ def dep_explicit(self, i): ) def test_deprecated_call_raises(self): - with pytest.raises(AssertionError) as excinfo: + with pytest.raises(pytest.fail.Exception, match="No warnings of type"): pytest.deprecated_call(self.dep, 3, 5) - assert "Did not produce" in str(excinfo) def test_deprecated_call(self): pytest.deprecated_call(self.dep, 0, 5) @@ -100,7 +99,7 @@ def test_deprecated_call_preserves(self): assert warn_explicit is warnings.warn_explicit def test_deprecated_explicit_call_raises(self): - with pytest.raises(AssertionError): + with pytest.raises(pytest.fail.Exception): pytest.deprecated_call(self.dep_explicit, 3) def test_deprecated_explicit_call(self): @@ -116,8 +115,8 @@ def test_deprecated_call_no_warning(self, mode): def f(): pass - msg = "Did not produce DeprecationWarning or PendingDeprecationWarning" - with pytest.raises(AssertionError, match=msg): + msg = "No warnings of type (.*DeprecationWarning.*, .*PendingDeprecationWarning.*)" + with pytest.raises(pytest.fail.Exception, match=msg): if mode == "call": pytest.deprecated_call(f) else: @@ -179,12 +178,20 @@ def test_deprecated_call_specificity(self): def f(): warnings.warn(warning("hi")) - with pytest.raises(AssertionError): + with pytest.raises(pytest.fail.Exception): pytest.deprecated_call(f) - with pytest.raises(AssertionError): + with pytest.raises(pytest.fail.Exception): with pytest.deprecated_call(): f() + def test_deprecated_call_supports_match(self): + with pytest.deprecated_call(match=r"must be \d+$"): + warnings.warn("value must be 42", DeprecationWarning) + + with pytest.raises(pytest.fail.Exception): + with pytest.deprecated_call(match=r"must be \d+$"): + warnings.warn("this is not here", DeprecationWarning) + class TestWarns(object): def test_strings(self): From e0f6fce9e9213d5beda7f54e800a72ac95cbb3b6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 10 Oct 2018 09:36:19 -0700 Subject: [PATCH 2/3] In python2, display previously warned warnings --- changelog/4012.bugfix.rst | 1 + src/_pytest/recwarn.py | 13 +++++++++++++ testing/test_recwarn.py | 10 ++++++++++ 3 files changed, 24 insertions(+) create mode 100644 changelog/4012.bugfix.rst diff --git a/changelog/4012.bugfix.rst b/changelog/4012.bugfix.rst new file mode 100644 index 00000000000..11d8deef702 --- /dev/null +++ b/changelog/4012.bugfix.rst @@ -0,0 +1 @@ +``pytest.warn`` will capture previously-warned warnings in python2. Previously they were never raised. diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index c0121f7c1b4..592abdf5bc1 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -149,12 +149,25 @@ def __enter__(self): raise RuntimeError("Cannot enter %r twice" % self) self._list = super(WarningsRecorder, self).__enter__() warnings.simplefilter("always") + # python3 keeps track of a "filter version", when the filters are + # updated previously seen warnings can be re-warned. python2 has no + # concept of this so we must reset the warnings registry manually. + # trivial patching of `warnings.warn` seems to be enough somehow? + if six.PY2: + + def warn(*args, **kwargs): + return self._warn(*args, **kwargs) + + warnings.warn, self._warn = warn, warnings.warn return self def __exit__(self, *exc_info): if not self._entered: __tracebackhide__ = True raise RuntimeError("Cannot exit %r without entering first" % self) + # see above where `self.mp` is assigned + if six.PY2: + warnings.warn = self._warn super(WarningsRecorder, self).__exit__(*exc_info) diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 897f4c5e8ca..3ae5432483e 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -350,3 +350,13 @@ def test_none_of_multiple_warns(self): with pytest.warns(UserWarning, match=r"aaa"): warnings.warn("bbbbbbbbbb", UserWarning) warnings.warn("cccccccccc", UserWarning) + + @pytest.mark.filterwarnings("ignore") + def test_can_capture_previously_warned(self): + def f(): + warnings.warn(UserWarning("ohai")) + return 10 + + assert f() == 10 + assert pytest.warns(UserWarning, f) == 10 + assert pytest.warns(UserWarning, f) == 10 From bf265a424d1d146978d3847142d543b46cc0f117 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 10 Oct 2018 19:35:49 -0300 Subject: [PATCH 3/3] Minor adjustments found during code review --- changelog/4012.bugfix.rst | 1 - changelog/4102.bugfix.rst | 1 + changelog/4102.feature.rst | 3 +++ src/_pytest/recwarn.py | 8 ++++---- 4 files changed, 8 insertions(+), 5 deletions(-) delete mode 100644 changelog/4012.bugfix.rst create mode 100644 changelog/4102.bugfix.rst diff --git a/changelog/4012.bugfix.rst b/changelog/4012.bugfix.rst deleted file mode 100644 index 11d8deef702..00000000000 --- a/changelog/4012.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -``pytest.warn`` will capture previously-warned warnings in python2. Previously they were never raised. diff --git a/changelog/4102.bugfix.rst b/changelog/4102.bugfix.rst new file mode 100644 index 00000000000..dd066c38d92 --- /dev/null +++ b/changelog/4102.bugfix.rst @@ -0,0 +1 @@ +``pytest.warn`` will capture previously-warned warnings in Python 2. Previously they were never raised. diff --git a/changelog/4102.feature.rst b/changelog/4102.feature.rst index b7916c841bc..ee43ddc2411 100644 --- a/changelog/4102.feature.rst +++ b/changelog/4102.feature.rst @@ -1 +1,4 @@ Reimplement ``pytest.deprecated_call`` using ``pytest.warns`` so it supports the ``match='...'`` keyword argument. + +This has the side effect that ``pytest.deprecated_call`` now raises ``pytest.fail.Exception`` instead +of ``AssertionError``. diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 592abdf5bc1..6ca1b038453 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -156,18 +156,18 @@ def __enter__(self): if six.PY2: def warn(*args, **kwargs): - return self._warn(*args, **kwargs) + return self._saved_warn(*args, **kwargs) - warnings.warn, self._warn = warn, warnings.warn + warnings.warn, self._saved_warn = warn, warnings.warn return self def __exit__(self, *exc_info): if not self._entered: __tracebackhide__ = True raise RuntimeError("Cannot exit %r without entering first" % self) - # see above where `self.mp` is assigned + # see above where `self._saved_warn` is assigned if six.PY2: - warnings.warn = self._warn + warnings.warn = self._saved_warn super(WarningsRecorder, self).__exit__(*exc_info)