From 60a44921d619f1c772d782b79999588263d37c13 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 4 Mar 2026 15:33:01 +0000 Subject: [PATCH 1/3] typing: Correct hint for TestCase.assertRaises This accepts a tuple of exception types also, as the `isinstance` check in `_AssertRaisesContext.__exit__` reveals. Signed-off-by: Stephen Finucane --- testtools/testcase.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/testtools/testcase.py b/testtools/testcase.py index e07d1ecb..7add708e 100644 --- a/testtools/testcase.py +++ b/testtools/testcase.py @@ -495,7 +495,7 @@ def assertIsInstance( # type: ignore[override] def assertRaises( # type: ignore[override] self, - expected_exception: type[BaseException], + expected_exception: type[BaseException] | tuple[type[BaseException]], callable: Callable[..., object] | None = None, *args: object, **kwargs: object, @@ -1206,7 +1206,10 @@ class _AssertRaisesContext: """ def __init__( - self, expected: type[BaseException], test_case: TestCase, msg: str | None = None + self, + expected: type[BaseException] | tuple[type[BaseException]], + test_case: TestCase, + msg: str | None = None, ) -> None: """Construct an `_AssertRaisesContext`. From 1a97856c5491003caf7cfa3ee22e47ccdb8abefc Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 4 Mar 2026 15:36:15 +0000 Subject: [PATCH 2/3] typing: Return Self from __enter__ Signed-off-by: Stephen Finucane --- testtools/testcase.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/testtools/testcase.py b/testtools/testcase.py index 7add708e..f1e42459 100644 --- a/testtools/testcase.py +++ b/testtools/testcase.py @@ -23,7 +23,7 @@ import types import unittest from collections.abc import Callable, Iterator -from typing import TypeVar, cast +from typing import TYPE_CHECKING, TypeVar, cast from unittest.case import SkipTest T = TypeVar("T") @@ -56,6 +56,12 @@ TestResult, ) +if TYPE_CHECKING: + if sys.version_info >= (3, 11): + from typing import Self + else: + from typing_extensions import Self + # Circular import: fixtures imports gather_details from here, we import # fixtures, leading to gather_details not being available and fixtures being # unable to import it. @@ -1222,7 +1228,7 @@ def __init__( self.msg = msg self.exception: BaseException | None = None - def __enter__(self) -> "_AssertRaisesContext": + def __enter__(self) -> "Self": return self def __exit__( From 58f96e3c96100b5500326373c6fcd9301d002bce Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 4 Mar 2026 17:32:33 +0000 Subject: [PATCH 3/3] typing: Further improvements to TestCase.assertRaises Provider overloads so we know we can distinguish between use as context manager vs the historical function call pattern. Signed-off-by: Stephen Finucane --- testtools/testcase.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/testtools/testcase.py b/testtools/testcase.py index f1e42459..c235dd03 100644 --- a/testtools/testcase.py +++ b/testtools/testcase.py @@ -23,7 +23,7 @@ import types import unittest from collections.abc import Callable, Iterator -from typing import TYPE_CHECKING, TypeVar, cast +from typing import TYPE_CHECKING, TypeVar, cast, overload from unittest.case import SkipTest T = TypeVar("T") @@ -499,6 +499,24 @@ def assertIsInstance( # type: ignore[override] matcher = IsInstance(klass) self.assertThat(obj, matcher, msg or "") + @overload # type: ignore[override] + def assertRaises( + self, + expected_exception: type[BaseException] | tuple[type[BaseException]], + callable: Callable[..., object], + *args: object, + **kwargs: object, + ) -> BaseException: ... + + @overload # type: ignore[override] + def assertRaises( + self, + expected_exception: type[BaseException] | tuple[type[BaseException]], + callable: None = ..., + *args: object, + **kwargs: object, + ) -> "_AssertRaisesContext": ... + def assertRaises( # type: ignore[override] self, expected_exception: type[BaseException] | tuple[type[BaseException]],