From 107864786f8a40af7984f8cc0638751a42f341e5 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 5 Mar 2026 12:09:17 +0000 Subject: [PATCH] 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