How to execute PyQt5 application on a resful call

▼魔方 西西 提交于 2021-02-11 09:58:44

问题


Context:

I have a Flask application serving a resource POST /start. The logic to be executed involves a PyQt5 QWebEnginePage loading a URL and returning certain data about it.

Problem:

When the QApplication is executed (calling app.exec_()) I get the warning:

WARNING: QApplication was not created in the main() thread.

and then the error:

2019-07-17 13:06:19.461 Python[56513:5183122] *** Assertion failure in +[NSUndoManager _endTopLevelGroupings], /BuildRoot/Library/Caches/com.apple.xbs/Sources/Foundation/Foundation-1562/Foundation/Misc.subproj/NSUndoManager.m:361
2019-07-17 13:06:19.464 Python[56513:5183122] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '+[NSUndoManager(NSInternal) _endTopLevelGroupings] is only safe to invoke on the main thread.'
*** First throw call stack:
(
   0   CoreFoundation                      0x00007fff4e1abded __exceptionPreprocess + 256
   1   libobjc.A.dylib                     0x00007fff7a273720 objc_exception_throw + 48
   ...
   ...
   122 libsystem_pthread.dylib             0x00007fff7b53826f _pthread_start + 70
   123 libsystem_pthread.dylib             0x00007fff7b534415 thread_start + 13
)
libc++abi.dylib: terminating with uncaught exception of type NSException
Received signal 6
[0x00010a766de6]
[0x7fff7b52cb3d]
...
...
[0x000105a0de27]
[end of stack trace]

It seems like the QApplication always needs to run on the main thread, which is not the case since flask runs resources on background threads. A possible solution i have considered is to run the QApplication as a os subprocess but is not ideal.

Question:

Is it possible to keep it within the Flask app?

Example PyQt class:

import sys

from PyQt5.QtWebEngineWidgets import QWebEnginePage
from PyQt5.QtWebEngineWidgets import QWebEngineProfile
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QUrl


class PyQtWebClient(QWebEnginePage):
    def __init__(self, url):

        # Pointless variable for showcase purposes
        self.total_runtime = None

        self.app = QApplication(sys.argv)

        self.profile = QWebEngineProfile()

        # This is a sample to show the constructor I am actually using, my 'profile' is more complex than this
        super().__init__(self.profile, None)

        # Register callback to run when the page loads
        self.loadFinished.connect(self._on_load_finished)
        self.load(QUrl(url))
        self.app.exec_()

    def _on_load_finished(self):
        self.total_runtime = 10


if __name__ == '__main__':
    url = "https://www.example.com"
    page = PyQtWebClient(url)

Example Flask app.py

from flask import Flask
from flask_restful import Resource, Api
from lenomi import PyQtWebClient

app = Flask(__name__)
api = Api(app)


class TestPyqt5(Resource):
    def post(self):
        web = PyQtWebClient("http://www.example.com")
        # At this point PyQtWebClient should have finished loading the url, and the process is done
        print(web.total_runtime)


api.add_resource(TestPyqt5, "/pyqt")

if __name__ == '__main__':
    app.run(debug=True)

回答1:


Resource executes the post, get, etc methods in secondary threads to avoid that the thread where flask is executed does not block, and therefore the QApplication is running in that secondary thread that Qt prohibits generating that error.

In this case the solution is.

  • Create a class that handles requests through QWebEnginePage running on the main thread.

  • Make the flask run on a secondary thread so that it does not block the Qt eventloop.

  • Send the information through signals between the post method and the class that handles the requests.

Considering this I have implemented an example where you can make requests to pages via the API, obtaining the HTML of that page

lenomi.py

from functools import partial

from PyQt5 import QtCore, QtWebEngineWidgets


class Signaller(QtCore.QObject):
    emitted = QtCore.pyqtSignal(object)


class PyQtWebClient(QtCore.QObject):
    @QtCore.pyqtSlot(Signaller, str)
    def get(self, signaller, url):
        self.total_runtime = None
        profile = QtWebEngineWidgets.QWebEngineProfile(self)
        page = QtWebEngineWidgets.QWebEnginePage(profile, self)
        wrapper = partial(self._on_load_finished, signaller)
        page.loadFinished.connect(wrapper)
        page.load(QtCore.QUrl(url))

    @QtCore.pyqtSlot(Signaller, bool)
    def _on_load_finished(self, signaller, ok):
        page = self.sender()
        if not isinstance(page, QtWebEngineWidgets.QWebEnginePage) or not ok:
            signaller.emitted.emit(None)
            return

        self.total_runtime = 10
        html = PyQtWebClient.download_html(page)
        args = self.total_runtime, html
        signaller.emitted.emit(args)

        profile = page.profile()
        page.deleteLater()
        profile.deleteLater()

    @staticmethod
    def download_html(page):
        html = ""
        loop = QtCore.QEventLoop()

        def callback(r):
            nonlocal html
            html = r
            loop.quit()

        page.toHtml(callback)
        loop.exec_()
        return html

app.py

import sys
import threading
from functools import partial

from flask import Flask
from flask_restful import Resource, Api, reqparse

from PyQt5 import QtCore, QtWidgets

from lenomi import PyQtWebClient, Signaller


app = Flask(__name__)
api = Api(app)
parser = reqparse.RequestParser()


class TestPyqt5(Resource):
    def __init__(self, client):
        self.m_client = client

    def post(self):
        parser.add_argument("url", type=str)
        args = parser.parse_args()
        url = args["url"]
        if url:
            total_runtime, html, error = 0, "", "not error"

            def callback(loop, results=None):
                if results is None:
                    nonlocal error
                    error = "Not load"
                else:
                    nonlocal total_runtime, html
                    total_runtime, html = results
                loop.quit()

            signaller = Signaller()
            loop = QtCore.QEventLoop()
            signaller.emitted.connect(partial(callback, loop))
            wrapper = partial(self.m_client.get, signaller, url)
            QtCore.QTimer.singleShot(0, wrapper)
            loop.exec_()

            return {
                "html": html,
                "total_runtime": total_runtime,
                "error": error,
            }


qt_app = None


def main():

    global qt_app
    qt_app = QtWidgets.QApplication(sys.argv)

    client = PyQtWebClient()
    api.add_resource(
        TestPyqt5, "/pyqt", resource_class_kwargs={"client": client}
    )

    threading.Thread(
        target=app.run,
        kwargs=dict(debug=False, use_reloader=False),
        daemon=True,
    ).start()

    return qt_app.exec_()


if __name__ == "__main__":
    sys.exit(main())
curl http://localhost:5000/pyqt -d "url=https://www.example.com" -X POST

Output:

{"html": "<!DOCTYPE html><html><head>\n    <title>Example Domain</title>\n\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"Content-type\" content=\"text/html; charset=utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style type=\"text/css\">\n    body {\n        background-color: #f0f0f2;\n        margin: 0;\n        padding: 0;\n        font-family: \"Open Sans\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n        \n    }\n    div {\n        width: 600px;\n        margin: 5em auto;\n        padding: 50px;\n        background-color: #fff;\n        border-radius: 1em;\n    }\n    a:link, a:visited {\n        color: #38488f;\n        text-decoration: none;\n    }\n    @media (max-width: 700px) {\n        body {\n            background-color: #fff;\n        }\n        div {\n            width: auto;\n            margin: 0 auto;\n            border-radius: 0;\n            padding: 1em;\n        }\n    }\n    </style>    \n</head>\n\n<body>\n<div>\n    <h1>Example Domain</h1>\n    <p>This domain is established to be used for illustrative examples in documents. You may use this\n    domain in examples without prior coordination or asking for permission.</p>\n    <p><a href=\"http://www.iana.org/domains/example\">More information...</a></p>\n</div>\n\n\n</body></html>", "total_runtime": 10, "error": "not error"}


来源:https://stackoverflow.com/questions/57081813/how-to-execute-pyqt5-application-on-a-resful-call

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!