From 60a44921d619f1c772d782b79999588263d37c13 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 4 Mar 2026 15:33:01 +0000 Subject: [PATCH 1/5] 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/5] 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/5] 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]], From 107864786f8a40af7984f8cc0638751a42f341e5 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 5 Mar 2026 12:09:17 +0000 Subject: [PATCH 4/5] typing: Make use of ParamSpec So we can catch type issues like the below: def foo(a: str, b: int | None = None): ... class FooTest(testtools.TestCase): def test_foo(self): self.assertRaises(Exception, foo, 123, b='abc') We don't currently have type checking enabled for the tests directory, but adding a test like the above proves this works. Signed-off-by: Stephen Finucane --- testtools/testcase.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/testtools/testcase.py b/testtools/testcase.py index c235dd03..d3929a97 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, overload +from typing import TYPE_CHECKING, ParamSpec, TypeVar, cast, overload from unittest.case import SkipTest T = TypeVar("T") @@ -88,6 +88,8 @@ class _ExpectedFailure(Exception): # TypeVar for decorators +_P = ParamSpec("_P") +_R = TypeVar("_R") _F = TypeVar("_F", bound=Callable[..., object]) @@ -390,10 +392,10 @@ def _formatTypes( def addCleanup( self, - function: Callable[..., object], + function: Callable[_P, _R], /, - *arguments: object, - **keywordArguments: object, + *args: _P.args, + **kwargs: _P.kwargs, ) -> None: """Add a cleanup function to be called after tearDown. @@ -407,7 +409,7 @@ def addCleanup( Cleanup functions are always called before a test finishes running, even if setUp is aborted by an exception. """ - self._cleanups.append((function, arguments, keywordArguments)) + self._cleanups.append((function, args, kwargs)) def addOnException(self, handler: "Callable[[ExcInfo], None]") -> None: """Add a handler to be called when an exception occurs in test code. @@ -503,9 +505,9 @@ def assertIsInstance( # type: ignore[override] def assertRaises( self, expected_exception: type[BaseException] | tuple[type[BaseException]], - callable: Callable[..., object], - *args: object, - **kwargs: object, + callable: Callable[_P, _R], + *args: _P.args, + **kwargs: _P.kwargs, ) -> BaseException: ... @overload # type: ignore[override] @@ -513,16 +515,14 @@ 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]], - callable: Callable[..., object] | None = None, - *args: object, - **kwargs: object, + callable: Callable[_P, _R] | None = None, + *args: _P.args, + **kwargs: _P.kwargs, ) -> "_AssertRaisesContext | BaseException": """Fail unless an exception of class expected_exception is thrown by callable when invoked with arguments args and keyword @@ -678,9 +678,9 @@ def defaultTestResult(self) -> TestResult: def expectFailure( self, reason: str, - predicate: Callable[..., object], - *args: object, - **kwargs: object, + predicate: Callable[_P, _R], + *args: _P.args, + **kwargs: _P.kwargs, ) -> None: """Check that a test fails in a particular way. @@ -1349,7 +1349,10 @@ class Nullary: """ def __init__( - self, callable_object: Callable[..., object], *args: object, **kwargs: object + self, + callable_object: Callable[_P, _R], + *args: _P.args, + **kwargs: _P.kwargs, ) -> None: self._callable_object = callable_object self._args = args From d607bf4c11e1c887621a426f86578f25a8a3d25b Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 5 Mar 2026 16:19:29 +0000 Subject: [PATCH 5/5] release 2.8.6 Signed-off-by: Stephen Finucane --- NEWS | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/NEWS b/NEWS index 6e3865c9..74a2b173 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,27 @@ testtools NEWS Changes and improvements to testtools_, grouped by release. +NEXT +~~~~ + +2.8.6 +~~~~~ + +Improvements +------------ +* Improve the type annotations for ``testtools.testcase.TestCase.assertRaises``. + This method will now be typed correctly when called as a method or as a + context manager. + (Stephen Finucane, #577, #578) + +2.8.5 +~~~~~ + +Improvements +------------ +* Fix the type annotations for ``testtools.testcase.TestCase.useFixture`` + (Stephen Finucane, #574) + 2.8.4 ~~~~~ @@ -70,8 +91,7 @@ Improvements (Jelmer Vernooij, #538) * Allow stream=None to be passed to various TestResult - classes that now support verbosity; fixes a regression -from 2.8.0. + classes that now support verbosity; fixes a regression from 2.8.0. (Jelmer Vernooij) 2.8.1