From 7a0207e5ceea4cee77446d831686834ada5b4925 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 26 Jul 2021 15:30:51 -0400 Subject: [PATCH 1/7] Warn when the Qt application quits 'early' --- docs/source/index.rst | 1 + docs/source/lifetimes.rst | 21 +++++++++++++++++++++ qtrio/__init__.py | 2 ++ qtrio/_core.py | 8 ++++++++ qtrio/_exceptions.py | 8 ++++++++ qtrio/_tests/test_core.py | 32 +++++++++++++++++++++++++++++++- 6 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 docs/source/lifetimes.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index 095f076f..4ad98bc3 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -11,6 +11,7 @@ getting_started.rst core.rst + lifetimes.rst testing.rst dialogs.rst exceptions.rst diff --git a/docs/source/lifetimes.rst b/docs/source/lifetimes.rst new file mode 100644 index 00000000..6aac9ce4 --- /dev/null +++ b/docs/source/lifetimes.rst @@ -0,0 +1,21 @@ +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. QTrio will automatically launch and terminate the +application as needed. If you change this process, such as by calling +:meth:`QtCore.QCoreApplication.quit `, you can cause +parts of the system to not work correctly. Trio's guest mode can't call any of your +code after the application has quit. This includes cleanup code such as in ``finally`` +blocks. 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 + +QTrio will make an effort to warn you as well including directing you to this page. + +.. code:: + + .../qtrio/_core.py:751: ApplicationQuitWarning: The Qt application quit early. See https://qtrio.readthedocs.io/en/stable/lifetimes.html diff --git a/qtrio/__init__.py b/qtrio/__init__.py index e8f8c5e3..526f6edd 100644 --- a/qtrio/__init__.py +++ b/qtrio/__init__.py @@ -15,6 +15,8 @@ InvalidInputError, InternalError, DialogNotActiveError, + QTrioWarning, + ApplicationQuitWarning, ) from ._core import ( diff --git a/qtrio/_core.py b/qtrio/_core.py index 3ec962ae..ce57e44c 100644 --- a/qtrio/_core.py +++ b/qtrio/_core.py @@ -8,6 +8,7 @@ import sys import typing import typing_extensions +import warnings import async_generator import attr @@ -648,6 +649,9 @@ def run( instruments=self.instruments, ) + if self.quit_application: + self.application.aboutToQuit.connect(self._about_to_quit) + if execute_application: return_code = self.application.exec_() @@ -728,6 +732,10 @@ def trio_done(self, run_outcome: outcome.Outcome) -> None: self.done_callback(self.outcomes) if self.quit_application: + self.application.aboutToQuit.disconnect(self._about_to_quit) self.application.quit() self._done = True + + def _about_to_quit(self): + warnings.warn(message="The Qt application quit early. See https://qtrio.readthedocs.io/en/stable/lifetimes.html", category=qtrio.ApplicationQuitWarning) diff --git a/qtrio/_exceptions.py b/qtrio/_exceptions.py index e3d5cf82..f7d9ac6a 100644 --- a/qtrio/_exceptions.py +++ b/qtrio/_exceptions.py @@ -86,3 +86,11 @@ class DialogNotActiveError(QTrioException): """Raised when attempting to interact with a dialog while it is not actually available. """ + + +class QTrioWarning(UserWarning): + pass + + +class ApplicationQuitWarning(QTrioWarning): + pass diff --git a/qtrio/_tests/test_core.py b/qtrio/_tests/test_core.py index 142277bf..6f2dc09d 100644 --- a/qtrio/_tests/test_core.py +++ b/qtrio/_tests/test_core.py @@ -131,7 +131,7 @@ def test_run_passes_args(testdir): def test(): result = [] - + async def main(arg1, arg2): result.append(arg1) result.append(arg2) @@ -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: .*"]) From eba93c1d67a20a29caf315fce3429b9e51bcffd8 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 26 Jul 2021 16:18:53 -0400 Subject: [PATCH 2/7] black --- qtrio/_core.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qtrio/_core.py b/qtrio/_core.py index ce57e44c..ccd3fe59 100644 --- a/qtrio/_core.py +++ b/qtrio/_core.py @@ -738,4 +738,7 @@ def trio_done(self, run_outcome: outcome.Outcome) -> None: self._done = True def _about_to_quit(self): - warnings.warn(message="The Qt application quit early. See https://qtrio.readthedocs.io/en/stable/lifetimes.html", category=qtrio.ApplicationQuitWarning) + warnings.warn( + message="The Qt application quit early. See https://qtrio.readthedocs.io/en/stable/lifetimes.html", + category=qtrio.ApplicationQuitWarning, + ) From 81dc58fb8cab20653c6ede82fb4c11e5f09b4f63 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 26 Jul 2021 16:23:01 -0400 Subject: [PATCH 3/7] mypy --- qtrio/_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qtrio/_core.py b/qtrio/_core.py index ccd3fe59..456a29de 100644 --- a/qtrio/_core.py +++ b/qtrio/_core.py @@ -737,7 +737,7 @@ def trio_done(self, run_outcome: outcome.Outcome) -> None: self._done = True - def _about_to_quit(self): + def _about_to_quit(self) -> None: warnings.warn( message="The Qt application quit early. See https://qtrio.readthedocs.io/en/stable/lifetimes.html", category=qtrio.ApplicationQuitWarning, From fd27e24261bc1d2b3b19d0af628035055223c4e0 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 26 Jul 2021 16:41:10 -0400 Subject: [PATCH 4/7] _about_to_quit(self) -> _early_quit_warning() --- qtrio/_core.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/qtrio/_core.py b/qtrio/_core.py index 456a29de..05a8e49d 100644 --- a/qtrio/_core.py +++ b/qtrio/_core.py @@ -569,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.""" @@ -650,7 +657,7 @@ def run( ) if self.quit_application: - self.application.aboutToQuit.connect(self._about_to_quit) + self.application.aboutToQuit.connect(_early_quit_warning) if execute_application: return_code = self.application.exec_() @@ -732,13 +739,7 @@ def trio_done(self, run_outcome: outcome.Outcome) -> None: self.done_callback(self.outcomes) if self.quit_application: - self.application.aboutToQuit.disconnect(self._about_to_quit) + self.application.aboutToQuit.disconnect(_early_quit_warning) self.application.quit() self._done = True - - def _about_to_quit(self) -> None: - warnings.warn( - message="The Qt application quit early. See https://qtrio.readthedocs.io/en/stable/lifetimes.html", - category=qtrio.ApplicationQuitWarning, - ) From 3b966874f3245274b2819bf4143b4929cec0f26b Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 26 Jul 2021 17:04:32 -0400 Subject: [PATCH 5/7] documentation --- docs/source/exceptions.rst | 12 +++++++++++- docs/source/lifetimes.rst | 27 +++++++++++++++++---------- qtrio/_exceptions.py | 6 ++++-- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/docs/source/exceptions.rst b/docs/source/exceptions.rst index 49b410f4..4c6c4ea2 100644 --- a/docs/source/exceptions.rst +++ b/docs/source/exceptions.rst @@ -1,5 +1,8 @@ +Exceptions and Warnings +======================= + Exceptions -========== +---------- .. autoclass:: qtrio.QTrioException .. autoclass:: qtrio.NoOutcomesError @@ -11,3 +14,10 @@ Exceptions .. autoclass:: qtrio.InternalError .. autoclass:: qtrio.UserCancelledError .. autoclass:: qtrio.InvalidInputError + + +Warnings +-------- + +.. autoclass:: qtrio.QTrioWarning +.. autoclass:: qtrio.ApplicationQuitWarning diff --git a/docs/source/lifetimes.rst b/docs/source/lifetimes.rst index 6aac9ce4..19659450 100644 --- a/docs/source/lifetimes.rst +++ b/docs/source/lifetimes.rst @@ -1,21 +1,28 @@ Lifetimes ========= -In default usage QTrio will automatically manage the lifetime of the Qt application. +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. QTrio will automatically launch and terminate the -application as needed. If you change this process, such as by calling -:meth:`QtCore.QCoreApplication.quit `, you can cause -parts of the system to not work correctly. Trio's guest mode can't call any of your -code after the application has quit. This includes cleanup code such as in ``finally`` -blocks. In some cases Trio will emit a warning. +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:: - .../trio/_core/_run.py:2221: RuntimeWarning: Trio guest run got abandoned without properly finishing... weird stuff might happen + .../qtrio/_core.py:751: ApplicationQuitWarning: The Qt application quit early. See https://qtrio.readthedocs.io/en/stable/lifetimes.html -QTrio will make an effort to warn you as well including directing you to this page. +In some cases Trio will emit a warning. .. code:: - .../qtrio/_core.py:751: ApplicationQuitWarning: The Qt application quit early. See https://qtrio.readthedocs.io/en/stable/lifetimes.html + .../trio/_core/_run.py:2221: RuntimeWarning: Trio guest run got abandoned without properly finishing... weird stuff might happen diff --git a/qtrio/_exceptions.py b/qtrio/_exceptions.py index f7d9ac6a..86cb6056 100644 --- a/qtrio/_exceptions.py +++ b/qtrio/_exceptions.py @@ -89,8 +89,10 @@ class DialogNotActiveError(QTrioException): class QTrioWarning(UserWarning): - pass + """Base warning for all QTrio warnings.""" class ApplicationQuitWarning(QTrioWarning): - pass + """Emitted when the Qt application quits but QTrio is expecting to manage the + application lifetime. + """ From ed69f6979a2b96bbb3353bb4313ee8e038033fa9 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 26 Jul 2021 17:04:46 -0400 Subject: [PATCH 6/7] add 267.feature.rst --- newsfragments/267.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/267.feature.rst diff --git a/newsfragments/267.feature.rst b/newsfragments/267.feature.rst new file mode 100644 index 00000000..73d19f2c --- /dev/null +++ b/newsfragments/267.feature.rst @@ -0,0 +1 @@ +Emit :class:`qtrio.ApplicationQuitWarning` when the Qt application quits but QTrio is expecting to manage the application lifetime. From c1a3a97f25df5592f9d482f210e71f309f027410 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 26 Jul 2021 17:10:36 -0400 Subject: [PATCH 7/7] link from changelog and warning to the lifetimes page --- docs/source/lifetimes.rst | 2 ++ newsfragments/267.feature.rst | 2 +- qtrio/_exceptions.py | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/source/lifetimes.rst b/docs/source/lifetimes.rst index 19659450..3544d332 100644 --- a/docs/source/lifetimes.rst +++ b/docs/source/lifetimes.rst @@ -1,3 +1,5 @@ +.. _lifetime: + Lifetimes ========= diff --git a/newsfragments/267.feature.rst b/newsfragments/267.feature.rst index 73d19f2c..0990895d 100644 --- a/newsfragments/267.feature.rst +++ b/newsfragments/267.feature.rst @@ -1 +1 @@ -Emit :class:`qtrio.ApplicationQuitWarning` when the Qt application quits but QTrio is expecting to manage the application lifetime. +Emit :class:`qtrio.ApplicationQuitWarning` when the Qt application quits but QTrio is expecting to manage :ref:`the application lifetime `. diff --git a/qtrio/_exceptions.py b/qtrio/_exceptions.py index 86cb6056..4f423861 100644 --- a/qtrio/_exceptions.py +++ b/qtrio/_exceptions.py @@ -94,5 +94,6 @@ class QTrioWarning(UserWarning): class ApplicationQuitWarning(QTrioWarning): """Emitted when the Qt application quits but QTrio is expecting to manage the - application lifetime. + application lifetime. See the documentation on + :ref:`the application lifetime ` for more information. """