Skip to content

Commit

Permalink
Merge pull request #267 from altendky/warning_for_early_quit
Browse files Browse the repository at this point in the history
  • Loading branch information
altendky authored Jul 28, 2021
2 parents 2cc53c0 + c1a3a97 commit 43c7ff2
Show file tree
Hide file tree
Showing 8 changed files with 99 additions and 2 deletions.
12 changes: 11 additions & 1 deletion docs/source/exceptions.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
Exceptions and Warnings
=======================

Exceptions
==========
----------

.. autoclass:: qtrio.QTrioException
.. autoclass:: qtrio.NoOutcomesError
Expand All @@ -11,3 +14,10 @@ Exceptions
.. autoclass:: qtrio.InternalError
.. autoclass:: qtrio.UserCancelledError
.. autoclass:: qtrio.InvalidInputError


Warnings
--------

.. autoclass:: qtrio.QTrioWarning
.. autoclass:: qtrio.ApplicationQuitWarning
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

getting_started.rst
core.rst
lifetimes.rst
testing.rst
dialogs.rst
exceptions.rst
Expand Down
30 changes: 30 additions & 0 deletions docs/source/lifetimes.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.. _lifetime:

Lifetimes
=========

In default usage, QTrio will automatically manage the lifetime of the Qt application.
For the Trio guest mode to function, including during cleanup, the Qt application must
be active and processing events. If a program changes this process it can cause parts
of the system to not work correctly. Trio's guest mode can't call any of your
code after the Qt application has quit. This includes cleanup code such as in
``finally`` blocks.

The most direct way to cause this is by calling :meth:`QtCore.QCoreApplication.quit`.
Enabling :meth:`QtGui.QGuiApplication.setQuitOnLastWindowClosed` and closing all
windows will cause early event loop termination as well. If manual termination of the
application is truly needed this can be enabled by setting
:attr:`qtrio.Runner.quit_application` to :data:`False`.

QTrio makes an effort to emit a :class:`qtrio.ApplicationQuitWarning`. The message
includes a link to this page as a reminder.

.. code::
.../qtrio/_core.py:751: ApplicationQuitWarning: The Qt application quit early. See https://qtrio.readthedocs.io/en/stable/lifetimes.html
In some cases Trio will emit a warning.

.. code::
.../trio/_core/_run.py:2221: RuntimeWarning: Trio guest run got abandoned without properly finishing... weird stuff might happen
1 change: 1 addition & 0 deletions newsfragments/267.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Emit :class:`qtrio.ApplicationQuitWarning` when the Qt application quits but QTrio is expecting to manage :ref:`the application lifetime <lifetime>`.
2 changes: 2 additions & 0 deletions qtrio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
InvalidInputError,
InternalError,
DialogNotActiveError,
QTrioWarning,
ApplicationQuitWarning,
)

from ._core import (
Expand Down
12 changes: 12 additions & 0 deletions qtrio/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import sys
import typing
import typing_extensions
import warnings

import async_generator
import attr
Expand Down Expand Up @@ -568,6 +569,13 @@ def create_reenter() -> "qtrio.qt.Reenter":
return qtrio.qt.Reenter()


def _early_quit_warning() -> None:
warnings.warn(
message="The Qt application quit early. See https://qtrio.readthedocs.io/en/stable/lifetimes.html",
category=qtrio.ApplicationQuitWarning,
)


@attr.s(auto_attribs=True, slots=True)
class Runner:
"""This class helps run Trio in guest mode on a Qt host application."""
Expand Down Expand Up @@ -648,6 +656,9 @@ def run(
instruments=self.instruments,
)

if self.quit_application:
self.application.aboutToQuit.connect(_early_quit_warning)

if execute_application:
return_code = self.application.exec_()

Expand Down Expand Up @@ -728,6 +739,7 @@ def trio_done(self, run_outcome: outcome.Outcome) -> None:
self.done_callback(self.outcomes)

if self.quit_application:
self.application.aboutToQuit.disconnect(_early_quit_warning)
self.application.quit()

self._done = True
11 changes: 11 additions & 0 deletions qtrio/_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,14 @@ class DialogNotActiveError(QTrioException):
"""Raised when attempting to interact with a dialog while it is not actually
available.
"""


class QTrioWarning(UserWarning):
"""Base warning for all QTrio warnings."""


class ApplicationQuitWarning(QTrioWarning):
"""Emitted when the Qt application quits but QTrio is expecting to manage the
application lifetime. See the documentation on
:ref:`the application lifetime <lifetime>` for more information.
"""
32 changes: 31 additions & 1 deletion qtrio/_tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ def test_run_passes_args(testdir):
def test():
result = []
async def main(arg1, arg2):
result.append(arg1)
result.append(arg2)
Expand Down Expand Up @@ -1220,3 +1220,33 @@ def about_to_quit():

result = testdir.runpytest_subprocess(timeout=timeout)
result.assert_outcomes(passed=1)


def test_warning_on_early_application_quit(testdir):
"""Emit a warning if the runner is supposed to handle quitting the application
but the application is terminated early.
"""

test_file = r"""
import qtrio
from qts import QtWidgets
import trio
async def quit():
QtWidgets.QApplication.quit()
async def main():
async with trio.open_nursery() as nursery:
nursery.start_soon(quit)
await trio.sleep(9999)
qtrio.run(main)
"""

test_path = testdir.makepyfile(test_file)

result = testdir.runpython(script=test_path)
result.stderr.re_match_lines(lines2=[r".* ApplicationQuitWarning: .*"])

0 comments on commit 43c7ff2

Please sign in to comment.