From 0c57f88592c9131d4158f53acc0a371efec661d5 Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Thu, 22 May 2025 23:22:31 +0100 Subject: [PATCH 01/24] try import from qtpy --- src/pytestqt/qtbot.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/pytestqt/qtbot.py b/src/pytestqt/qtbot.py index 321b2a3..fb6cdf4 100644 --- a/src/pytestqt/qtbot.py +++ b/src/pytestqt/qtbot.py @@ -1,6 +1,7 @@ import contextlib import weakref import warnings +from typing import TYPE_CHECKING, Callable, Optional from pytestqt.exceptions import TimeoutError, ScreenshotError from pytestqt.qt_compat import qt_api @@ -13,6 +14,11 @@ CallbackCalledTwiceError, ) +if TYPE_CHECKING: + from qtpy.QtWidgets import QWidget + +BeforeCloseFunc = Callable[["QWidget"], None] + def _parse_ini_boolean(value): if value in (True, False): @@ -170,7 +176,9 @@ def _should_raise(self, raising_arg): else: return True - def addWidget(self, widget, *, before_close_func=None): + def addWidget( + self, widget: "QWidget", *, before_close_func: Optional[BeforeCloseFunc] = None + ): """ Adds a widget to be tracked by this bot. This is not required, but will ensure that the widget gets closed by the end of the test, so it is highly recommended. @@ -731,7 +739,12 @@ def mouseRelease(*args, **kwargs): QtBot.CallbackCalledTwiceError = CallbackCalledTwiceError -def _add_widget(item, widget, *, before_close_func=None): +def _add_widget( + item: object, + widget: "QWidget", + *, + before_close_func: Optional[BeforeCloseFunc] = None, +): """ Register a widget into the given pytest item for later closing. """ From 373ea8ddb53d40909d89b76cd2f7c9a160945529 Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Thu, 22 May 2025 23:27:27 +0100 Subject: [PATCH 02/24] mypy precommit --- .pre-commit-config.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7d4ae8e..eb465ce 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,3 +46,7 @@ repos: files: ^(HOWTORELEASE.rst|README.rst)$ language: python additional_dependencies: [pygments, restructuredtext_lint] +- repo: https://github.com/pre-commit/mirrors-mypy + rev: 'v1.15.0' + hooks: + - id: mypy From c0bb43894461f5f217e307d463bc54d1a6095af6 Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Thu, 22 May 2025 23:33:47 +0100 Subject: [PATCH 03/24] ignore attrdefined --- src/pytestqt/qtbot.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pytestqt/qtbot.py b/src/pytestqt/qtbot.py index fb6cdf4..996a0b0 100644 --- a/src/pytestqt/qtbot.py +++ b/src/pytestqt/qtbot.py @@ -733,14 +733,14 @@ def mouseRelease(*args, **kwargs): # provide easy access to exceptions to qtbot fixtures -QtBot.SignalEmittedError = SignalEmittedError -QtBot.TimeoutError = TimeoutError -QtBot.ScreenshotError = ScreenshotError -QtBot.CallbackCalledTwiceError = CallbackCalledTwiceError +QtBot.SignalEmittedError = SignalEmittedError # type: ignore[attr-defined] +QtBot.TimeoutError = TimeoutError # type: ignore[attr-defined] +QtBot.ScreenshotError = ScreenshotError # type: ignore[attr-defined] +QtBot.CallbackCalledTwiceError = CallbackCalledTwiceError # type: ignore[attr-defined] def _add_widget( - item: object, + item, widget: "QWidget", *, before_close_func: Optional[BeforeCloseFunc] = None, From 30f9b690595ccf11833abdb65563365aeb15c8bf Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Thu, 22 May 2025 23:38:50 +0100 Subject: [PATCH 04/24] only check src --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eb465ce..d634d66 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,3 +50,4 @@ repos: rev: 'v1.15.0' hooks: - id: mypy + args: [src, --ignore-missing-imports, --follow-imports=silent] From 0720c534adaa4ff0b96deba9977faaf2accc847b Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Thu, 22 May 2025 23:42:40 +0100 Subject: [PATCH 05/24] ignore more types --- .pre-commit-config.yaml | 1 - docs/conf.py | 2 +- tests/test_modeltest.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d634d66..eb465ce 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,4 +50,3 @@ repos: rev: 'v1.15.0' hooks: - id: mypy - args: [src, --ignore-missing-imports, --follow-imports=silent] diff --git a/docs/conf.py b/docs/conf.py index 816b5b9..b2b8b83 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -174,7 +174,7 @@ # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', -} +} # type: dict[str, str] # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). diff --git a/tests/test_modeltest.py b/tests/test_modeltest.py index c427a26..7ae2a9d 100644 --- a/tests/test_modeltest.py +++ b/tests/test_modeltest.py @@ -6,7 +6,7 @@ pytestmark = pytest.mark.usefixtures("qtbot") -class BasicModel(qt_api.QtCore.QAbstractItemModel): +class BasicModel(qt_api.QtCore.QAbstractItemModel): # type: ignore[name-defined] def data(self, index, role=qt_api.QtCore.Qt.ItemDataRole.DisplayRole): return None From 9128ce30b77e831b21d49e09d6580b542e2420f9 Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Sat, 31 May 2025 18:49:09 +0100 Subject: [PATCH 06/24] add pytyped files-partial --- src/pytestqt/py.typed | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/pytestqt/py.typed diff --git a/src/pytestqt/py.typed b/src/pytestqt/py.typed new file mode 100644 index 0000000..b648ac9 --- /dev/null +++ b/src/pytestqt/py.typed @@ -0,0 +1 @@ +partial From 9a8a4b5515bc97821ffcff5ae62b7107a25fb149 Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Sat, 31 May 2025 18:55:00 +0100 Subject: [PATCH 07/24] change qtpy to Any alias --- src/pytestqt/qtbot.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pytestqt/qtbot.py b/src/pytestqt/qtbot.py index 996a0b0..38469ab 100644 --- a/src/pytestqt/qtbot.py +++ b/src/pytestqt/qtbot.py @@ -1,7 +1,7 @@ import contextlib import weakref import warnings -from typing import TYPE_CHECKING, Callable, Optional +from typing import TYPE_CHECKING, Callable, Optional, Any from pytestqt.exceptions import TimeoutError, ScreenshotError from pytestqt.qt_compat import qt_api @@ -15,9 +15,11 @@ ) if TYPE_CHECKING: - from qtpy.QtWidgets import QWidget + # Type hint objects until figuring out how to import across qt + # versions possibly using 'qtpy' library. + QWidget = Any -BeforeCloseFunc = Callable[["QWidget"], None] +BeforeCloseFunc = Callable[[QWidget], None] def _parse_ini_boolean(value): From d9c8a5ca00fc143a72195db7ba563e858cae58a5 Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Mon, 2 Jun 2025 18:44:52 +0100 Subject: [PATCH 08/24] save --- src/pytestqt/qtbot.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/pytestqt/qtbot.py b/src/pytestqt/qtbot.py index 38469ab..e85f618 100644 --- a/src/pytestqt/qtbot.py +++ b/src/pytestqt/qtbot.py @@ -1,7 +1,7 @@ import contextlib import weakref import warnings -from typing import TYPE_CHECKING, Callable, Optional, Any +from typing import TYPE_CHECKING, Callable, Optional, Any, cast from pytestqt.exceptions import TimeoutError, ScreenshotError from pytestqt.qt_compat import qt_api @@ -19,14 +19,14 @@ # versions possibly using 'qtpy' library. QWidget = Any -BeforeCloseFunc = Callable[[QWidget], None] +BeforeCloseFunc = Callable[["QWidget"], None] -def _parse_ini_boolean(value): +def _parse_ini_boolean(value: bool | str) -> bool: if value in (True, False): - return value + return cast("bool", value) try: - return {"true": True, "false": False}[value.lower()] + return {"true": True, "false": False}[cast("str", value).lower()] except KeyError: raise ValueError("unknown string for bool: %r" % value) @@ -178,9 +178,7 @@ def _should_raise(self, raising_arg): else: return True - def addWidget( - self, widget: "QWidget", *, before_close_func: Optional[BeforeCloseFunc] = None - ): + def addWidget(self, widget, *, before_close_func: Optional[BeforeCloseFunc] = None): """ Adds a widget to be tracked by this bot. This is not required, but will ensure that the widget gets closed by the end of the test, so it is highly recommended. @@ -198,7 +196,7 @@ def addWidget( raise TypeError(f"Need to pass a QWidget to addWidget: {widget!r}") _add_widget(self._request.node, widget, before_close_func=before_close_func) - def waitActive(self, widget, *, timeout=5000): + def waitActive(self, widget, *, timeout: int = 5000): """ Context manager that waits for ``timeout`` milliseconds or until the window is active. If window is not exposed within ``timeout`` milliseconds, raise @@ -743,7 +741,7 @@ def mouseRelease(*args, **kwargs): def _add_widget( item, - widget: "QWidget", + widget, *, before_close_func: Optional[BeforeCloseFunc] = None, ): From acc1994d66593a027fd75f08f1303bc3fbf2070f Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Sun, 8 Jun 2025 21:08:54 +0100 Subject: [PATCH 09/24] add more typing --- src/pytestqt/__init__.py | 6 ++- src/pytestqt/exceptions.py | 4 ++ src/pytestqt/qt_compat.py | 2 +- src/pytestqt/qtbot.py | 93 ++++++++++++++++++++++--------------- src/pytestqt/wait_signal.py | 3 +- 5 files changed, 66 insertions(+), 42 deletions(-) diff --git a/src/pytestqt/__init__.py b/src/pytestqt/__init__.py index 7c6237c..7065722 100644 --- a/src/pytestqt/__init__.py +++ b/src/pytestqt/__init__.py @@ -1,4 +1,6 @@ +from typing import cast + # _version is automatically generated by setuptools_scm -from pytestqt._version import version +from pytestqt._version import version # type: ignore[import-not-found] -__version__ = version +__version__ = cast("str", version) diff --git a/src/pytestqt/exceptions.py b/src/pytestqt/exceptions.py index d342876..3b51f1e 100644 --- a/src/pytestqt/exceptions.py +++ b/src/pytestqt/exceptions.py @@ -2,10 +2,14 @@ import sys import traceback from contextlib import contextmanager +from types import TracebackType +from typing import List, Tuple, Type import pytest from pytestqt.utils import get_marker +CapturedException = Tuple[Type[BaseException], BaseException, TracebackType] +CapturedExceptions = List[CapturedException] @contextmanager def capture_exceptions(): diff --git a/src/pytestqt/qt_compat.py b/src/pytestqt/qt_compat.py index e66f369..acecb53 100644 --- a/src/pytestqt/qt_compat.py +++ b/src/pytestqt/qt_compat.py @@ -162,7 +162,7 @@ def exec(self, obj, *args, **kwargs): def get_versions(self): if self.pytest_qt_api == "pyside6": - import PySide6 + import PySide6 # type: ignore[import-not-found] version = PySide6.__version__ diff --git a/src/pytestqt/qtbot.py b/src/pytestqt/qtbot.py index e85f618..f48291d 100644 --- a/src/pytestqt/qtbot.py +++ b/src/pytestqt/qtbot.py @@ -1,7 +1,9 @@ import contextlib +from types import TracebackType import weakref import warnings -from typing import TYPE_CHECKING, Callable, Optional, Any, cast +from typing import TYPE_CHECKING, Callable, Generator, Iterator, List, Literal, Optional, Any, Self, Type, cast +from pathlib import Path from pytestqt.exceptions import TimeoutError, ScreenshotError from pytestqt.qt_compat import qt_api @@ -12,17 +14,27 @@ SignalEmittedError, CallbackBlocker, CallbackCalledTwiceError, + CheckParamsCb, ) +from pytest import FixtureRequest + +# Type hint objects until figuring out how to import across qt +# versions possibly using 'qtpy' library. +QWidget = Any +SignalInstance = Any +QRect = Any +QKeySequence = Any + if TYPE_CHECKING: - # Type hint objects until figuring out how to import across qt - # versions possibly using 'qtpy' library. - QWidget = Any + # Keep local import behavior the same. + from pytestqt.exceptions import CapturedExceptions -BeforeCloseFunc = Callable[["QWidget"], None] +BeforeCloseFunc = Callable[[QWidget], None] +WaitSignalsOrder = Literal["none", "simple", "strict"] -def _parse_ini_boolean(value: bool | str) -> bool: +def _parse_ini_boolean(value: Any) -> bool: if value in (True, False): return cast("bool", value) try: @@ -154,7 +166,7 @@ class QtBot: """ - def __init__(self, request): + def __init__(self, request: FixtureRequest) -> None: self._request = request # pep8 aliases. Set here to automatically use implementations defined in sub-classes for alias creation self.add_widget = self.addWidget @@ -168,7 +180,7 @@ def __init__(self, request): self.wait_until = self.waitUntil self.wait_callback = self.waitCallback - def _should_raise(self, raising_arg): + def _should_raise(self, raising_arg: Optional[bool]) -> bool: ini_val = self._request.config.getini("qt_default_raising") if raising_arg is not None: @@ -178,7 +190,7 @@ def _should_raise(self, raising_arg): else: return True - def addWidget(self, widget, *, before_close_func: Optional[BeforeCloseFunc] = None): + def addWidget(self, widget: QWidget, *, before_close_func: Optional[BeforeCloseFunc] = None) -> None: """ Adds a widget to be tracked by this bot. This is not required, but will ensure that the widget gets closed by the end of the test, so it is highly recommended. @@ -196,7 +208,7 @@ def addWidget(self, widget, *, before_close_func: Optional[BeforeCloseFunc] = No raise TypeError(f"Need to pass a QWidget to addWidget: {widget!r}") _add_widget(self._request.node, widget, before_close_func=before_close_func) - def waitActive(self, widget, *, timeout: int = 5000): + def waitActive(self, widget: QWidget, *, timeout: int = 5000) -> "_WaitWidgetContextManager": """ Context manager that waits for ``timeout`` milliseconds or until the window is active. If window is not exposed within ``timeout`` milliseconds, raise @@ -223,7 +235,7 @@ def waitActive(self, widget, *, timeout: int = 5000): "qWaitForWindowActive", "activated", widget, timeout ) - def waitExposed(self, widget, *, timeout=5000): + def waitExposed(self, widget: QWidget, *, timeout: int=5000) -> "_WaitWidgetContextManager": """ Context manager that waits for ``timeout`` milliseconds or until the window is exposed. If the window is not exposed within ``timeout`` milliseconds, raise @@ -250,7 +262,7 @@ def waitExposed(self, widget, *, timeout=5000): "qWaitForWindowExposed", "exposed", widget, timeout ) - def waitForWindowShown(self, widget): + def waitForWindowShown(self, widget: QWidget) -> bool: """ Waits until the window is shown in the screen. This is mainly useful for asynchronous systems like X11, where a window will be mapped to screen some time after being asked to @@ -282,7 +294,7 @@ def waitForWindowShown(self, widget): ) return qt_api.QtTest.QTest.qWaitForWindowExposed(widget) - def stop(self): + def stop(self) -> None: """ Stops the current test flow, letting the user interact with any visible widget. @@ -303,7 +315,14 @@ def stop(self): for widget, visible in widget_and_visibility: widget.setVisible(visible) - def waitSignal(self, signal, *, timeout=5000, raising=None, check_params_cb=None): + def waitSignal( + self, + signal: "SignalInstance", + *, + timeout: int = 5000, + raising: Optional[bool] = None, + check_params_cb: Optional[CheckParamsCb] = None, + ) -> "SignalBlocker": """ .. versionadded:: 1.2 @@ -366,13 +385,13 @@ def waitSignal(self, signal, *, timeout=5000, raising=None, check_params_cb=None def waitSignals( self, - signals, + signals: List[SignalInstance], *, - timeout=5000, - raising=None, - check_params_cbs=None, - order="none", - ): + timeout: int=5000, + raising:Optional[bool]=None, + check_params_cbs:Optional[List[CheckParamsCb]] =None, + order: WaitSignalsOrder="none", + ) -> "MultiSignalBlocker": """ .. versionadded:: 1.4 @@ -454,7 +473,7 @@ def waitSignals( blocker.add_signals(signals) return blocker - def wait(self, ms): + def wait(self, ms: int) -> None: """ .. versionadded:: 1.9 @@ -467,7 +486,7 @@ def wait(self, ms): blocker.wait() @contextlib.contextmanager - def assertNotEmitted(self, signal, *, wait=0): + def assertNotEmitted(self, signal: SignalInstance, *, wait: int=0) -> Generator[None, None, None]: """ .. versionadded:: 1.11 @@ -488,7 +507,7 @@ def assertNotEmitted(self, signal, *, wait=0): yield spy.assert_not_emitted() - def waitUntil(self, callback, *, timeout=5000): + def waitUntil(self, callback: Callable[[], Optional[bool]], *, timeout: int=5000) -> None: """ .. versionadded:: 2.0 @@ -559,7 +578,7 @@ def timed_out(): raise TimeoutError(timeout_msg) self.wait(10) - def waitCallback(self, *, timeout=5000, raising=None): + def waitCallback(self, *, timeout: int = 5000, raising: Optional[bool] = None) -> "CallbackBlocker": """ .. versionadded:: 3.1 @@ -601,7 +620,7 @@ def waitCallback(self, *, timeout=5000, raising=None): return blocker @contextlib.contextmanager - def captureExceptions(self): + def captureExceptions(self) -> Generator["CapturedExceptions", None, None]: """ .. versionadded:: 2.1 @@ -625,9 +644,7 @@ def captureExceptions(self): with capture_exceptions() as exceptions: yield exceptions - capture_exceptions = captureExceptions - - def screenshot(self, widget, suffix="", region=None): + def screenshot(self, widget: QWidget, suffix: str="", region: Optional[QRect]=None) -> Path: """ .. versionadded:: 4.1 @@ -700,13 +717,13 @@ def keyRelease(*args, **kwargs): qt_api.QtTest.QTest.keyRelease(*args, **kwargs) @staticmethod - def keySequence(widget, key_sequence): + def keySequence(widget: QWidget, key_sequence: QKeySequence) -> None: if not hasattr(qt_api.QtTest.QTest, "keySequence"): raise NotImplementedError("This method is available from Qt 5.10 upwards.") qt_api.QtTest.QTest.keySequence(widget, key_sequence) @staticmethod - def keyToAscii(key): + def keyToAscii(key: Any) -> None: if not hasattr(qt_api.QtTest.QTest, "keyToAscii"): raise NotImplementedError("This method isn't available on PyQt5.") qt_api.QtTest.QTest.keyToAscii(key) @@ -740,11 +757,11 @@ def mouseRelease(*args, **kwargs): def _add_widget( - item, - widget, + item: Any, + widget: QWidget, *, before_close_func: Optional[BeforeCloseFunc] = None, -): +) -> None: """ Register a widget into the given pytest item for later closing. """ @@ -753,7 +770,7 @@ def _add_widget( item.qt_widgets = qt_widgets -def _close_widgets(item): +def _close_widgets(item: Any) -> None: """ Close all widgets registered in the pytest item. """ @@ -769,7 +786,7 @@ def _close_widgets(item): del item.qt_widgets -def _iter_widgets(item): +def _iter_widgets(item: Any) -> Iterator[weakref.ReferenceType[QWidget]]: """ Iterates over widgets registered in the given pytest item. """ @@ -782,7 +799,7 @@ class _WaitWidgetContextManager: Context manager implementation used by ``waitActive`` and ``waitExposed`` methods. """ - def __init__(self, method_name, adjective_name, widget, timeout): + def __init__(self, method_name: str, adjective_name: str, widget: QWidget, timeout: int) -> None: """ :param str method_name: name to the ``QtTest`` method to call to check if widget is active/exposed. :param str adjective_name: "activated" or "exposed". @@ -794,11 +811,11 @@ def __init__(self, method_name, adjective_name, widget, timeout): self._widget = widget self._timeout = timeout - def __enter__(self): + def __enter__(self) -> Self: __tracebackhide__ = True return self - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__(self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType]) -> None: __tracebackhide__ = True try: if exc_type is None: diff --git a/src/pytestqt/wait_signal.py b/src/pytestqt/wait_signal.py index 359e744..c7a45a0 100644 --- a/src/pytestqt/wait_signal.py +++ b/src/pytestqt/wait_signal.py @@ -1,10 +1,11 @@ import functools import dataclasses -from typing import Any +from typing import Any, Callable from pytestqt.exceptions import TimeoutError from pytestqt.qt_compat import qt_api +CheckParamsCb = Callable[..., bool] class _AbstractSignalBlocker: """ From 97c08fe0f7a6bffb17c92346523e24d2e22fcb79 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 8 Jun 2025 20:09:12 +0000 Subject: [PATCH 10/24] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/pytestqt/exceptions.py | 1 + src/pytestqt/qtbot.py | 61 ++++++++++++++++++++++++++++--------- src/pytestqt/wait_signal.py | 1 + 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/src/pytestqt/exceptions.py b/src/pytestqt/exceptions.py index 3b51f1e..243e272 100644 --- a/src/pytestqt/exceptions.py +++ b/src/pytestqt/exceptions.py @@ -11,6 +11,7 @@ CapturedException = Tuple[Type[BaseException], BaseException, TracebackType] CapturedExceptions = List[CapturedException] + @contextmanager def capture_exceptions(): """ diff --git a/src/pytestqt/qtbot.py b/src/pytestqt/qtbot.py index f48291d..81828e5 100644 --- a/src/pytestqt/qtbot.py +++ b/src/pytestqt/qtbot.py @@ -2,7 +2,19 @@ from types import TracebackType import weakref import warnings -from typing import TYPE_CHECKING, Callable, Generator, Iterator, List, Literal, Optional, Any, Self, Type, cast +from typing import ( + TYPE_CHECKING, + Callable, + Generator, + Iterator, + List, + Literal, + Optional, + Any, + Self, + Type, + cast, +) from pathlib import Path from pytestqt.exceptions import TimeoutError, ScreenshotError @@ -190,7 +202,9 @@ def _should_raise(self, raising_arg: Optional[bool]) -> bool: else: return True - def addWidget(self, widget: QWidget, *, before_close_func: Optional[BeforeCloseFunc] = None) -> None: + def addWidget( + self, widget: QWidget, *, before_close_func: Optional[BeforeCloseFunc] = None + ) -> None: """ Adds a widget to be tracked by this bot. This is not required, but will ensure that the widget gets closed by the end of the test, so it is highly recommended. @@ -208,7 +222,9 @@ def addWidget(self, widget: QWidget, *, before_close_func: Optional[BeforeCloseF raise TypeError(f"Need to pass a QWidget to addWidget: {widget!r}") _add_widget(self._request.node, widget, before_close_func=before_close_func) - def waitActive(self, widget: QWidget, *, timeout: int = 5000) -> "_WaitWidgetContextManager": + def waitActive( + self, widget: QWidget, *, timeout: int = 5000 + ) -> "_WaitWidgetContextManager": """ Context manager that waits for ``timeout`` milliseconds or until the window is active. If window is not exposed within ``timeout`` milliseconds, raise @@ -235,7 +251,9 @@ def waitActive(self, widget: QWidget, *, timeout: int = 5000) -> "_WaitWidgetCon "qWaitForWindowActive", "activated", widget, timeout ) - def waitExposed(self, widget: QWidget, *, timeout: int=5000) -> "_WaitWidgetContextManager": + def waitExposed( + self, widget: QWidget, *, timeout: int = 5000 + ) -> "_WaitWidgetContextManager": """ Context manager that waits for ``timeout`` milliseconds or until the window is exposed. If the window is not exposed within ``timeout`` milliseconds, raise @@ -387,10 +405,10 @@ def waitSignals( self, signals: List[SignalInstance], *, - timeout: int=5000, - raising:Optional[bool]=None, - check_params_cbs:Optional[List[CheckParamsCb]] =None, - order: WaitSignalsOrder="none", + timeout: int = 5000, + raising: Optional[bool] = None, + check_params_cbs: Optional[List[CheckParamsCb]] = None, + order: WaitSignalsOrder = "none", ) -> "MultiSignalBlocker": """ .. versionadded:: 1.4 @@ -486,7 +504,9 @@ def wait(self, ms: int) -> None: blocker.wait() @contextlib.contextmanager - def assertNotEmitted(self, signal: SignalInstance, *, wait: int=0) -> Generator[None, None, None]: + def assertNotEmitted( + self, signal: SignalInstance, *, wait: int = 0 + ) -> Generator[None, None, None]: """ .. versionadded:: 1.11 @@ -507,7 +527,9 @@ def assertNotEmitted(self, signal: SignalInstance, *, wait: int=0) -> Generator[ yield spy.assert_not_emitted() - def waitUntil(self, callback: Callable[[], Optional[bool]], *, timeout: int=5000) -> None: + def waitUntil( + self, callback: Callable[[], Optional[bool]], *, timeout: int = 5000 + ) -> None: """ .. versionadded:: 2.0 @@ -578,7 +600,9 @@ def timed_out(): raise TimeoutError(timeout_msg) self.wait(10) - def waitCallback(self, *, timeout: int = 5000, raising: Optional[bool] = None) -> "CallbackBlocker": + def waitCallback( + self, *, timeout: int = 5000, raising: Optional[bool] = None + ) -> "CallbackBlocker": """ .. versionadded:: 3.1 @@ -644,7 +668,9 @@ def captureExceptions(self) -> Generator["CapturedExceptions", None, None]: with capture_exceptions() as exceptions: yield exceptions - def screenshot(self, widget: QWidget, suffix: str="", region: Optional[QRect]=None) -> Path: + def screenshot( + self, widget: QWidget, suffix: str = "", region: Optional[QRect] = None + ) -> Path: """ .. versionadded:: 4.1 @@ -799,7 +825,9 @@ class _WaitWidgetContextManager: Context manager implementation used by ``waitActive`` and ``waitExposed`` methods. """ - def __init__(self, method_name: str, adjective_name: str, widget: QWidget, timeout: int) -> None: + def __init__( + self, method_name: str, adjective_name: str, widget: QWidget, timeout: int + ) -> None: """ :param str method_name: name to the ``QtTest`` method to call to check if widget is active/exposed. :param str adjective_name: "activated" or "exposed". @@ -815,7 +843,12 @@ def __enter__(self) -> Self: __tracebackhide__ = True return self - def __exit__(self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType]) -> None: + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: __tracebackhide__ = True try: if exc_type is None: diff --git a/src/pytestqt/wait_signal.py b/src/pytestqt/wait_signal.py index c7a45a0..7d7f4f6 100644 --- a/src/pytestqt/wait_signal.py +++ b/src/pytestqt/wait_signal.py @@ -7,6 +7,7 @@ CheckParamsCb = Callable[..., bool] + class _AbstractSignalBlocker: """ Base class for :class:`SignalBlocker` and :class:`MultiSignalBlocker`. From 4aa9d93b43b7b826e62293e89800796ab616342a Mon Sep 17 00:00:00 2001 From: herobank110 Date: Thu, 12 Jun 2025 20:14:22 +0100 Subject: [PATCH 11/24] update to mypy 1.16 Co-authored-by: Bruno Oliveira --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eb465ce..b04242c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,6 +47,6 @@ repos: language: python additional_dependencies: [pygments, restructuredtext_lint] - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v1.15.0' + rev: 'v1.16.0' hooks: - id: mypy From 786aa1edd8a6e9efbba68a7d39778cd4b68ff7e5 Mon Sep 17 00:00:00 2001 From: herobank110 Date: Thu, 12 Jun 2025 20:25:51 +0100 Subject: [PATCH 12/24] change Generator to Iterator Co-authored-by: Bruno Oliveira --- src/pytestqt/qtbot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pytestqt/qtbot.py b/src/pytestqt/qtbot.py index 81828e5..cba1962 100644 --- a/src/pytestqt/qtbot.py +++ b/src/pytestqt/qtbot.py @@ -644,7 +644,7 @@ def waitCallback( return blocker @contextlib.contextmanager - def captureExceptions(self) -> Generator["CapturedExceptions", None, None]: + def captureExceptions(self) -> Iterator["CapturedExceptions"]: """ .. versionadded:: 2.1 From 2d0adb1884b43f1d900a54535f675644e0024bcd Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Thu, 12 Jun 2025 20:32:19 +0100 Subject: [PATCH 13/24] move error types to classvars --- src/pytestqt/qtbot.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pytestqt/qtbot.py b/src/pytestqt/qtbot.py index cba1962..61e6d9f 100644 --- a/src/pytestqt/qtbot.py +++ b/src/pytestqt/qtbot.py @@ -774,12 +774,11 @@ def mousePress(*args, **kwargs): def mouseRelease(*args, **kwargs): qt_api.QtTest.QTest.mouseRelease(*args, **kwargs) - -# provide easy access to exceptions to qtbot fixtures -QtBot.SignalEmittedError = SignalEmittedError # type: ignore[attr-defined] -QtBot.TimeoutError = TimeoutError # type: ignore[attr-defined] -QtBot.ScreenshotError = ScreenshotError # type: ignore[attr-defined] -QtBot.CallbackCalledTwiceError = CallbackCalledTwiceError # type: ignore[attr-defined] + # provide easy access to exceptions to qtbot fixtures + SignalEmittedError = SignalEmittedError + TimeoutError = TimeoutError + ScreenshotError = ScreenshotError + CallbackCalledTwiceError = CallbackCalledTwiceError def _add_widget( From 7bdb09928bbeaa3596942d4522bb45132fb8dc50 Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Thu, 12 Jun 2025 20:32:44 +0100 Subject: [PATCH 14/24] add mypy config file --- mypy.ini | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 mypy.ini diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..04449c8 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,9 @@ +[mypy] +files = src,tests +no_implicit_optional = True +pretty = True +show_error_codes = True +strict_equality = True +warn_redundant_casts = True +warn_unused_configs = True +warn_unused_ignores = True From a55f75dccad20fde0db1348ae9b9bd54b9b624eb Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Fri, 13 Jun 2025 11:45:40 +0100 Subject: [PATCH 15/24] exclude docs, remove type ignore comment --- .pre-commit-config.yaml | 1 + docs/conf.py | 2 +- mypy.ini | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a9f616b..14f34de 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,3 +50,4 @@ repos: rev: 'v1.16.0' hooks: - id: mypy + exclude: ^docs/ diff --git a/docs/conf.py b/docs/conf.py index b2b8b83..816b5b9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -174,7 +174,7 @@ # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', -} # type: dict[str, str] +} # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). diff --git a/mypy.ini b/mypy.ini index 04449c8..c0847cb 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,5 +1,5 @@ [mypy] -files = src,tests +exclude = ^docs/ no_implicit_optional = True pretty = True show_error_codes = True From 3b95d0e9c2f5711e82af6dab60cb1e637875fec9 Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Fri, 13 Jun 2025 11:47:08 +0100 Subject: [PATCH 16/24] change cast to annotation --- src/pytestqt/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pytestqt/__init__.py b/src/pytestqt/__init__.py index 7065722..66b5a5f 100644 --- a/src/pytestqt/__init__.py +++ b/src/pytestqt/__init__.py @@ -1,6 +1,4 @@ -from typing import cast - # _version is automatically generated by setuptools_scm -from pytestqt._version import version # type: ignore[import-not-found] +from pytestqt._version import version -__version__ = cast("str", version) +__version__: str = version From 9ac08edfaf57dac780a179f85ca453a992b52cf5 Mon Sep 17 00:00:00 2001 From: herobank110 Date: Fri, 13 Jun 2025 11:49:51 +0100 Subject: [PATCH 17/24] change cast str to str conversion Co-authored-by: Bruno Oliveira --- src/pytestqt/qtbot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pytestqt/qtbot.py b/src/pytestqt/qtbot.py index 61e6d9f..c3c0076 100644 --- a/src/pytestqt/qtbot.py +++ b/src/pytestqt/qtbot.py @@ -50,7 +50,7 @@ def _parse_ini_boolean(value: Any) -> bool: if value in (True, False): return cast("bool", value) try: - return {"true": True, "false": False}[cast("str", value).lower()] + return {"true": True, "false": False}[str(value).lower()] except KeyError: raise ValueError("unknown string for bool: %r" % value) From 58c1545f757e207576641b3d11eebd6a9bc23db0 Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Fri, 13 Jun 2025 11:55:03 +0100 Subject: [PATCH 18/24] ignore unused ignore --- src/pytestqt/qt_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pytestqt/qt_compat.py b/src/pytestqt/qt_compat.py index acecb53..49c4e80 100644 --- a/src/pytestqt/qt_compat.py +++ b/src/pytestqt/qt_compat.py @@ -162,7 +162,7 @@ def exec(self, obj, *args, **kwargs): def get_versions(self): if self.pytest_qt_api == "pyside6": - import PySide6 # type: ignore[import-not-found] + import PySide6 # type: ignore[import-not-found,unused-ignore] version = PySide6.__version__ From 3951f6f1543443c8a722997809e206441e4a8469 Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Fri, 13 Jun 2025 11:58:04 +0100 Subject: [PATCH 19/24] type alias annotation for qt objects --- src/pytestqt/qtbot.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pytestqt/qtbot.py b/src/pytestqt/qtbot.py index c3c0076..d1fef52 100644 --- a/src/pytestqt/qtbot.py +++ b/src/pytestqt/qtbot.py @@ -13,6 +13,7 @@ Any, Self, Type, + TypeAlias, cast, ) from pathlib import Path @@ -33,10 +34,10 @@ # Type hint objects until figuring out how to import across qt # versions possibly using 'qtpy' library. -QWidget = Any -SignalInstance = Any -QRect = Any -QKeySequence = Any +QWidget: TypeAlias = Any +SignalInstance: TypeAlias = Any +QRect: TypeAlias = Any +QKeySequence: TypeAlias = Any if TYPE_CHECKING: # Keep local import behavior the same. From 4a0acb54b1adc0f193764ae190ec4f5ba1cb8d9e Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Fri, 13 Jun 2025 12:02:25 +0100 Subject: [PATCH 20/24] remove quotes from type --- src/pytestqt/qtbot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pytestqt/qtbot.py b/src/pytestqt/qtbot.py index d1fef52..cb6a168 100644 --- a/src/pytestqt/qtbot.py +++ b/src/pytestqt/qtbot.py @@ -336,7 +336,7 @@ def stop(self) -> None: def waitSignal( self, - signal: "SignalInstance", + signal: SignalInstance, *, timeout: int = 5000, raising: Optional[bool] = None, From fd4229ec4d40caf94214825985b2befd13025088 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 13 Jun 2025 12:15:21 -0300 Subject: [PATCH 21/24] Update CHANGELOG.rst --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 88aeb9b..963a433 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,7 @@ UNRELEASED - Added official support for Python 3.13. - Dropped support for EOL Python 3.8. - Dropped support for EOL PySide 2. +- Type annotations are now provided. Note that because the Qt library used is defined at runtime, Qt classes are currently annotated as ``Any``. - Fixed PySide6 exceptions / warnings about being unable to disconnect signals with ``qtbot.waitSignal`` (`#552`_, `#558`_). - Reduced the likelyhood of trouble when using ``qtbot.waitSignal(s)`` and From 794bdad95aa27dd54f2da1d215a9cd661bf1edb0 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 13 Jun 2025 12:56:52 -0300 Subject: [PATCH 22/24] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index cc64bf2..a84a288 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ packages=find_packages(where="src"), package_dir={"": "src"}, entry_points={"pytest11": ["pytest-qt = pytestqt.plugin"]}, - install_requires=["pytest", "pluggy>=1.1"], + install_requires=["pytest", "pluggy>=1.1", "typing_extensions"], extras_require={ "doc": ["sphinx", "sphinx_rtd_theme"], "dev": ["pre-commit", "tox"], From bd3f70a631937fe411bfdf66388a2f616b68005a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 13 Jun 2025 12:57:57 -0300 Subject: [PATCH 23/24] Update qtbot.py --- src/pytestqt/qtbot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pytestqt/qtbot.py b/src/pytestqt/qtbot.py index cb6a168..1ad83f9 100644 --- a/src/pytestqt/qtbot.py +++ b/src/pytestqt/qtbot.py @@ -11,12 +11,12 @@ Literal, Optional, Any, - Self, Type, TypeAlias, cast, ) from pathlib import Path +from typing_extensions import Self from pytestqt.exceptions import TimeoutError, ScreenshotError from pytestqt.qt_compat import qt_api From dd07768d9f3dbb4b72bcaa4d272dedc8c25d30dd Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 13 Jun 2025 13:00:34 -0300 Subject: [PATCH 24/24] Update qtbot.py --- src/pytestqt/qtbot.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pytestqt/qtbot.py b/src/pytestqt/qtbot.py index 1ad83f9..a68c159 100644 --- a/src/pytestqt/qtbot.py +++ b/src/pytestqt/qtbot.py @@ -12,11 +12,10 @@ Optional, Any, Type, - TypeAlias, cast, ) from pathlib import Path -from typing_extensions import Self +from typing_extensions import Self, TypeAlias from pytestqt.exceptions import TimeoutError, ScreenshotError from pytestqt.qt_compat import qt_api