From a120e6332f6ba6c173f68bad356c0e05f7e99ceb Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 20 Feb 2026 14:08:37 +0000 Subject: [PATCH 1/3] typing: Return Self from __enter__ Otherwise the return value is typed as the Fixture class instead of the actual class. Signed-off-by: Stephen Finucane --- fixtures/_fixtures/popen.py | 12 ++++++++++-- fixtures/callmany.py | 15 ++++++++++----- fixtures/fixture.py | 29 ++++++++++++++--------------- 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/fixtures/_fixtures/popen.py b/fixtures/_fixtures/popen.py index 038b8d0..e128adc 100644 --- a/fixtures/_fixtures/popen.py +++ b/fixtures/_fixtures/popen.py @@ -13,6 +13,8 @@ # license you chose for the specific language governing permissions and # limitations under that license. +from __future__ import annotations + __all__ = [ "FakePopen", "PopenFixture", @@ -21,11 +23,17 @@ import random import subprocess import sys -from typing import Any, IO, Final +from typing import Any, IO, Final, TYPE_CHECKING from collections.abc import Callable from fixtures import Fixture +if TYPE_CHECKING: + if sys.version_info >= (3, 11): + from typing import Self + else: + from typing_extensions import Self + class _Unpassed: """Sentinel type for unpassed arguments.""" @@ -77,7 +85,7 @@ def communicate( err = "" return out, err - def __enter__(self) -> "FakeProcess": + def __enter__(self) -> Self: return self def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: diff --git a/fixtures/callmany.py b/fixtures/callmany.py index b756184..da06f05 100644 --- a/fixtures/callmany.py +++ b/fixtures/callmany.py @@ -13,17 +13,22 @@ # license you chose for the specific language governing permissions and # limitations under that license. +from __future__ import annotations + __all__ = [ "CallMany", ] import sys -from typing import Any, Literal, Optional, TYPE_CHECKING from collections.abc import Callable +from typing import Any, Literal, TYPE_CHECKING +from types import TracebackType if TYPE_CHECKING: - from types import TracebackType - + if sys.version_info >= (3, 11): + from typing import Self + else: + from typing_extensions import Self try: from testtools import MultipleExceptions @@ -68,7 +73,7 @@ def __call__( tuple[ type[BaseException] | None, BaseException | None, - Optional["TracebackType"], + TracebackType | None, ] ] | None @@ -115,7 +120,7 @@ def __call__( return result return None - def __enter__(self) -> "CallMany": + def __enter__(self) -> Self: return self def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> Literal[False]: diff --git a/fixtures/fixture.py b/fixtures/fixture.py index e81859d..ec04ee0 100644 --- a/fixtures/fixture.py +++ b/fixtures/fixture.py @@ -13,6 +13,8 @@ # license you chose for the specific language governing permissions and # limitations under that license. +from __future__ import annotations + __all__ = [ "CompoundFixture", "Fixture", @@ -24,23 +26,20 @@ import itertools import sys -from typing import ( - Any, - Literal, - Optional, - TypeVar, - TYPE_CHECKING, -) +from typing import Any, Literal, TypeVar, TYPE_CHECKING from collections.abc import Callable, Iterable -from fixtures.callmany import ( - CallMany, -) +from fixtures.callmany import CallMany # Deprecated, imported for compatibility. import fixtures.callmany if TYPE_CHECKING: + if sys.version_info >= (3, 11): + from typing import Self + else: + from typing_extensions import Self + from types import TracebackType T = TypeVar("T", bound="Fixture") @@ -80,7 +79,7 @@ class SetupError(Exception): class Fixture: _cleanups: CallMany | None _details: dict[str, Any] | None - _detail_sources: list["Fixture"] | None + _detail_sources: list[Fixture] | None """A Fixture representing some state or resource. Often used in tests, a Fixture must be setUp before using it, and cleanUp @@ -130,7 +129,7 @@ def cleanUp( tuple[ type[BaseException] | None, BaseException | None, - Optional["TracebackType"], + TracebackType | None, ] ] | None @@ -185,7 +184,7 @@ def _remove_state(self) -> None: self._details = None self._detail_sources = None - def __enter__(self) -> "Fixture": + def __enter__(self) -> Self: self.setUp() return self @@ -467,7 +466,7 @@ def cleanUp( tuple[ type[BaseException] | None, BaseException | None, - Optional["TracebackType"], + TracebackType | None, ] ] | None @@ -489,7 +488,7 @@ class CompoundFixture(Fixture): :ivar fixtures: The list of fixtures that make up this one. (read only). """ - def __init__(self, fixtures: Iterable["Fixture"]) -> None: + def __init__(self, fixtures: Iterable[Fixture]) -> None: """Construct a fixture made of many fixtures. :param fixtures: An iterable of fixtures. From eb1f2df229c5a520fa6e39dfa059c4867baf7f30 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 20 Feb 2026 14:12:59 +0000 Subject: [PATCH 2/3] typing: Use ParamSpec This ensures args and kwargs are actually valid the cleanup function(s) in question. Signed-off-by: Stephen Finucane --- fixtures/callmany.py | 9 +++++++-- fixtures/fixture.py | 7 ++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/fixtures/callmany.py b/fixtures/callmany.py index da06f05..10d750d 100644 --- a/fixtures/callmany.py +++ b/fixtures/callmany.py @@ -21,7 +21,7 @@ import sys from collections.abc import Callable -from typing import Any, Literal, TYPE_CHECKING +from typing import Any, Literal, ParamSpec, TYPE_CHECKING from types import TracebackType if TYPE_CHECKING: @@ -38,6 +38,9 @@ class MultipleExceptions(Exception): # type: ignore[no-redef] """Report multiple exc_info tuples in self.args.""" +P = ParamSpec("P") + + class CallMany: """A stack of functions which will all be called on __call__. @@ -53,7 +56,9 @@ def __init__(self) -> None: tuple[Callable[..., Any], tuple[Any, ...], dict[str, Any]] ] = [] - def push(self, cleanup: Callable[..., Any], *args: Any, **kwargs: Any) -> None: + def push( + self, cleanup: Callable[P, Any], *args: P.args, **kwargs: P.kwargs + ) -> None: """Add a function to be called from __call__. On __call__ all functions are called - see __call__ for details on how diff --git a/fixtures/fixture.py b/fixtures/fixture.py index ec04ee0..72d895d 100644 --- a/fixtures/fixture.py +++ b/fixtures/fixture.py @@ -26,8 +26,9 @@ import itertools import sys -from typing import Any, Literal, TypeVar, TYPE_CHECKING from collections.abc import Callable, Iterable +from typing import Any, Literal, ParamSpec, TypeVar, TYPE_CHECKING +from types import TracebackType from fixtures.callmany import CallMany @@ -40,9 +41,9 @@ else: from typing_extensions import Self - from types import TracebackType T = TypeVar("T", bound="Fixture") +P = ParamSpec("P") MultipleExceptions = fixtures.callmany.MultipleExceptions # type: ignore[attr-defined] @@ -91,7 +92,7 @@ class Fixture: """ def addCleanup( - self, cleanup: Callable[..., Any], *args: Any, **kwargs: Any + self, cleanup: Callable[P, Any], *args: P.args, **kwargs: P.kwargs ) -> None: """Add a clean function to be called from cleanUp. From 6cb5e4f86be3e4a0a764e52a0425904fd6b26fc6 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 20 Feb 2026 14:26:36 +0000 Subject: [PATCH 3/3] typing: Improve types for WarningsCapture Actually expose the kwargs expected. Signed-off-by: Stephen Finucane --- fixtures/_fixtures/warnings.py | 36 ++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/fixtures/_fixtures/warnings.py b/fixtures/_fixtures/warnings.py index dc37752..984afbb 100644 --- a/fixtures/_fixtures/warnings.py +++ b/fixtures/_fixtures/warnings.py @@ -11,17 +11,35 @@ # license you chose for the specific language governing permissions and # limitations under that license. +from __future__ import annotations + __all__ = [ "WarningsCapture", "WarningsFilter", ] +import sys import warnings -from typing import Any, cast +from typing import Any, cast, Literal, TextIO, TypedDict, TYPE_CHECKING import fixtures from fixtures._fixtures.monkeypatch import MonkeyPatch +if TYPE_CHECKING: + if sys.version_info >= (3, 11): + from typing import NotRequired + else: + from typing_extensions import NotRequired + + +class _WarningFilterArgs(TypedDict): + action: Literal["error", "ignore", "always", "default", "module", "once"] + message: NotRequired[str] + category: NotRequired[type[Warning]] + module: NotRequired[str] + lineno: NotRequired[int] + append: NotRequired[bool] + class WarningsCapture(fixtures.Fixture): """Capture warnings. @@ -34,8 +52,18 @@ class WarningsCapture(fixtures.Fixture): captures: list[warnings.WarningMessage] - def _showwarning(self, *args: Any, **kwargs: Any) -> None: - self.captures.append(warnings.WarningMessage(*args, **kwargs)) + def _showwarning( + self, + message: str, + category: type[Warning], + filename: str, + lineno: int, + file: TextIO | None = None, + line: str | None = None, + ) -> None: + self.captures.append( + warnings.WarningMessage(message, category, filename, lineno, file, line) + ) def _setUp(self) -> None: patch = MonkeyPatch("warnings.showwarning", self._showwarning) @@ -50,7 +78,7 @@ class WarningsFilter(fixtures.Fixture): configuration. """ - def __init__(self, filters: list[dict[str, Any]] | None = None) -> None: + def __init__(self, filters: list[_WarningFilterArgs] | None = None) -> None: """Create a WarningsFilter fixture. :param filters: An optional list of dictionaries with arguments