CabbageDevelopment / qasync

Python library for using asyncio in Qt-based applications.
BSD 2-Clause "Simplified" License
334 stars 45 forks source link

`asyncio.CancelledError` not caught in `asyncSlot` error handler #126

Open QQ80 opened 2 months ago

QQ80 commented 2 months ago

For coroutines decorated with asyncSlot, if the enclosing task is cancelled, the asyncio.CancelledError is left uncaught. This is a problem if another signal triggers the cancelling of that enclosing task, while it is awaiting for something.

I believe the error handler at line 778 of qasync/__init__.py should handle asyncio.CancelledError with something like this:

    def _error_handler(task):
        try:
            task.result()
        except Exception:
            sys.excepthook(*sys.exc_info())
        except asyncio.CancelledError:
            pass

Note that since Python 3.8, asyncio.CancelledError is a derived class of BaseException but not Exception.

QQ80 commented 2 months ago

Here's a minimal example:

import asyncio
import qasync
from PyQt6.QtWidgets import QApplication, QPushButton

app: QApplication

@qasync.asyncSlot(bool)
async def button_clicked(_checked):
    match app.button.text():
        case "Wait":
            app.button.setText("Cancel wait")

            app.waiting_task = asyncio.current_task()
            await asyncio.sleep(5)

            app.button.setText("Wait")

        case "Cancel wait":
            app.waiting_task.cancel()

            app.button.setText("Wait")

async def main():
    global app
    app = QApplication.instance()
    app_quit_event = asyncio.Event()
    app.aboutToQuit.connect(app_quit_event.set)

    app.button = QPushButton("Wait")
    app.button.clicked.connect(button_clicked)
    app.button.show()

    await app_quit_event.wait()

qasync.run(main())

The exception is uncaught if "Wait" is pressed, and, while "Cancel wait" is shown, the button is pressed again.