diff --git a/docs/conf.py b/docs/conf.py index 02f7925..fbb9477 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -29,6 +29,20 @@ # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ["sphinx.ext.autodoc", "sphinx.ext.coverage", "sphinx.ext.viewcode"] +# Show the type annotations in the parameter descriptions. +autodoc_typehints = "description" +autodoc_typehints_description_target = "documented" + +# Qt classes are type-aliased to ``Any`` in the source, keep their names in the docs. +# Sphinx 9 does not resolve these aliases when nested (``QRect | None``), such +# parameters keep a ``:type:`` field in their docstring. +autodoc_type_aliases = { + "QWidget": "QWidget", + "QRect": "QRect", + "QKeySequence": "QKeySequence", + "SignalInstance": "Signal", +} + # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] diff --git a/src/pytestqt/logging.py b/src/pytestqt/logging.py index 10a701b..44d85a6 100644 --- a/src/pytestqt/logging.py +++ b/src/pytestqt/logging.py @@ -126,7 +126,7 @@ class _QtMessageCapture: """ def __init__(self, ignore_regexes): - self._records = [] + self._records: list[Record] = [] self._ignore_regexes = ignore_regexes or [] self._previous_handler = None @@ -200,11 +200,8 @@ def _handle_with_context(self, msg_type, context, message): self._append_new_record(msg_type, message, context=context) @property - def records(self): - """Access messages captured so far. - - :rtype: list of `Record` instances. - """ + def records(self) -> list[Record]: + """Access messages captured so far.""" return self._records[:] diff --git a/src/pytestqt/qtbot.py b/src/pytestqt/qtbot.py index c62b73c..7127d34 100644 --- a/src/pytestqt/qtbot.py +++ b/src/pytestqt/qtbot.py @@ -26,6 +26,7 @@ CallbackBlocker, CallbackCalledTwiceError, CheckParamsCb, + SignalInstance, ) from pytest import FixtureRequest @@ -33,7 +34,6 @@ # Type hint objects until figuring out how to import across qt # versions possibly using 'qtpy' library. QWidget: TypeAlias = Any -SignalInstance: TypeAlias = Any QRect: TypeAlias = Any QKeySequence: TypeAlias = Any @@ -208,7 +208,7 @@ def addWidget( 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. - :param QWidget widget: + :param widget: Widget to keep track of. :kwparam before_close_func: @@ -237,10 +237,10 @@ def waitActive( with qtbot.waitActive(widget, timeout=500): show_action() - :param QWidget widget: + :param widget: Widget to wait for. - :param int|None timeout: + :param timeout: How many milliseconds to wait for. .. note:: This method is also available as ``wait_active`` (pep-8 alias) @@ -266,10 +266,10 @@ def waitExposed( with qtbot.waitExposed(splash, timeout=500): startup() - :param QWidget widget: + :param widget: Widget to wait for. - :param int|None timeout: + :param timeout: How many milliseconds to wait for. .. note:: This method is also available as ``wait_exposed`` (pep-8 alias) @@ -292,7 +292,7 @@ def waitForWindowShown(self, widget: QWidget) -> bool: .. deprecated:: 4.0 Use the ``qtbot.waitExposed`` context manager instead. - :param QWidget widget: + :param widget: Widget to wait on. :returns: @@ -369,17 +369,17 @@ def waitSignal( .. versionadded:: 2.0 The *check_params_cb* parameter. - :param Signal signal: + :param signal: A signal to wait for, or a tuple ``(signal, signal_name_as_str)`` to improve the error message that is part of :class:`qtbot.TimeoutError `. - :param int timeout: + :param timeout: How many milliseconds to wait before resuming control flow. - :param bool raising: + :param raising: If :class:`qtbot.TimeoutError ` should be raised if a timeout occurred. This defaults to ``True`` unless ``qt_default_raising = false`` is set in the config. - :param Callable check_params_cb: + :param check_params_cb: Optional ``callable`` that compares the provided signal parameters to some expected parameters. It has to match the signature of ``signal`` (just like a slot function would) and return ``True`` if parameters match, ``False`` otherwise. @@ -430,24 +430,25 @@ def waitSignals( long_function_that_calls_signal() blocker.wait() - :param list signals: + :param signals: A list of :class:`Signal` objects to wait for. Alternatively: a list of (``Signal, str``) tuples of the form ``(signal, signal_name_as_str)`` to improve the error message that is part of ``qtbot.TimeoutError``. - :param int timeout: + :type signals: list[Signal] + :param timeout: How many milliseconds to wait before resuming control flow. - :param bool raising: + :param raising: If :class:`qtbot.TimeoutError ` should be raised if a timeout occurred. This defaults to ``True`` unless ``qt_default_raising = false`` is set in the config. - :param list check_params_cbs: + :param check_params_cbs: optional list of callables that compare the provided signal parameters to some expected parameters. Each callable has to match the signature of the corresponding signal in ``signals`` (just like a slot function would) and return ``True`` if parameters match, ``False`` otherwise. Instead of a specific callable, ``None`` can be provided, to disable parameter checking for the corresponding signal. If the number of callbacks doesn't match the number of signals ``ValueError`` will be raised. - :param str order: + :param order: Determines the order in which to expect signals: - ``"none"``: no order is enforced @@ -511,7 +512,7 @@ def assertNotEmitted( Make sure the given ``signal`` doesn't get emitted. - :param int wait: + :param wait: How many milliseconds to wait to make sure the signal isn't emitted asynchronously. By default, this method returns immediately and only catches signals emitted inside the ``with``-block. @@ -625,9 +626,9 @@ def waitCallback( blocker.wait() - :param int timeout: + :param timeout: How many milliseconds to wait before resuming control flow. - :param bool raising: + :param raising: If :class:`qtbot.TimeoutError ` should be raised if a timeout occurred. This defaults to ``True`` unless ``qt_default_raising = false`` @@ -683,13 +684,14 @@ def screenshot( Raises :class:`qtbot.ScreenshotError ` if taking the screenshot or saving the file failed. - :param QWidget widget: + :param widget: The widget to take a screenshot of. - :param str suffix: + :param suffix: An optional suffix to add to the filename. - :param QRect region: + :param region: The region of the widget to screeshot. By default, the entire widget is contained. + :type region: QRect | None :returns: A ``pathlib.Path`` object with the taken screenshot. """ @@ -834,8 +836,8 @@ def __init__( 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". + :param method_name: name to the ``QtTest`` method to call to check if widget is active/exposed. + :param adjective_name: "activated" or "exposed". :param widget: :param timeout: """ diff --git a/src/pytestqt/wait_signal.py b/src/pytestqt/wait_signal.py index 11a0edd..5262fc7 100644 --- a/src/pytestqt/wait_signal.py +++ b/src/pytestqt/wait_signal.py @@ -5,9 +5,12 @@ import dataclasses from typing import Any +from typing_extensions import TypeAlias + from pytestqt.exceptions import TimeoutError from pytestqt.qt_compat import qt_api +SignalInstance: TypeAlias = Any CheckParamsCb = Callable[..., bool] @@ -77,7 +80,7 @@ def _get_timeout_error_message(self): """Subclasses have to implement this, returning an appropriate error message for a TimeoutError.""" raise NotImplementedError # pragma: no cover - def _extract_pyqt_signal_name(self, potential_pyqt_signal): + def _extract_pyqt_signal_name(self, potential_pyqt_signal: SignalInstance) -> str: signal_name = potential_pyqt_signal.signal # type: str if not isinstance(signal_name, str): raise TypeError( @@ -89,7 +92,9 @@ def _extract_pyqt_signal_name(self, potential_pyqt_signal): signal_name = signal_name.lstrip("2") return signal_name - def _extract_signal_from_signal_tuple(self, potential_signal_tuple): + def _extract_signal_from_signal_tuple( + self, potential_signal_tuple: SignalInstance | tuple[SignalInstance, str] + ) -> str: if isinstance(potential_signal_tuple, tuple): if len(potential_signal_tuple) != 2: raise ValueError( @@ -108,13 +113,15 @@ def _extract_signal_from_signal_tuple(self, potential_signal_tuple): return signal_name return "" - def determine_signal_name(self, potential_signal_tuple): + def determine_signal_name( + self, potential_signal_tuple: SignalInstance | tuple[SignalInstance, str] + ) -> str: """ Attempts to determine the signal's name. If the user provided the signal name as 2nd value of the tuple, this name has preference. Bad values cause a ``ValueError``. Otherwise it attempts to get the signal from the ``signal`` attribute of ``signal`` (which only exists for PyQt signals). - :returns: str name of the signal, an empty string if no signal name can be determined, or raises an error + :returns: name of the signal, an empty string if no signal name can be determined, or raises an error in case the user provided an invalid signal name manually """ signal_name = self._extract_signal_from_signal_tuple(potential_signal_tuple) @@ -320,12 +327,14 @@ def __init__(self, timeout=5000, raising=True, check_params_cbs=None, order="non self._signal_names = {} self.all_signals_and_args = [] # list of SignalAndArgs instances - def add_signals(self, signals): + def add_signals( + self, signals: list[SignalInstance | tuple[SignalInstance, str]] + ) -> None: """ Adds the given signal to the list of signals which :meth:`wait()` waits for. - :param list signals: list of QtCore.Signal`s or tuples (QtCore.Signal, str) + :param signals: list of QtCore.Signal`s or tuples (QtCore.Signal, str) """ self._determine_unique_signals(signals) self._create_signal_emitted_indices(signals) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index ca28f0b..8c863ea 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -24,12 +24,12 @@ def has_pyside6_exception_capture(): @pytest.mark.parametrize("raise_error", [False, True]) -def test_catch_exceptions_in_virtual_methods(pytester, raise_error): +def test_catch_exceptions_in_virtual_methods( + pytester: pytest.Pytester, raise_error: bool +) -> None: """ Catch exceptions that happen inside Qt's event loop and make the tests fail if any. - - :type pytester: pytest.Pytester """ pytester.makepyfile( """ @@ -114,12 +114,10 @@ def test_format_captured_exceptions_chained(): @pytest.mark.parametrize("no_capture_by_marker", [True, False]) @exception_capture_pyside6 -def test_no_capture(pytester, no_capture_by_marker): +def test_no_capture(pytester: pytest.Pytester, no_capture_by_marker: bool) -> None: """ Make sure options that disable exception capture are working (either marker or ini configuration value). - - :type pytester: TmpTestdir """ if no_capture_by_marker: marker_code = "@pytest.mark.qt_no_exception_capture" @@ -152,11 +150,9 @@ def test_widget(qtbot): res.stdout.fnmatch_lines(["*1 passed*"]) -def test_no_capture_preserves_custom_excepthook(pytester): +def test_no_capture_preserves_custom_excepthook(pytester: pytest.Pytester) -> None: """ Capturing must leave custom excepthooks alone when disabled. - - :type pytester: TmpTestdir """ pytester.makepyfile(""" import pytest @@ -179,11 +175,9 @@ def test_capture(qtbot): res.stdout.fnmatch_lines(["*2 passed*"]) -def test_exception_capture_on_call(pytester): +def test_exception_capture_on_call(pytester: pytest.Pytester) -> None: """ Exceptions should also be captured during test execution. - - :type pytester: TmpTestdir """ pytester.makepyfile(""" import pytest @@ -203,11 +197,9 @@ def test_widget(qtbot, qapp): res.stdout.fnmatch_lines(["*RuntimeError('event processed')*", "*1 failed*"]) -def test_exception_capture_on_widget_close(pytester): +def test_exception_capture_on_widget_close(pytester: pytest.Pytester) -> None: """ Exceptions should also be captured when widget is being closed. - - :type pytester: TmpTestdir """ pytester.makepyfile(""" import pytest @@ -228,12 +220,12 @@ def test_widget(qtbot, qapp): @pytest.mark.parametrize("mode", ["setup", "teardown"]) -def test_exception_capture_on_fixture_setup_and_teardown(pytester, mode): +def test_exception_capture_on_fixture_setup_and_teardown( + pytester: pytest.Pytester, mode: str +) -> None: """ Setup/teardown exception capturing as early/late as possible to catch all exceptions, even from other fixtures (#105). - - :type pytester: TmpTestdir """ if mode == "setup": setup_code = "send_event(w, qapp)"