From 9ba8b5fffcf7b78bde85c0e5a81a8248579d1323 Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Thu, 22 May 2025 23:22:31 +0100 Subject: [PATCH 01/28] 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 cc46c75b86d0e8be8ae2912a795a5ba994a74ce9 Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Thu, 22 May 2025 23:27:27 +0100 Subject: [PATCH 02/28] 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 fb2b142..2ef90d5 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 5d7df2fa9dca2c986488c9f42960d75ac8f0cbad Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Thu, 22 May 2025 23:33:47 +0100 Subject: [PATCH 03/28] 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 a792e9faa7b07c2a71e3d3692b7f53b9144bacdb Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Thu, 22 May 2025 23:38:50 +0100 Subject: [PATCH 04/28] 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 2ef90d5..9f1d7f0 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 37218053d870bdb71b811cf766a468e5fd8185eb Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Thu, 22 May 2025 23:42:40 +0100 Subject: [PATCH 05/28] 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 9f1d7f0..2ef90d5 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 1102449b1d5144d647189fb5b7230bad469aaa9f Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Sat, 31 May 2025 18:49:09 +0100 Subject: [PATCH 06/28] 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 03f68e7e2e7bd3701f6f4ee558d664f71e473b50 Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Sat, 31 May 2025 18:55:00 +0100 Subject: [PATCH 07/28] 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 91570c29ffdead183322f8886a783446d57deb2f Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Mon, 2 Jun 2025 18:44:52 +0100 Subject: [PATCH 08/28] 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 9b734903935ff9869bc66b67a0c9cea3abd1e627 Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Sun, 8 Jun 2025 21:08:54 +0100 Subject: [PATCH 09/28] 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 6fe03d89254b3e64ffc7e1cf87a711e9e49a540a 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/28] [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 4402f355eaa0723c537da2722a525e28eeea2bcc Mon Sep 17 00:00:00 2001 From: herobank110 Date: Thu, 12 Jun 2025 20:14:22 +0100 Subject: [PATCH 11/28] 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 2ef90d5..a9f616b 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 cc71bc660b87bd22c41d4f773e1540f1862dfea6 Mon Sep 17 00:00:00 2001 From: herobank110 Date: Thu, 12 Jun 2025 20:25:51 +0100 Subject: [PATCH 12/28] 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 696492ba4c74d8db13b734dd38a6941a5a1c5078 Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Thu, 12 Jun 2025 20:32:19 +0100 Subject: [PATCH 13/28] 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 9be150de3359ae340452cb8c2a5a9dfefd415c79 Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Thu, 12 Jun 2025 20:32:44 +0100 Subject: [PATCH 14/28] 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 daa953b17dfd1dddfc490ccfaa3cd6a05e3bd84a Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Fri, 13 Jun 2025 11:45:40 +0100 Subject: [PATCH 15/28] 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 f4cbcfa455943a7b4f5f260932c840196c6e7dec Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Fri, 13 Jun 2025 11:47:08 +0100 Subject: [PATCH 16/28] 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 054f95f7d48a5db9fc7aa25050bfb70f4d9a6bef Mon Sep 17 00:00:00 2001 From: herobank110 Date: Fri, 13 Jun 2025 11:49:51 +0100 Subject: [PATCH 17/28] 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 2ea1012b06f41ab4cf9996eadbfa63bcb597de1f Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Fri, 13 Jun 2025 11:55:03 +0100 Subject: [PATCH 18/28] 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 9bc38ef5b74b5edb6e42ab76aea8173faa0f36ba Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Fri, 13 Jun 2025 11:58:04 +0100 Subject: [PATCH 19/28] 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 ce166ead071c5d10f562d66ff768bb944b50b906 Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Fri, 13 Jun 2025 12:02:25 +0100 Subject: [PATCH 20/28] 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 280baef0b392641bee9db32fa999f95e6f0001b3 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 13 Jun 2025 12:15:21 -0300 Subject: [PATCH 21/28] 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 8217be69ee000beaaeb0d212a0ded8bb5d04a459 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 13 Jun 2025 12:56:52 -0300 Subject: [PATCH 22/28] 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 9548e72a92f5e08a3f61318997760f26f233462e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 13 Jun 2025 12:57:57 -0300 Subject: [PATCH 23/28] 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 39ae0c3c64ec1f2fcf8a22c7226cd0f6a4ad1591 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 13 Jun 2025 13:00:34 -0300 Subject: [PATCH 24/28] 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 From f66f6880f01f69fc074419bd619449ad14085c74 Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Mon, 23 Jun 2025 07:43:32 +0100 Subject: [PATCH 25/28] remove already default config --- mypy.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/mypy.ini b/mypy.ini index c0847cb..b4dc48d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,6 +1,5 @@ [mypy] exclude = ^docs/ -no_implicit_optional = True pretty = True show_error_codes = True strict_equality = True From ae7381cfcdbea8fddf3a6d99946dfeeae7619cff Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Mon, 23 Jun 2025 08:17:42 +0100 Subject: [PATCH 26/28] remove deprecated annotations --- src/pytestqt/exceptions.py | 5 ++--- src/pytestqt/qtbot.py | 32 +++++++++++++++++++------------- src/pytestqt/wait_signal.py | 3 ++- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/pytestqt/exceptions.py b/src/pytestqt/exceptions.py index 243e272..ef3c7da 100644 --- a/src/pytestqt/exceptions.py +++ b/src/pytestqt/exceptions.py @@ -3,13 +3,12 @@ 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] +CapturedException = tuple[type[BaseException], BaseException, TracebackType] +CapturedExceptions = list[CapturedException] @contextmanager diff --git a/src/pytestqt/qtbot.py b/src/pytestqt/qtbot.py index a68c159..95d686b 100644 --- a/src/pytestqt/qtbot.py +++ b/src/pytestqt/qtbot.py @@ -1,20 +1,19 @@ +from collections.abc import Callable import contextlib from types import TracebackType import weakref import warnings from typing import ( TYPE_CHECKING, - Callable, Generator, Iterator, - List, Literal, Optional, Any, - Type, cast, ) from pathlib import Path +import pytest from typing_extensions import Self, TypeAlias from pytestqt.exceptions import TimeoutError, ScreenshotError @@ -403,11 +402,11 @@ def waitSignal( def waitSignals( self, - signals: List[SignalInstance], + signals: list[SignalInstance], *, timeout: int = 5000, raising: Optional[bool] = None, - check_params_cbs: Optional[List[CheckParamsCb]] = None, + check_params_cbs: Optional[list[CheckParamsCb]] = None, order: WaitSignalsOrder = "none", ) -> "MultiSignalBlocker": """ @@ -782,7 +781,7 @@ def mouseRelease(*args, **kwargs): def _add_widget( - item: Any, + item: pytest.Item, widget: QWidget, *, before_close_func: Optional[BeforeCloseFunc] = None, @@ -792,26 +791,26 @@ def _add_widget( """ qt_widgets = getattr(item, "qt_widgets", []) qt_widgets.append((weakref.ref(widget), before_close_func)) - item.qt_widgets = qt_widgets + item.qt_widgets = qt_widgets # type: ignore[assignment] -def _close_widgets(item: Any) -> None: +def _close_widgets(item: pytest.Item) -> None: """ Close all widgets registered in the pytest item. """ widgets = getattr(item, "qt_widgets", None) if widgets: - for w, before_close_func in item.qt_widgets: + for w, before_close_func in item.qt_widgets: # type: ignore[attr-defined] w = w() if w is not None: if before_close_func is not None: before_close_func(w) w.close() w.deleteLater() - del item.qt_widgets + del item.qt_widgets # type: ignore[attr-defined] -def _iter_widgets(item: Any) -> Iterator[weakref.ReferenceType[QWidget]]: +def _iter_widgets(item: pytest.Item) -> Iterator[weakref.ReferenceType[QWidget]]: """ Iterates over widgets registered in the given pytest item. """ @@ -819,13 +818,20 @@ def _iter_widgets(item: Any) -> Iterator[weakref.ReferenceType[QWidget]]: return (w for (w, _) in qt_widgets) +WaitAdjectiveName = Literal["activated", "exposed"] + + class _WaitWidgetContextManager: """ Context manager implementation used by ``waitActive`` and ``waitExposed`` methods. """ def __init__( - self, method_name: str, adjective_name: str, widget: QWidget, timeout: int + self, + method_name: str, + adjective_name: WaitAdjectiveName, + widget: QWidget, + timeout: int, ) -> None: """ :param str method_name: name to the ``QtTest`` method to call to check if widget is active/exposed. @@ -844,7 +850,7 @@ def __enter__(self) -> Self: def __exit__( self, - exc_type: Optional[Type[BaseException]], + exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> None: diff --git a/src/pytestqt/wait_signal.py b/src/pytestqt/wait_signal.py index 7d7f4f6..da98228 100644 --- a/src/pytestqt/wait_signal.py +++ b/src/pytestqt/wait_signal.py @@ -1,6 +1,7 @@ +from collections.abc import Callable import functools import dataclasses -from typing import Any, Callable +from typing import Any from pytestqt.exceptions import TimeoutError from pytestqt.qt_compat import qt_api From fbb1ddb9820f29e9265cc52305083d8eb5aabc9c Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Mon, 23 Jun 2025 08:22:52 +0100 Subject: [PATCH 27/28] pytest dependency for mypy --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 14f34de..b03291a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,4 +50,5 @@ repos: rev: 'v1.16.0' hooks: - id: mypy + additional_dependencies: [pytest] exclude: ^docs/ From 5e4dbd5c08376ceaf82149fb2ed1afd18e13bc85 Mon Sep 17 00:00:00 2001 From: David Kanekanian Date: Mon, 23 Jun 2025 08:28:43 +0100 Subject: [PATCH 28/28] fix wrong type ignore --- 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 95d686b..a8cfdfb 100644 --- a/src/pytestqt/qtbot.py +++ b/src/pytestqt/qtbot.py @@ -791,7 +791,7 @@ def _add_widget( """ qt_widgets = getattr(item, "qt_widgets", []) qt_widgets.append((weakref.ref(widget), before_close_func)) - item.qt_widgets = qt_widgets # type: ignore[assignment] + item.qt_widgets = qt_widgets # type: ignore[attr-defined] def _close_widgets(item: pytest.Item) -> None: