From 7e92e251c85596f62d5c6e882739db1d21f1bce1 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Tue, 29 Sep 2026 15:20:56 +0200 Subject: [PATCH 1/3] Use 'from __future__ import annotations' in every file Enforce it with ruff's required-imports (I002). Refs #610 Co-Authored-By: Claude Opus 5.5 --- docs/conf.py | 2 ++ pyproject.toml | 4 ++++ src/pytestqt/__init__.py | 2 ++ src/pytestqt/exceptions.py | 2 ++ src/pytestqt/logging.py | 2 ++ src/pytestqt/modeltest.py | 2 ++ src/pytestqt/plugin.py | 2 ++ src/pytestqt/qt_compat.py | 2 ++ src/pytestqt/qtbot.py | 2 ++ src/pytestqt/utils.py | 2 ++ src/pytestqt/wait_signal.py | 2 ++ tests/conftest.py | 2 ++ tests/test_basics.py | 2 ++ tests/test_exceptions.py | 2 ++ tests/test_logging.py | 2 ++ tests/test_modeltest.py | 2 ++ tests/test_qtbot_pep8_aliases.py | 2 ++ tests/test_qtest_proxies.py | 2 ++ tests/test_screenshot.py | 2 ++ tests/test_wait_signal.py | 2 ++ tests/test_wait_until.py | 2 ++ 21 files changed, 44 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 816b5b90..02f79259 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,6 +10,8 @@ # All configuration values have a default; values that are commented out # serve to show the default. +from __future__ import annotations + import sys import os diff --git a/pyproject.toml b/pyproject.toml index e0bef18a..2f66b00f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,11 +65,15 @@ select = [ "W", # pycodestyle warnings "F", # pyflakes "UP", # pyupgrade + "I002", # missing-required-import ] ignore = [ "UP031", # printf-string-formatting, not enforced by pyupgrade "UP032", # f-string, not enforced by pyupgrade ] +[tool.ruff.lint.isort] +required-imports = ["from __future__ import annotations"] + [tool.ruff.lint.pycodestyle] max-line-length = 120 diff --git a/src/pytestqt/__init__.py b/src/pytestqt/__init__.py index 66b5a5f8..688f95d3 100644 --- a/src/pytestqt/__init__.py +++ b/src/pytestqt/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + # _version is automatically generated by setuptools_scm from pytestqt._version import version diff --git a/src/pytestqt/exceptions.py b/src/pytestqt/exceptions.py index 90b7f7c7..f69e8d2d 100644 --- a/src/pytestqt/exceptions.py +++ b/src/pytestqt/exceptions.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import functools import sys import traceback diff --git a/src/pytestqt/logging.py b/src/pytestqt/logging.py index 5ae4e0d5..10a701bd 100644 --- a/src/pytestqt/logging.py +++ b/src/pytestqt/logging.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from collections import namedtuple from contextlib import contextmanager import datetime diff --git a/src/pytestqt/modeltest.py b/src/pytestqt/modeltest.py index 6e364b49..e60609d0 100644 --- a/src/pytestqt/modeltest.py +++ b/src/pytestqt/modeltest.py @@ -41,6 +41,8 @@ # # $QT_END_LICENSE$ +from __future__ import annotations + import enum import collections diff --git a/src/pytestqt/plugin.py b/src/pytestqt/plugin.py index 72646164..207351ff 100644 --- a/src/pytestqt/plugin.py +++ b/src/pytestqt/plugin.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import warnings import pytest diff --git a/src/pytestqt/qt_compat.py b/src/pytestqt/qt_compat.py index d25e0a4e..4f3e6bf5 100644 --- a/src/pytestqt/qt_compat.py +++ b/src/pytestqt/qt_compat.py @@ -8,6 +8,8 @@ Based on from https://github.com/epage/PythonUtils. """ +from __future__ import annotations + from collections import namedtuple, OrderedDict import os import sys diff --git a/src/pytestqt/qtbot.py b/src/pytestqt/qtbot.py index fa24d3e0..df95fe07 100644 --- a/src/pytestqt/qtbot.py +++ b/src/pytestqt/qtbot.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from collections.abc import Callable import contextlib from types import TracebackType diff --git a/src/pytestqt/utils.py b/src/pytestqt/utils.py index b9e0a371..2f3495bc 100644 --- a/src/pytestqt/utils.py +++ b/src/pytestqt/utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Optional import pytest diff --git a/src/pytestqt/wait_signal.py b/src/pytestqt/wait_signal.py index 8361d446..11a0eddd 100644 --- a/src/pytestqt/wait_signal.py +++ b/src/pytestqt/wait_signal.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from collections.abc import Callable import functools import dataclasses diff --git a/tests/conftest.py b/tests/conftest.py index e09ea8b3..52978e7c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import time import pytest diff --git a/tests/test_basics.py b/tests/test_basics.py index 5f9fa014..7868171e 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import weakref import pytest diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 92628043..ca28f0b8 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys import pytest diff --git a/tests/test_logging.py b/tests/test_logging.py index 4a772ce4..b76e2e09 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime import pytest diff --git a/tests/test_modeltest.py b/tests/test_modeltest.py index 4c9084bb..cf73fbe0 100644 --- a/tests/test_modeltest.py +++ b/tests/test_modeltest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from pytestqt.qt_compat import qt_api diff --git a/tests/test_qtbot_pep8_aliases.py b/tests/test_qtbot_pep8_aliases.py index e208a9d2..8dc0cd92 100644 --- a/tests/test_qtbot_pep8_aliases.py +++ b/tests/test_qtbot_pep8_aliases.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import inspect from collections.abc import Iterator from unittest.mock import MagicMock diff --git a/tests/test_qtest_proxies.py b/tests/test_qtest_proxies.py index bae015c1..486d85b4 100644 --- a/tests/test_qtest_proxies.py +++ b/tests/test_qtest_proxies.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from pytestqt.qt_compat import qt_api diff --git a/tests/test_screenshot.py b/tests/test_screenshot.py index 4032873d..11439c11 100644 --- a/tests/test_screenshot.py +++ b/tests/test_screenshot.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pathlib import pytest diff --git a/tests/test_wait_signal.py b/tests/test_wait_signal.py index f498a764..453f95a0 100644 --- a/tests/test_wait_signal.py +++ b/tests/test_wait_signal.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import functools import fnmatch diff --git a/tests/test_wait_until.py b/tests/test_wait_until.py index 9884ffad..903fb8c0 100644 --- a/tests/test_wait_until.py +++ b/tests/test_wait_until.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from collections.abc import Callable, Iterator from typing import Optional From 75a39ee0c0c40286f579a14b103117fb20c974ef Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Tue, 29 Sep 2026 15:23:34 +0200 Subject: [PATCH 2/3] Use 'X | None' instead of 'Optional[X]' in annotations Runtime type aliases keep Optional, as they are still evaluated on Python 3.9. Refs #610 Co-Authored-By: Claude Opus 5.5 --- src/pytestqt/exceptions.py | 4 ++-- src/pytestqt/qtbot.py | 27 +++++++++++++-------------- src/pytestqt/utils.py | 3 +-- tests/test_wait_until.py | 7 +++---- 4 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/pytestqt/exceptions.py b/src/pytestqt/exceptions.py index f69e8d2d..bdc72dad 100644 --- a/src/pytestqt/exceptions.py +++ b/src/pytestqt/exceptions.py @@ -35,7 +35,7 @@ def capture_exceptions() -> Iterator[CapturedExceptions]: def _except_hook( type_: type[BaseException], value: BaseException, - tback: Optional[TracebackType], + tback: TracebackType | None, exceptions: CapturedExceptions, ) -> None: """Hook functions installed by _QtExceptionCaptureManager""" @@ -49,7 +49,7 @@ class _QtExceptionCaptureManager: """ def __init__(self) -> None: - self.old_hook: Optional[ExceptHook] = None + self.old_hook: ExceptHook | None = None self.exceptions: CapturedExceptions = [] def start(self) -> None: diff --git a/src/pytestqt/qtbot.py b/src/pytestqt/qtbot.py index df95fe07..3ef8a87c 100644 --- a/src/pytestqt/qtbot.py +++ b/src/pytestqt/qtbot.py @@ -8,7 +8,6 @@ from typing import ( TYPE_CHECKING, Literal, - Optional, Any, cast, ) @@ -192,7 +191,7 @@ def __init__(self, request: FixtureRequest) -> None: self.wait_until = self.waitUntil self.wait_callback = self.waitCallback - def _should_raise(self, raising_arg: Optional[bool]) -> bool: + def _should_raise(self, raising_arg: bool | None) -> bool: ini_val = self._request.config.getini("qt_default_raising") if raising_arg is not None: @@ -203,7 +202,7 @@ def _should_raise(self, raising_arg: Optional[bool]) -> bool: return True def addWidget( - self, widget: QWidget, *, before_close_func: Optional[BeforeCloseFunc] = None + self, widget: QWidget, *, before_close_func: BeforeCloseFunc | None = None ) -> None: """ Adds a widget to be tracked by this bot. This is not required, but will ensure that the @@ -338,8 +337,8 @@ def waitSignal( signal: SignalInstance, *, timeout: int = 5000, - raising: Optional[bool] = None, - check_params_cb: Optional[CheckParamsCb] = None, + raising: bool | None = None, + check_params_cb: CheckParamsCb | None = None, ) -> "SignalBlocker": """ .. versionadded:: 1.2 @@ -406,8 +405,8 @@ def waitSignals( signals: list[SignalInstance], *, timeout: int = 5000, - raising: Optional[bool] = None, - check_params_cbs: Optional[list[CheckParamsCb]] = None, + raising: bool | None = None, + check_params_cbs: list[CheckParamsCb] | None = None, order: WaitSignalsOrder = "none", ) -> "MultiSignalBlocker": """ @@ -528,7 +527,7 @@ def assertNotEmitted( spy.assert_not_emitted() def waitUntil( - self, callback: Callable[[], Optional[bool]], *, timeout: int = 5000 + self, callback: Callable[[], bool | None], *, timeout: int = 5000 ) -> None: """ .. versionadded:: 2.0 @@ -601,7 +600,7 @@ def timed_out(): self.wait(10) def waitCallback( - self, *, timeout: int = 5000, raising: Optional[bool] = None + self, *, timeout: int = 5000, raising: bool | None = None ) -> "CallbackBlocker": """ .. versionadded:: 3.1 @@ -669,7 +668,7 @@ def captureExceptions(self) -> Iterator["CapturedExceptions"]: yield exceptions def screenshot( - self, widget: QWidget, suffix: str = "", region: Optional[QRect] = None + self, widget: QWidget, suffix: str = "", region: QRect | None = None ) -> Path: """ .. versionadded:: 4.1 @@ -785,7 +784,7 @@ def _add_widget( item: pytest.Item, widget: QWidget, *, - before_close_func: Optional[BeforeCloseFunc] = None, + before_close_func: BeforeCloseFunc | None = None, ) -> None: """ Register a widget into the given pytest item for later closing. @@ -851,9 +850,9 @@ def __enter__(self) -> Self: def __exit__( self, - exc_type: Optional[type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> None: __tracebackhide__ = True try: diff --git a/src/pytestqt/utils.py b/src/pytestqt/utils.py index 2f3495bc..6ab5d662 100644 --- a/src/pytestqt/utils.py +++ b/src/pytestqt/utils.py @@ -1,11 +1,10 @@ from __future__ import annotations -from typing import Optional import pytest -def get_marker(item: pytest.Item, name: str) -> Optional["pytest.Mark"]: +def get_marker(item: pytest.Item, name: str) -> "pytest.Mark" | None: """Get a marker from a pytest item. This is here in order to stay compatible with pytest < 3.6 and not produce diff --git a/tests/test_wait_until.py b/tests/test_wait_until.py index 903fb8c0..5816fcff 100644 --- a/tests/test_wait_until.py +++ b/tests/test_wait_until.py @@ -1,7 +1,6 @@ from __future__ import annotations from collections.abc import Callable, Iterator -from typing import Optional import pytest @@ -32,7 +31,7 @@ def ticks(self) -> int: def test_wait_until( qtbot: QtBot, - wait_4_ticks_callback: Callable[[], Optional[bool]], + wait_4_ticks_callback: Callable[[], bool | None], tick_counter: Counter, ) -> None: tick_counter.start(100) @@ -42,7 +41,7 @@ def test_wait_until( def test_wait_until_timeout( qtbot: QtBot, - wait_4_ticks_callback: Callable[[], Optional[bool]], + wait_4_ticks_callback: Callable[[], bool | None], tick_counter: Counter, ) -> None: tick_counter.start(200) @@ -63,7 +62,7 @@ def test_pep8_alias(qtbot: QtBot) -> None: @pytest.fixture(params=["predicate", "assert"]) def wait_4_ticks_callback( request: pytest.FixtureRequest, tick_counter: Counter -) -> Callable[[], Optional[bool]]: +) -> Callable[[], bool | None]: """Parametrized fixture which returns the two possible callback methods that can be passed to ``waitUntil``: predicate and assertion. """ From a766f54dcef6edc692a7e82a542c7dddeb54aa27 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Tue, 29 Sep 2026 15:24:24 +0200 Subject: [PATCH 3/3] Remove quotes from forward references in annotations Refs #610 Co-Authored-By: Claude Opus 5.5 --- src/pytestqt/qtbot.py | 12 ++++++------ src/pytestqt/utils.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pytestqt/qtbot.py b/src/pytestqt/qtbot.py index 3ef8a87c..c62b73c0 100644 --- a/src/pytestqt/qtbot.py +++ b/src/pytestqt/qtbot.py @@ -223,7 +223,7 @@ def addWidget( def waitActive( self, widget: QWidget, *, timeout: int = 5000 - ) -> "_WaitWidgetContextManager": + ) -> _WaitWidgetContextManager: """ Context manager that waits for ``timeout`` milliseconds or until the window is active. If window is not exposed within ``timeout`` milliseconds, raise @@ -252,7 +252,7 @@ def waitActive( def waitExposed( self, widget: QWidget, *, timeout: int = 5000 - ) -> "_WaitWidgetContextManager": + ) -> _WaitWidgetContextManager: """ Context manager that waits for ``timeout`` milliseconds or until the window is exposed. If the window is not exposed within ``timeout`` milliseconds, raise @@ -339,7 +339,7 @@ def waitSignal( timeout: int = 5000, raising: bool | None = None, check_params_cb: CheckParamsCb | None = None, - ) -> "SignalBlocker": + ) -> SignalBlocker: """ .. versionadded:: 1.2 @@ -408,7 +408,7 @@ def waitSignals( raising: bool | None = None, check_params_cbs: list[CheckParamsCb] | None = None, order: WaitSignalsOrder = "none", - ) -> "MultiSignalBlocker": + ) -> MultiSignalBlocker: """ .. versionadded:: 1.4 @@ -601,7 +601,7 @@ def timed_out(): def waitCallback( self, *, timeout: int = 5000, raising: bool | None = None - ) -> "CallbackBlocker": + ) -> CallbackBlocker: """ .. versionadded:: 3.1 @@ -643,7 +643,7 @@ def waitCallback( return blocker @contextlib.contextmanager - def captureExceptions(self) -> Iterator["CapturedExceptions"]: + def captureExceptions(self) -> Iterator[CapturedExceptions]: """ .. versionadded:: 2.1 diff --git a/src/pytestqt/utils.py b/src/pytestqt/utils.py index 6ab5d662..d479e639 100644 --- a/src/pytestqt/utils.py +++ b/src/pytestqt/utils.py @@ -4,7 +4,7 @@ import pytest -def get_marker(item: pytest.Item, name: str) -> "pytest.Mark" | None: +def get_marker(item: pytest.Item, name: str) -> pytest.Mark | None: """Get a marker from a pytest item. This is here in order to stay compatible with pytest < 3.6 and not produce