From 50f61f9337507c66024ec452eaee912c0924bff4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Mus=C3=ADlek?= Date: Fri, 15 Sep 2023 08:36:09 +0200 Subject: [PATCH] Add order_matters argument to ShouldWarn context manager (#185) --- docs/warnings.txt | 20 ++++++++--- testfixtures/shouldwarn.py | 19 +++++++--- testfixtures/tests/test_shouldwarn.py | 51 +++++++++++++++++++++------ 3 files changed, 72 insertions(+), 18 deletions(-) diff --git a/docs/warnings.txt b/docs/warnings.txt index 439834c..f14c9ef 100644 --- a/docs/warnings.txt +++ b/docs/warnings.txt @@ -26,8 +26,8 @@ causing the test in which it occurs to fail: ... warn("sorry dave, I can't let you do that") Traceback (most recent call last): ... -AssertionError: sequence not as expected: - +AssertionError:... + same: [] @@ -40,6 +40,7 @@ attributes differ: actual: [UserWarning("sorry dave, I can't let you do that"...)] + (expected) != [UserWarning("sorry dave, I can't let you do that"...)] (actual) You can check multiple warnings in a particular piece of code: @@ -50,6 +51,16 @@ You can check multiple warnings in a particular piece of code: ... warn('you should fix that') ... warn('and that too') +If you don't care about the order of issued warnings, you can use ``order_matters=False``: + +>>> from warnings import warn +>>> from testfixtures import ShouldWarn +>>> with ShouldWarn(UserWarning('you should fix that'), +... UserWarning('and that too'), +... order_matters=False): +... warn('and that too') +... warn('you should fix that') + If you want to inspect more details of the warnings issued, you can capture them into a list as follows: @@ -78,8 +89,8 @@ context it manages, it will raise an :class:`AssertionError` to indicate this: ... warn("woah dude") Traceback (most recent call last): ... -AssertionError: sequence not as expected: - +AssertionError:... + same: [] @@ -88,3 +99,4 @@ expected: actual: [UserWarning('woah dude'...)] + (expected) != [UserWarning('woah dude'...)] (actual) diff --git a/testfixtures/shouldwarn.py b/testfixtures/shouldwarn.py index 08ee630..165a23b 100644 --- a/testfixtures/shouldwarn.py +++ b/testfixtures/shouldwarn.py @@ -1,7 +1,7 @@ import warnings from typing import Union, Type -from testfixtures import Comparison as C, compare +from testfixtures import Comparison, SequenceComparison, compare WarningOrType = Union[Warning, Type[Warning]] @@ -26,6 +26,13 @@ class ShouldWarn(warnings.catch_warnings): If no expected warnings are passed, you will need to inspect the contents of the list returned by the context manager. + + :param order_matters: + + A keyword-only parameter that controls whether the order of the + captured entries is required to match those of the expected entries. + Defaults to ``True``. + :param filters: If passed, these are used to create a filter such that only warnings you are interested in will be considered by this :class:`ShouldWarn` @@ -36,9 +43,10 @@ class ShouldWarn(warnings.catch_warnings): _empty_okay = False - def __init__(self, *expected: WarningOrType, **filters): + def __init__(self, *expected: WarningOrType, order_matters: bool = True, **filters): super(ShouldWarn, self).__init__(record=True) - self.expected = [C(e) for e in expected] + self.order_matters = order_matters + self.expected = [Comparison(e) for e in expected] self.filters = filters def __enter__(self): @@ -52,7 +60,10 @@ def __exit__(self, exc_type, exc_val, exc_tb): return if not self.expected and self.recorded and not self._empty_okay: return - compare(self.expected, actual=[wm.message for wm in self.recorded]) + compare( + expected=SequenceComparison(*self.expected, ordered=self.order_matters), + actual=[wm.message for wm in self.recorded] + ) class ShouldNotWarn(ShouldWarn): diff --git a/testfixtures/tests/test_shouldwarn.py b/testfixtures/tests/test_shouldwarn.py index eb4d1a1..2685873 100644 --- a/testfixtures/tests/test_shouldwarn.py +++ b/testfixtures/tests/test_shouldwarn.py @@ -25,10 +25,12 @@ def test_warn_expected(self): def test_warn_not_expected(self): with ShouldAssert( - "sequence not as expected:\n\n" + "\n\n" "same:\n[]\n\n" "expected:\n[]\n\n" - "actual:\n[UserWarning('foo'"+comma+")]" + "actual:\n[UserWarning('foo'"+comma+")]\n" + " (expected) " + "!= [UserWarning('foo'"+comma+")] (actual)" ): with warnings.catch_warnings(record=True) as backstop: with ShouldNotWarn(): @@ -41,10 +43,11 @@ def test_no_warn_expected(self): def test_no_warn_not_expected(self): with ShouldAssert( - "sequence not as expected:\n\n" + "\n\n" "same:\n[]\n\n" "expected:\n[args: ('foo',)]" - "\n\nactual:\n[]" + "\n\nactual:\n[]\n" + " (expected) != [] (actual)" ): with ShouldWarn(UserWarning('foo')): pass @@ -64,17 +67,42 @@ def test_multiple_warnings(self): self.assertTrue('foo' in content) self.assertTrue('bar' in content) + def test_multiple_warnings_ordered(self): + with warnings.catch_warnings(record=True) as backstop: + with ShouldWarn(UserWarning('foo'), UserWarning('bar')): + warnings.warn('foo') + warnings.warn('bar') + compare(len(backstop), expected=0) + + def test_multiple_warnings_wrong_order(self): + with ShouldRaise(AssertionError) as s: + with ShouldWarn(UserWarning('foo'), UserWarning('bar')): + warnings.warn('bar') + warnings.warn('foo') + content = str(s.raised) + self.assertTrue('foo' in content) + self.assertTrue('bar' in content) + + def test_multiple_warnings_ignore_order(self): + with warnings.catch_warnings(record=True) as backstop: + with ShouldWarn(UserWarning('foo'), UserWarning('bar'), order_matters=False): + warnings.warn('bar') + warnings.warn('foo') + compare(len(backstop), expected=0) + def test_minimal_ok(self): with ShouldWarn(UserWarning): warnings.warn('foo') def test_minimal_bad(self): with ShouldAssert( - "sequence not as expected:\n\n" + "\n\n" "same:\n[]\n\n" "expected:\n" "[wrong type]\n\n" - "actual:\n[UserWarning('foo'"+comma+")]" + "actual:\n[UserWarning('foo'"+comma+")]\n" + " (expected) " + "!= [UserWarning('foo'"+comma+")] (actual)" ): with ShouldWarn(DeprecationWarning): warnings.warn('foo') @@ -87,14 +115,16 @@ def test_maximal_ok(self): def test_maximal_bad(self): with ShouldAssert( - "sequence not as expected:\n\n" + "\n\n" "same:\n[]\n\n" "expected:\n[\n" "\n" "attributes differ:\n" "'args': ('bar',) (Comparison) != ('foo',) (actual)\n" "]\n\n" - "actual:\n[DeprecationWarning('foo'"+comma+")]" + "actual:\n[DeprecationWarning('foo'"+comma+")]\n" + " (expected) " + "!= [DeprecationWarning('foo'"+comma+")] (actual)" ): with ShouldWarn(DeprecationWarning('bar')): warnings.warn_explicit( @@ -130,10 +160,11 @@ def test_filter_present(self): def test_filter_missing(self): with ShouldAssert( - "sequence not as expected:\n\n" + "\n\n" "same:\n[]\n\n" "expected:\n[]\n\n" - "actual:\n[]" + "actual:\n[]\n" + " (expected) != [] (actual)" ): with ShouldWarn(DeprecationWarning, message="This function is deprecated."):