From af65c0e24ba144c57a2f0d4cd0c12d6d0c17e824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Mon, 16 Jun 2025 17:26:17 +0100 Subject: [PATCH 1/2] Add myself to list of authors --- LICENSE | 1 + 1 file changed, 1 insertion(+) diff --git a/LICENSE b/LICENSE index 8f39c07c..b0ae632f 100644 --- a/LICENSE +++ b/LICENSE @@ -21,6 +21,7 @@ The testtools authors are: * Tristan Seligmann * Julian Edwards * Jonathan Jacobs + * Jelmer Vernooij and are collectively referred to as "testtools developers". From 4669c5ab77e7665c8d0c653631b9b19835808b0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Mon, 16 Jun 2025 17:27:24 +0100 Subject: [PATCH 2/2] Support TestCase::assertRaises being used as a context manager. Fixes #442 --- LICENSE | 2 +- testtools/testcase.py | 65 +++++++++++++++++++++++++++++++- testtools/tests/test_testcase.py | 39 +++++++++++++++++++ 3 files changed, 104 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index b0ae632f..9e0021e7 100644 --- a/LICENSE +++ b/LICENSE @@ -43,7 +43,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -Some code in testtools/run.py taken from Python's unittest module: +Some code in testtools/run.py and testtools/testcase.py taken from Python's unittest module: Copyright (c) 1999-2003 Steve Purcell Copyright (c) 2003-2010 Python Software Foundation diff --git a/testtools/testcase.py b/testtools/testcase.py index 4f4923b3..84551861 100644 --- a/testtools/testcase.py +++ b/testtools/testcase.py @@ -436,14 +436,32 @@ def assertIsInstance(self, obj, klass, msg=None): matcher = IsInstance(klass) self.assertThat(obj, matcher, msg) - def assertRaises(self, excClass, callableObj, *args, **kwargs): + def assertRaises(self, excClass, callableObj=None, *args, **kwargs): """Fail unless an exception of class excClass is thrown by callableObj when invoked with arguments args and keyword arguments kwargs. If a different type of exception is thrown, it will not be caught, and the test case will be deemed to have suffered an error, exactly as for an unexpected exception. + + If called with the callable omitted, will return a + context object used like this:: + + with self.assertRaises(SomeException): + do_something() + + The context manager keeps a reference to the exception as + the 'exception' attribute. This allows you to inspect the + exception after the assertion:: + + with self.assertRaises(SomeException) as cm: + do_something() + the_exception = cm.exception + self.assertEqual(the_exception.error_code, 3) """ + # If callableObj is None, we're being used as a context manager + if callableObj is None: + return _AssertRaisesContext(excClass, self, msg=kwargs.get("msg")) class ReRaiseOtherTypes: def match(self, matchee): @@ -1011,6 +1029,51 @@ def _id(obj): return _id +class _AssertRaisesContext: + """A context manager to handle expected exceptions for assertRaises. + + This provides compatibility with unittest's assertRaises context manager. + """ + + def __init__(self, expected, test_case, msg=None): + """Construct an `_AssertRaisesContext`. + + :param expected: The type of exception to expect. + :param test_case: The TestCase instance using this context. + :param msg: An optional message explaining the failure. + """ + self.expected = expected + self.test_case = test_case + self.msg = msg + self.exception = None + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + if exc_type is None: + try: + if isinstance(self.expected, tuple): + exc_name = "({})".format( + ", ".join(e.__name__ for e in self.expected) + ) + else: + exc_name = self.expected.__name__ + except AttributeError: + exc_name = str(self.expected) + if self.msg: + error_msg = "{} not raised : {}".format(exc_name, self.msg) + else: + error_msg = "{} not raised".format(exc_name) + raise self.test_case.failureException(error_msg) + if not issubclass(exc_type, self.expected): + # let unexpected exceptions pass through + return False + # store exception for later retrieval + self.exception = exc_value + return True + + class ExpectedException: """A context manager to handle expected exceptions. diff --git a/testtools/tests/test_testcase.py b/testtools/tests/test_testcase.py index 909f5148..eb6fc3ba 100644 --- a/testtools/tests/test_testcase.py +++ b/testtools/tests/test_testcase.py @@ -457,6 +457,45 @@ def test_assertRaisesRegex_wrong_message(self): "Observed", ) + def test_assertRaises_as_context_manager(self): + # assertRaises can be used as a context manager + with self.assertRaises(RuntimeError): + raise RuntimeError("Error message") + + def test_assertRaises_as_context_manager_no_exception(self): + # assertRaises used as context manager fails when no exception is raised + with self.assertRaises(self.failureException) as cm: + with self.assertRaises(RuntimeError): + pass + self.assertIn("RuntimeError not raised", str(cm.exception)) + + def test_assertRaises_as_context_manager_wrong_exception(self): + # assertRaises used as context manager re-raises unexpected exceptions + with self.assertRaises(ZeroDivisionError): + with self.assertRaises(RuntimeError): + raise ZeroDivisionError("Wrong exception") + + def test_assertRaises_as_context_manager_with_msg(self): + # assertRaises context manager can accept a msg parameter + with self.assertRaises(self.failureException) as cm: + with self.assertRaises(RuntimeError, msg="Custom message"): + pass + self.assertIn("Custom message", str(cm.exception)) + + def test_assertRaises_as_context_manager_stores_exception(self): + # assertRaises context manager stores the caught exception + with self.assertRaises(RuntimeError) as cm: + raise RuntimeError("Test error") + self.assertIsInstance(cm.exception, RuntimeError) + self.assertEqual("Test error", str(cm.exception)) + + def test_assertRaises_as_context_manager_with_multiple_exceptions(self): + # assertRaises context manager works with multiple exception types + exceptions = (RuntimeError, ValueError) + with self.assertRaises(exceptions) as cm: + raise ValueError("One of the expected exceptions") + self.assertIsInstance(cm.exception, ValueError) + def assertFails(self, message, function, *args, **kwargs): """Assert that function raises a failure with the given message.""" failure = self.assertRaises(self.failureException, function, *args, **kwargs)