diff --git a/AUTHORS b/AUTHORS index 13ea9469799..062f565c9ce 100644 --- a/AUTHORS +++ b/AUTHORS @@ -263,6 +263,7 @@ Mickey Pashov Mihai Capotă Mike Hoyle (hoylemd) Mike Lundy +Milan Lesnek Miro Hrončok Nathaniel Compton Nathaniel Waisbrot diff --git a/changelog/10701.bugfix.rst b/changelog/10701.bugfix.rst new file mode 100644 index 00000000000..f33fa7fb28b --- /dev/null +++ b/changelog/10701.bugfix.rst @@ -0,0 +1,2 @@ +:meth:`pytest.WarningsRecorder.pop` will return the most-closely-matched warning in the list, +rather than the first warning which is an instance of the requested type. diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index ff8e7082050..5484d6f3b33 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -206,10 +206,21 @@ def __len__(self) -> int: return len(self._list) def pop(self, cls: Type[Warning] = Warning) -> "warnings.WarningMessage": - """Pop the first recorded warning, raise exception if not exists.""" + """Pop the first recorded warning which is an instance of ``cls``, + but not an instance of a child class of any other match. + Raises ``AssertionError`` if there is no match. + """ + best_idx: Optional[int] = None for i, w in enumerate(self._list): - if issubclass(w.category, cls): - return self._list.pop(i) + if w.category == cls: + return self._list.pop(i) # exact match, stop looking + if issubclass(w.category, cls) and ( + best_idx is None + or not issubclass(w.category, self._list[best_idx].category) + ): + best_idx = i + if best_idx is not None: + return self._list.pop(best_idx) __tracebackhide__ = True raise AssertionError(f"{cls!r} not found in warning list") diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 16b8d54438f..8b70c8afffa 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -1,5 +1,7 @@ import warnings +from typing import List from typing import Optional +from typing import Type import pytest from _pytest.pytester import Pytester @@ -37,6 +39,47 @@ def test_recwarn_captures_deprecation_warning(recwarn: WarningsRecorder) -> None assert recwarn.pop(DeprecationWarning) +class TestSubclassWarningPop: + class ParentWarning(Warning): + pass + + class ChildWarning(ParentWarning): + pass + + class ChildOfChildWarning(ChildWarning): + pass + + @staticmethod + def raise_warnings_from_list(_warnings: List[Type[Warning]]): + for warn in _warnings: + warnings.warn(f"Warning {warn().__repr__()}", warn) + + def test_pop_finds_exact_match(self): + with pytest.warns((self.ParentWarning, self.ChildWarning)) as record: + self.raise_warnings_from_list( + [self.ChildWarning, self.ParentWarning, self.ChildOfChildWarning] + ) + + assert len(record) == 3 + _warn = record.pop(self.ParentWarning) + assert _warn.category is self.ParentWarning + + def test_pop_raises_if_no_match(self): + with pytest.raises(AssertionError): + with pytest.warns(self.ParentWarning) as record: + self.raise_warnings_from_list([self.ParentWarning]) + record.pop(self.ChildOfChildWarning) + + def test_pop_finds_best_inexact_match(self): + with pytest.warns(self.ParentWarning) as record: + self.raise_warnings_from_list( + [self.ChildOfChildWarning, self.ChildWarning, self.ChildOfChildWarning] + ) + + _warn = record.pop(self.ParentWarning) + assert _warn.category is self.ChildWarning + + class TestWarningsRecorderChecker: def test_recording(self) -> None: rec = WarningsRecorder(_ispytest=True)