Skip to content

PyQt5 - QThread

Python3
  • Eines der Geheimnisse, was mich schon was länger beschäftigt ist, wie man parallele Prozesse mit Python verwaltet. Dazu findet man viele Beispiele und man rätselt und rätselt.....

    Mal gehen die Beispiele nicht, mal versteht man sie nicht. Dabei habe ich nur was gesucht, was mir möglichst einfach darstellt, wie man so was grundsätzlich aufbaut. Der Rest kommt dann mit der Zeit, wenn ich lange genug damit herumgespielt habe 😊

    Ok, warum das Ganze? Ich habe in meinem Restic UI Prozesse, die was länger dauern. Ich möchte dem Anwender das signalisieren, mittels einem waitingspinnerwidget. Ok, so weit hatte ich das auch hinbekommen, nur was mache ich mit einem Ergebnis in diesem Prozess? Das muss ja irgendwie zurück 🤔

    Ich habe dann diese Code hier gefunden und zum Laufen bekommen. YEAH

    #!/usr/bin/env python3
    
    from PyQt5.QtCore import QObject, QThread
    from PyQt5.QtCore import pyqtSlot, pyqtSignal
    from PyQt5.QtWidgets import QMainWindow
    
    import sys
    import time
    from PyQt5.QtWidgets import QApplication
    
    class MainWindow(QMainWindow):
        def __init__(self):
            super().__init__()
            print("Base init")
    
            self.thread = QThread()
            self.w = Worker()
            self.w.finished[str].connect(self.onFinished)
            self.w.moveToThread(self.thread)
            self.thread.started.connect(self.w.work)
            self.thread.start()
    
        @pyqtSlot(str)
        def onFinished(self, i):
            #print("Base caught finished, {}".format(i))
            print("Worker end", i)
    
    class Worker(QObject):
        finished = pyqtSignal(str)
    
        def __init__(self):
            print("Worker init")
            super().__init__()
    
        def work(self):
            print("Worker work")
            time.sleep(5)
            self.finished.emit('TEXT')
    
    if __name__ == "__main__":
    
        app = QApplication(sys.argv)
        w = MainWindow()
        w.show()
    
        sys.exit(app.exec_())
    

    Das ist jetzt nicht so verdammt schwer, ein wenig hat man ja schon gelernt. Es geht ein Fenster auf, der Hauptprozess startet den Worker als eigenen Prozess. Der macht nicht viel, als 5 Sekunden zu warten und dann wenn er beendet wird, sendet er ein Signal an den Hauptprozess def onFinished(self, i): welcher dann eine Meldung in die Konsole ausgibt. Nix besonderes, aber für mich ein Ansatz meine Aufgabe zu lösen.

    Quelle: https://stackoverflow.com/questions/29243692/pyqt5-how-to-make-qthread-return-data-to-main-thread

  • Das Thema muss sitzen, also versuche ich das etwas zu vertiefen. Gestern habe ich viel Stunden damit verbracht, mein Restic UI auf das waitingspinnerwidget umzubauen. Bis auf eine Funktion, die heute noch dran kommt, habe ich alles hinbekommen 🙂 Aber, wie immer habe ich danach Fragen. Ok, fangen wir mal an.

    @pyqtSlot(str)
    def onFinished(self, i):
        #print("Base caught finished, {}".format(i))
        print("Worker end", i)
    

    Wenn wir uns den Code anschauen, kommt die Frage auf, was macht @pyqtSlot(str) ? Ich probiere viel aus, also hatte ich das in meinem Code auskommentiert und mich gewundert, warum das immer noch funktioniert!? Ok, also Dokumentation lesen.

    Connecting a signal to a decorated Python method also has the advantage of reducing the amount of memory used and is slightly faster.

    Das klärt meine Frage aber immer noch nicht komplett. Auf stackoverflow.com, das folgende gefunden.

    For your case consider it as a good practice - denoting a method as a slot for some signal.

    Also muss ich mir jetzt merken, das es weniger Speicher verbraucht und etwas schneller ist. Gut, und es ordentlicher Programmierstil(?), wenn ich die Funktionen als abhängig von Signalen kennzeichne!? Ich denke, so passt es ganz gut.

    Sehr gute Anleitungen zum Thema findet man auch immer wieder auf realpython.com. Hier z.B. zum Thema QThread. Anhand des Beispieles habe ich meinen ersten QThread heute folgendermaßen dokumentiert.

        "First Thread with documentation"
        # Worker for restic_stats
        # Create a QThread object
        self.thread = QThread()
        # Create a worker object
        self.worker1 = Worker()
        # Move worker to the thread
        self.worker1.moveToThread(self.thread)
        # Connect signals and slots
        self.thread.started.connect(self.worker1.run)
        self.worker1.stats_finished[str].connect(self.restic_stats_finished)
        self.worker1.stats_error[str].connect(self.restic_stats_error)
        "First thread end"
    

    Dann stand ich sehr schnell vor dem Problem, was mache ich bei der nächsten Funktion, die einen QThread braucht?

        # Worker for restic_backup
        self.thread2 = QThread()
        self.worker2 = Worker2()
        self.worker2.moveToThread(self.thread2)
        self.thread2.started.connect(self.worker2.run)
        self.worker2.backup_finished[str].connect(self.restic_backup_finished)
        self.worker2.backup_error[str].connect(self.restic_backup_error)
    

    Ok, das war jetzt nicht so wirklich schwierig.

  • Und hier mal ein komplettes Beispiel.

    class Worker

    Wir legen den Worker an, das ist der Prozess der die Arbeit macht und etwas länger braucht.

    class Worker(QObject):
    
        """ Worker Class for Rest function stats"""
        stats_finished = pyqtSignal(str)
        stats_error = pyqtSignal(str)
    
        def __init__(self):
            super().__init__()
    
        def run(self):
            # Restic function
    
        try:
            # long running task
        except Exception:
            # Process don't successful, send signal
            self.stats_error.emit(result.stderr)
        else:
            # Process successful, send signal
            self.stats_finished.emit(result.stdout)
        finally:
            pass
    

    in class MainWindow

    class MainWindow(QMainWindow):
        def __init__(self):
        super().__init__()
    
        "First Thread with documentation"
        # Worker for restic_stats
        # Create a QThread object
        self.thread = QThread()
        # Create a worker object
        self.worker1 = Worker()
        # Move worker to the thread
        self.worker1.moveToThread(self.thread)
        # Connect signals and slots
        self.thread.started.connect(self.worker1.run)
        self.worker1.stats_finished[str].connect(self.restic_stats_finished)
        self.worker1.stats_error[str].connect(self.restic_stats_error)
        "First thread end"
    
        ###############################################
        # Process for restic_stats is finished
        ###############################################
    
        @pyqtSlot(str)
        def restic_stats_finished(self, i):
            # Signal from worker thread without an error
    
        Spinner.stop(self)
    
        self.thread.quit()
    
        ###############################################
        # Process for restic_stats when get an error
        ###############################################
    
        @pyqtSlot(str)
        def restic_stats_error(self, i):
            # Signal from worker thread with an error!
    
            Spinner.stop(self)
    

    Funktion restic_stats

    def restic_stats(self):
        # we start the worker thread
        self.thread.start()
    
        # we start waitingspinnerwidget
        Spinner.start(self)
    

    Ich wollte gerade schreiben, das folgendes sehr wichtig ist

    self.thread.quit()
    

    da fällt mir ein Fehler auf. Kurz ausprobiert und bingo, wenn der Prozess einen Error triggert, muss natürlich auch der Prozess beendet werden. Wenn man das nicht macht, macht das Programm nicht das was es soll. Der Grund ist, das der Prozess einfach immer weiter läuft. Er MUSS beendet werden. Ich gehe dann mal in meinem Programm alles ändern 🙂

    Ok, jetzt geht die Funktion auch zweimal hintereinander und gibt auch ordentlich den Fehler aus.

    Fazit

    Ich habe wieder sehr viel gelernt und hoffe das ich es auch richtig verstanden habe 😉 Hoffe das es dem ein oder anderen Anfänger hilft. Und falls hier ein Profi mitliest und hier Blödsinn steht bitte ich um einen Kommentar, damit ich das ändern kann. Es steht schon genug Blödsinn im Netz 🙂

  • PyPi - Pakete ein Sicherheitsproblem?

    Python3
    1
    0 Stimmen
    1 Beiträge
    72 Aufrufe
    Niemand hat geantwortet
  • Restic UI - Changelog

    Angeheftet Restic UI
    3
    0 Stimmen
    3 Beiträge
    262 Aufrufe
    FrankMF

    v1.5.0 - Release for restic v0.14.0 with compression and migration tool

  • Restic UI - User documentation

    Restic UI
    1
    0 Stimmen
    1 Beiträge
    213 Aufrufe
    Niemand hat geantwortet
  • Python3 - Umzug nach Fedora 34

    Python3
    1
    0 Stimmen
    1 Beiträge
    145 Aufrufe
    Niemand hat geantwortet
  • 0 Stimmen
    3 Beiträge
    330 Aufrufe
    FrankMF

    Kein globalen Variablen mehr im Projekt 🙂

    Das Widget ließ sich relativ einfach erledigen.

    Vorher #----QPlainTextEdit ----# layout = QVBoxLayout() widget = QTextEdit() widget.setReadOnly(True) widget.setLayout(layout) setCentralWidget(widget) Nachher #----QPlainTextEdit ----# layout = QVBoxLayout() self.widget = QTextEdit() self.widget.setReadOnly(True) self.widget.setLayout(layout) self.setCentralWidget(self.widget)

    Aufruf dann nicht mehr mit

    # UI widget.setHtml("")

    sondern mit

    # UI mainWin.widget.setHtml("")

    Problem erledeigt und hoffentlich auch verstanden 😉

  • Python3 - subprocess.run

    Python3
    2
    0 Stimmen
    2 Beiträge
    177 Aufrufe
    FrankMF

    Ich möchte das Thema noch mal ausgraben. Bin beim Recherchieren über diese Links gestolpert, die mein Interesse geweckt haben.

    Link Preview Image Python Pipes to Avoid Shells — OpenStack Security Advisories 0.0.1.dev268 documentation

    favicon

    (security.openstack.org)

    B603: subprocess_without_shell_equals_true — Bandit documentation

    favicon

    (bandit.readthedocs.io)

    Da ich aktuell nur eine Desktop Anwendung entwickle, ist das Thema Sicherheit nicht ganz so wichtig, weil wer hackt sich schon selber!? Aber, da man ja nie weiß, wie so ein Tool evt. mal benutzt wird, sollte man von Anfang an auf ein paar Dinge achten.

    Ok, schauen wir uns das mal genauer an.

    Link 1 - shell=True

    Ich kopiere mal das Beispiel aus dem Link.

    def count_lines(website): return subprocess.check_output('curl %s | wc -l' % website, shell=True)

    Ok, das Problem ist

    shell=True

    Ein Beispiel aus meinem Projekt

    result = subprocess.run(['restic', '-r', backup_data[row].repository, 'stats'], input=pass_word.pw[0], capture_output=True, text=True)

    Gesetzt wird der in meinem Beispiel nicht. Besuchen wir mal die Webseite vom subprocess.run

    https://docs.python.org/3/library/subprocess.html?highlight=subprocess run#subprocess.run

    Wenn ich das jetzt richtig verstehe,

    subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None, capture_output=False, shell=False, cwd=None, timeout=None, check=False, encoding=None, errors=None, text=None, env=None, universal_newlines=None, **other_popen_kwargs)

    dann ist standardmäßig

    shell=False

    gesetzt. Damit ist das in meinem Projekt kein Problem.

    Link 2 - B603: subprocess_without_shell_equals_true

    Ein Tool auf gitlab.com wirft Security Warnings aus, dabei war diese. Schauen wir mal, was uns das sagen möchte.

    Python possesses many mechanisms to invoke an external executable. However, doing so may present a security issue if appropriate care is not taken to sanitize any user provided or variable input.

    Ok, es geht also um die Prüfung von Eingaben bzw. Variablen. Der Merksatz "Keine Benutzereingabe wird ungeprüft übernommen!" ist doch mit das Wichtigste, wenn man irgendwas programmiert. Nochmal mein Beispiel von oben.

    result = subprocess.run(['restic', '-r', backup_data[row].repository, 'stats'], input=pass_word.pw[0], capture_output=True, text=True)

    Ich übergebe dem Prozess einige Eingaben / Variablen.

    backup_data[row].repository pass_word.pw[0]

    Lesen wir wieder ein wenig in der Dokumentation von subprocess.run

    args is required for all calls and should be a string, or a sequence of program arguments. Providing a sequence of arguments is generally preferred, as it allows the module to take care of any required escaping and quoting of arguments (e.g. to permit spaces in file names). If passing a single string, either shell must be True (see below) or else the string must simply name the program to be executed without specifying any arguments.

    Das Wichtigste in Kürze Providing a sequence of arguments is generally preferred

    Der Aufruf von subprocess.run erwartet als erste Übergabe args

    subprocess.run(args, *, ....

    Mein Beispiel

    result = subprocess.run(['restic', '-r', backup_data[row].repository, 'stats'], ....

    Das zwischen den eckigen Klammern ist args. Laut der Anleitung ist es empfohlen, das als ein Argument zu übergeben, also so. Somit ist dafür gesorgt, das das Modul die Argumente selbst ein wenig "überwacht".

    as it allows the module to take care of any required escaping and quoting of arguments (e.g. to permit spaces in file names).

    Somit hätten man schon mal was für sie "Sicherheit" getan.

    args = ['restic', '-r', backup_data[row].repository, 'stats'] result = subprocess.run(args, ....

    Man sollte trotzdem auf diese beiden Variablen

    * backup_data[row].repository * pass_word.pw[0]

    ein Auge behalten. Ich denke, das habe ich in meinem Projekt eingehalten, indem ich Pfadangabe mit den Systemwerkzeugen auswählbar mache, keine Texteingaben! Passwörter und andere Bezeichnung werden mit regex auf korrekte Eingaben geprüft usw.

    Fazit

    Wieder viel gelernt und ich denke, es passt so weit alles. Bei der ganzen Spielerei, dann noch entdeckt, das ich im Code dieses Argument drin hatte.

    check=False

    Das ist aber Standard, somit kann das weg. Habe das im kompletten Projekt dann entfernt und mir fällt gerade auf, da der Code immer gleich ist, muss das jetzt eigentlich alles in eine Funktion 😉 Mal auf die ToDo-Liste drauf schreiben.

  • Python3 - String-Operatoren

    Python3
    1
    0 Stimmen
    1 Beiträge
    134 Aufrufe
    Niemand hat geantwortet
  • Python3 - Eingabeformular

    Python3
    3
    0 Stimmen
    3 Beiträge
    191 Aufrufe
    FrankMF

    Meine endgültige Lösung, zu mindestens im Moment 😉 , ist ein zweites Window. Das gefällt mir am Besten, komme ich zu mindestens im Moment einfach besser mit klar. Aber, eines der größten Probleme war für mich, wie aktualisiere ich die Liste im Hauptfenster!? Da habe ich doch etliche Stunden dran rum gefummelt....

    Hier nur mal das Layout

    ##################### # Layout ##################### self.formGroupBox = QGroupBox("Form layout") layout = QFormLayout() layout.addRow(self.label_1) layout.addRow(QLabel("Backup Name:"), self.input1) layout.addRow(QLabel("Repository:"), self.button3) layout.addRow(QLabel("Source:"), self.button4) layout.addRow(QLabel("Password:"), self.input4) layout.addRow(self.label_6) layout.addRow(self.button2) self.formGroupBox.setLayout(layout) self.setLayout(layout)

    Das mit dem SecondWindow ist aber was aufwändiger. Da mach ich dann mal einen eigenen Beitrag zu. Aktuell sieht das so aus.

    6c68c956-9127-438f-bc54-4dcef8e18f00-image.png