Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down
9 changes: 3 additions & 6 deletions src/pytestqt/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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[:]


Expand Down
50 changes: 26 additions & 24 deletions src/pytestqt/qtbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@
CallbackBlocker,
CallbackCalledTwiceError,
CheckParamsCb,
SignalInstance,
)

from pytest import FixtureRequest

# 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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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 <pytestqt.exceptions.TimeoutError>`.
:param int timeout:
:param timeout:
How many milliseconds to wait before resuming control flow.
:param bool raising:
:param raising:
If :class:`qtbot.TimeoutError <pytestqt.exceptions.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.
Expand Down Expand Up @@ -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 <pytestqt.exceptions.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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 <pytestqt.exceptions.TimeoutError>`
should be raised if a timeout occurred.
This defaults to ``True`` unless ``qt_default_raising = false``
Expand Down Expand Up @@ -683,13 +684,14 @@ def screenshot(
Raises :class:`qtbot.ScreenshotError <pytestqt.exceptions.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.
"""
Expand Down Expand Up @@ -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:
"""
Expand Down
21 changes: 15 additions & 6 deletions src/pytestqt/wait_signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]


Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
28 changes: 10 additions & 18 deletions tests/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
"""
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)"
Expand Down
Loading