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 diff --git a/testtools/testcase.py b/testtools/testcase.py index e07d1ecb..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 TypeVar, cast +from typing import TYPE_CHECKING, ParamSpec, TypeVar, cast, overload 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. @@ -82,6 +88,8 @@ class _ExpectedFailure(Exception): # TypeVar for decorators +_P = ParamSpec("_P") +_R = TypeVar("_R") _F = TypeVar("_F", bound=Callable[..., object]) @@ -384,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. @@ -401,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. @@ -493,12 +501,28 @@ 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[_P, _R], + *args: _P.args, + **kwargs: _P.kwargs, + ) -> BaseException: ... + + @overload # type: ignore[override] + def assertRaises( + self, + expected_exception: type[BaseException] | tuple[type[BaseException]], + callable: None = ..., + ) -> "_AssertRaisesContext": ... + def assertRaises( # type: ignore[override] self, - expected_exception: type[BaseException], - callable: Callable[..., object] | None = None, - *args: object, - **kwargs: object, + expected_exception: type[BaseException] | tuple[type[BaseException]], + 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 @@ -654,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. @@ -1206,7 +1230,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`. @@ -1219,7 +1246,7 @@ def __init__( self.msg = msg self.exception: BaseException | None = None - def __enter__(self) -> "_AssertRaisesContext": + def __enter__(self) -> "Self": return self def __exit__( @@ -1322,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