Skip to content

Commit 532d9b0

Browse files
committed
Merge branch 'feature/assert-warns'
2 parents e0336df + 0cc636c commit 532d9b0

File tree

4 files changed

+267
-6
lines changed

4 files changed

+267
-6
lines changed

NEWS.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
News in asserts 0.7.2
22
=====================
33

4+
API Additions
5+
-------------
6+
7+
* Add assert_warns() and assert_warns_regex().
8+
49
News in asserts 0.7.1
510
=====================
611

712
* Distribute as wheel.
813
* asserts is now a package, instead of a module.
914

10-
1115
News in asserts 0.7.0
1216
=====================
1317

asserts/__init__.py

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@
2020
2121
"""
2222

23-
from datetime import datetime, timedelta
2423
import re
24+
from datetime import datetime, timedelta
25+
from warnings import catch_warnings
2526

2627

2728
def fail(msg=None):
@@ -660,3 +661,118 @@ def __exit__(self, exc_type, exc_val, exc_tb):
660661
fail(msg or exception.__name__ + " was unexpectedly raised")
661662

662663
return _AssertSucceeds()
664+
665+
666+
class AssertWarnsContext(object):
667+
668+
"""A context manager to test for warning with certain properties.
669+
670+
When the context is left and the expected warning has not been raised, an
671+
AssertionError will be raised:
672+
673+
>>> context = AssertWarnsContext(DeprecationWarning)
674+
>>> with context:
675+
... pass
676+
Traceback (most recent call last):
677+
...
678+
AssertionError: DeprecationWarning not issued
679+
680+
If the warning has the right class, any additional tests that have been
681+
configured on the context, will be called:
682+
683+
>>> from warnings import warn
684+
>>> def test(warning):
685+
... return False
686+
>>> context.add_test(test)
687+
>>> with context:
688+
... warn("Wrong Message", DeprecationWarning)
689+
Traceback (most recent call last):
690+
...
691+
AssertionError: DeprecationWarning not issued
692+
693+
"""
694+
695+
def __init__(self, warning_class, msg=None):
696+
self._warning_class = warning_class
697+
self._msg = msg or "{} not issued".format(warning_class.__name__)
698+
self._warning_context = None
699+
self._warnings = []
700+
self._tests = []
701+
702+
def __enter__(self):
703+
self._warning_context = catch_warnings(record=True)
704+
self._warnings = self._warning_context.__enter__()
705+
706+
def __exit__(self, exc_type, exc_val, exc_tb):
707+
self._warning_context.__exit__(exc_type, exc_val, exc_tb)
708+
if not any(self._is_expected_warning(w) for w in self._warnings):
709+
raise AssertionError(self._msg)
710+
711+
def _is_expected_warning(self, warning):
712+
if not issubclass(warning.category, self._warning_class):
713+
return False
714+
return all(test(warning) for test in self._tests)
715+
716+
def add_test(self, cb):
717+
"""Add a test callback.
718+
719+
This callback is called after determining that the right warning
720+
class was issued. The callback will get the issued warning as only
721+
argument and must return a boolean value.
722+
723+
"""
724+
self._tests.append(cb)
725+
726+
727+
def assert_warns(warning_type, msg=None):
728+
"""Fail unless a specific warning is issued inside the context.
729+
730+
If a different type of warning is issued, it will not be caught.
731+
732+
>>> from warnings import warn
733+
>>> with assert_warns(UserWarning):
734+
... warn("warning message", UserWarning)
735+
...
736+
>>> with assert_warns(UserWarning):
737+
... pass
738+
...
739+
Traceback (most recent call last):
740+
...
741+
AssertionError: UserWarning not issued
742+
>>> with assert_warns(UserWarning):
743+
... warn("warning message", UnicodeWarning)
744+
...
745+
Traceback (most recent call last):
746+
...
747+
AssertionError: UserWarning not issued
748+
749+
"""
750+
return AssertWarnsContext(warning_type, msg)
751+
752+
753+
def assert_warns_regex(warning_type, regex, msg=None):
754+
"""Fail unless a warning with a message is issued inside the context.
755+
756+
The message can be a regular expression string or object.
757+
758+
>>> from warnings import warn
759+
>>> with assert_warns_regex(UserWarning, r"#\d+"):
760+
... warn("Error #42", UserWarning)
761+
...
762+
>>> with assert_warns_regex(UserWarning, r"Expected Error"):
763+
... warn("Generic Error", UserWarning)
764+
...
765+
Traceback (most recent call last):
766+
...
767+
AssertionError: no UserWarning matching 'Expected Error' issued
768+
"""
769+
770+
def test(warning):
771+
return re.search(regex, str(warning.message)) is not None
772+
773+
if msg is None:
774+
msg = "no {} matching {} issued".format(
775+
warning_type.__name__, repr(regex))
776+
context = AssertWarnsContext(warning_type, msg)
777+
context.add_test(test)
778+
return context

asserts/__init__.pyi

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ class AssertRaisesContext:
1212
def __exit__(self, exc_type: Type[BaseException], exc_val: BaseException, exc_tb: Any) -> None: ...
1313
def add_test(self, cb: Callable[[BaseException], None]) -> None: ...
1414

15+
class AssertWarnsContext:
16+
def __init__(self, warning_class: Type[Warning], msg: Optional[str] = ...) -> None: ...
17+
def __enter__(self) -> None: ...
18+
def __exit__(self, exc_type: Type[BaseException], exc_val: BaseException, exc_tb: Any) -> None: ...
19+
def add_test(self, cb: Callable[[Warning], None]) -> None: ...
20+
1521
def fail(msg: Optional[str] = ...) -> None: ...
1622
def assert_true(expr: Any, msg: Optional[str] = ...) -> None: ...
1723
def assert_false(expr: Any, msg: Optional[str] = ...) -> None: ...
@@ -39,6 +45,8 @@ def assert_has_attr(obj: Any, attribute: str, msg: Optional[str] = ...) -> None:
3945
def assert_datetime_about_now(actual: Optional[datetime.datetime], msg: Optional[str] = ...) -> None: ...
4046
def assert_datetime_about_now_utc(actual: Optional[datetime.datetime], msg: Optional[str] = ...) -> None: ...
4147
def assert_raises(exception: Type[BaseException], msg: Optional[str] = ...) -> AssertRaisesContext: ...
42-
def assert_raises_regex(exception: Type[BaseException], regex: str, msg: Optional[str] = ...) -> AssertRaisesContext: ...
48+
def assert_raises_regex(exception: Type[BaseException], regex: str, msg: Optional[str] = ...) -> AssertRaisesContext: ...
4349
def assert_raises_errno(exception: Type[BaseException], errno: int, msg: Optional[str] = ...) -> AssertRaisesContext: ...
4450
def assert_succeeds(exception: Type[BaseException], msg: Optional[str] = ...) -> ContextManager: ...
51+
def assert_warns(warning_type: Type[Warning], msg: Optional[str] = ...) -> AssertWarnsContext: ...
52+
def assert_warns_regex(warning_type: Type[Warning], regex: str, msg: Optional[str] = ...) -> AssertWarnsContext: ...

test_asserts.py

Lines changed: 136 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
from datetime import datetime, timedelta
21
import re
3-
from unittest import TestCase
42
import sys
3+
from datetime import datetime, timedelta
4+
from unittest import TestCase
5+
from warnings import warn, catch_warnings
56

67
from asserts import (
78
fail,
@@ -34,7 +35,8 @@
3435
assert_raises_regex,
3536
assert_raises_errno,
3637
assert_succeeds,
37-
)
38+
assert_warns,
39+
assert_warns_regex)
3840

3941

4042
class _DummyObject(object):
@@ -562,6 +564,8 @@ def test_assert_raises_errno__wrong_errno__custom_message(self):
562564
with assert_raises_errno(OSError, 20, msg="test message"):
563565
raise OSError(1, "Test error")
564566

567+
# assert_succeeds()
568+
565569
def test_assert_succeeds__no_exception_raised(self):
566570
with assert_succeeds(KeyError):
567571
pass
@@ -584,3 +588,132 @@ def test_assert_succeeds__unexpected_exception(self):
584588
pass
585589
else:
586590
raise AssertionError("KeyError was not raised")
591+
592+
# assert_warns()
593+
594+
def test_assert_warns__warned(self):
595+
with assert_succeeds(AssertionError):
596+
with assert_warns(FutureWarning):
597+
warn("foo", FutureWarning)
598+
599+
def test_assert_warns__not_warned(self):
600+
with assert_raises(AssertionError):
601+
with assert_warns(ImportWarning):
602+
pass
603+
604+
def test_assert_warns__wrong_type(self):
605+
with assert_raises(AssertionError):
606+
with assert_warns(ImportWarning):
607+
warn("foo", UnicodeWarning)
608+
609+
def test_assert_warns__multiple_warnings(self):
610+
with assert_succeeds(AssertionError):
611+
with assert_warns(UserWarning):
612+
warn("foo", UnicodeWarning)
613+
warn("bar", UserWarning)
614+
warn("baz", FutureWarning)
615+
616+
def test_assert_warns__warning_handler_deinstalled_on_success(self):
617+
with catch_warnings(record=1) as warnings:
618+
with assert_warns(UserWarning):
619+
warn("foo", UserWarning)
620+
assert_equal(0, len(warnings))
621+
warn("bar", UserWarning)
622+
assert_equal(1, len(warnings))
623+
624+
def test_assert_warns__warning_handler_deinstalled_on_failure(self):
625+
with catch_warnings(record=1) as warnings:
626+
try:
627+
with assert_warns(UserWarning):
628+
pass
629+
except AssertionError:
630+
pass
631+
assert_equal(0, len(warnings))
632+
warn("bar", UserWarning)
633+
assert_equal(1, len(warnings))
634+
635+
def test_assert_warns__default_message(self):
636+
with assert_raises_regex(AssertionError,
637+
r"^ImportWarning not issued"):
638+
with assert_warns(ImportWarning):
639+
pass
640+
641+
def test_assert_warns__custom_message(self):
642+
with assert_raises_regex(AssertionError, r"^Custom Test Message$"):
643+
with assert_warns(ImportWarning, msg="Custom Test Message"):
644+
pass
645+
646+
# assert_warns_regex()
647+
648+
def test_assert_warns_regex__warned(self):
649+
with assert_succeeds(AssertionError):
650+
with assert_warns_regex(FutureWarning, r"fo+"):
651+
warn("foo", FutureWarning)
652+
653+
def test_assert_warns_regex__warning_text_matches_in_the_middle(self):
654+
with assert_succeeds(AssertionError):
655+
with assert_warns_regex(FutureWarning, r"o"):
656+
warn("foo", FutureWarning)
657+
658+
def test_assert_warns_regex__not_warned(self):
659+
with assert_raises(AssertionError):
660+
with assert_warns_regex(UserWarning, r"foo"):
661+
pass
662+
663+
def test_assert_warns_regex__wrong_type(self):
664+
with assert_raises(AssertionError):
665+
with assert_warns_regex(ImportWarning, r"foo"):
666+
warn("foo", UnicodeWarning)
667+
668+
def test_assert_warns_regex__wrong_message(self):
669+
with assert_raises(AssertionError):
670+
with assert_warns_regex(UnicodeWarning, r"foo"):
671+
warn("bar", UnicodeWarning)
672+
673+
def test_assert_warns_regex__multiple_warnings(self):
674+
with assert_succeeds(AssertionError):
675+
with assert_warns_regex(UserWarning, r"bar2"):
676+
warn("foo", UnicodeWarning)
677+
warn("bar1", UserWarning)
678+
warn("bar2", UserWarning)
679+
warn("bar3", UserWarning)
680+
warn("baz", FutureWarning)
681+
682+
def test_assert_warns_regex__warning_handler_deinstalled_on_success(self):
683+
with catch_warnings(record=1) as warnings:
684+
with assert_warns_regex(UserWarning, r"foo"):
685+
warn("foo", UserWarning)
686+
assert_equal(0, len(warnings))
687+
warn("bar", UserWarning)
688+
assert_equal(1, len(warnings))
689+
690+
def test_assert_warns_regex__warning_handler_deinstalled_on_failure(self):
691+
with catch_warnings(record=1) as warnings:
692+
try:
693+
with assert_warns_regex(UserWarning, r""):
694+
pass
695+
except AssertionError:
696+
pass
697+
assert_equal(0, len(warnings))
698+
warn("bar", UserWarning)
699+
assert_equal(1, len(warnings))
700+
701+
def test_assert_warns_regex__default_message_not_issued(self):
702+
with assert_raises_regex(
703+
AssertionError,
704+
r"^no UserWarning matching 'foo.*bar' issued$"):
705+
with assert_warns_regex(UserWarning, r"foo.*bar"):
706+
pass
707+
708+
def test_assert_warns_regex__default_message_wrong_message(self):
709+
with assert_raises_regex(
710+
AssertionError,
711+
r"^no UserWarning matching 'foo.*bar' issued$"):
712+
with assert_warns_regex(UserWarning, r"foo.*bar"):
713+
pass
714+
715+
def test_assert_warns_regex__custom_message(self):
716+
with assert_raises_regex(AssertionError, r"^Custom Test Message$"):
717+
with assert_warns_regex(
718+
ImportWarning, r"", msg="Custom Test Message"):
719+
pass

0 commit comments

Comments
 (0)