Search code examples
pythonpyqt5python-multithreading

Qthreading In Pyqt5


I am Working on an app where you enter an amazon product URL and phone number and it texts you when that item is in stock. I am working with the URL and I am going to have it so that when you press the start button it goes to the URL you entered and checks if it is out of stock but when I press the start button the program crashes and says not responding. here is my code.

from PyQt5 import QtCore, QtGui, QtWidgets
import requests
from bs4 import BeautifulSoup
import time
import threading
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import *

class Ui_AmazonStockCheck(object):
        def setupUi(self, AmazonStockCheck):
            AmazonStockCheck.setObjectName("AmazonStockCheck")
            AmazonStockCheck.resize(804, 617)
            AmazonStockCheck.setWindowTitle('Stocker')
            self.centralwidget = QtWidgets.QWidget(AmazonStockCheck)
            self.centralwidget.setObjectName("centralwidget")
            self.urlBox = QtWidgets.QLineEdit(self.centralwidget)
            self.urlBox.setGeometry(QtCore.QRect(50, 90, 161, 51))
            self.urlBox.setObjectName("urlBox")
            font = QtGui.QFont()
            font.setFamily("Bebas Neue")
            font.setPointSize(20)
            self.phonenumberBox = QtWidgets.QLineEdit(self.centralwidget)
            self.phonenumberBox.setGeometry(QtCore.QRect(590, 90, 161, 51))
            self.phonenumberBox.setObjectName("phonenumberBox")
            self.producturl = QtWidgets.QLabel(self.centralwidget)
            self.producturl.setGeometry(QtCore.QRect(60, 50, 111, 31))
            font = QtGui.QFont()
            font.setFamily("Bebas Neue")
            font.setPointSize(20)
            self.producturl.setFont(font)
            self.producturl.setObjectName("producturl")
            self.phonenumber = QtWidgets.QLabel(self.centralwidget)
            self.phonenumber.setGeometry(QtCore.QRect(600, 60, 131, 31))
            font = QtGui.QFont()
            font.setFamily("Bebas Neue")
            font.setPointSize(20)
            self.phonenumber.setFont(font)
            self.phonenumber.setObjectName("phonenumber")
            self.startbutton = QtWidgets.QPushButton(self.centralwidget)
            self.startbutton.setGeometry(QtCore.QRect(320, 160, 141, 41))
            self.startbutton.clicked.connect(lambda: self.onclick(self.urlBox.text()))
            font = QtGui.QFont()
            font.setFamily("Bebas Neue")
            font.setPointSize(16)
            self.startbutton.setFont(font)
            self.startbutton.setObjectName("startbutton")
            self.abouttext = QtWidgets.QLabel(self.centralwidget)
            self.abouttext.setGeometry(QtCore.QRect(120, 240, 601, 231))
            font = QtGui.QFont()
            font.setFamily("Bebas Neue")
            font.setPointSize(28)
            self.abouttext.setFont(font)
            self.abouttext.setObjectName("abouttext")
            AmazonStockCheck.setCentralWidget(self.centralwidget)
            self.menubar = QtWidgets.QMenuBar(AmazonStockCheck)
            self.menubar.setGeometry(QtCore.QRect(0, 0, 804, 21))
            self.menubar.setObjectName("menubar")
            AmazonStockCheck.setMenuBar(self.menubar)
            self.statusbar = QtWidgets.QStatusBar(AmazonStockCheck)
            self.statusbar.setObjectName("statusbar")
            AmazonStockCheck.setStatusBar(self.statusbar)

            self.retranslateUi(AmazonStockCheck)
            QtCore.QMetaObject.connectSlotsByName(AmazonStockCheck)

        def retranslateUi(self, AmazonStockCheck):
                _translate = QtCore.QCoreApplication.translate
                AmazonStockCheck.setWindowTitle(_translate("AmazonStockCheck", "MainWindow"))
                self.producturl.setText(_translate("AmazonStockCheck", "Product Url"))
                self.phonenumber.setText(_translate("AmazonStockCheck", "Phone Number"))
                self.startbutton.setText(_translate("AmazonStockCheck", "Start"))
                self.abouttext.setText(_translate("AmazonStockCheck", "App developed by Gabriel Zebersky Beta Version"))


        def onclick(self, url):
            headers = {"User-Agent": 'Mozilla/5.0 (X11; Linux x86_64)', 'Cache-Control': 'no-cache', "Pragma": "no-cache"}
            page = requests.get(url, headers=headers)
            soup = BeautifulSoup(page.content, 'html.parser')
            while True:
                if soup.find(id='outOfStock'):
                    print('NOT IN STOCK')
                    time.sleep(2)
            else:
                print('IN STOCK')


if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    AmazonStockCheck = QtWidgets.QMainWindow()
    ui = Ui_AmazonStockCheck()
    ui.setupUi(AmazonStockCheck)
    AmazonStockCheck.show()
    sys.exit(app.exec_())

I saw that maybe threading might work. Is there any way I could use Qthread or Qthreadpool to fix this problem?


Solution

  • Windows will tell you that your application (supposedly) crashed because it has hang. What it means is that the code is not processing any event (so the app is frozen), probably because it is stuck doing something else.

    In your application, when you click on the button, the onclick method gets called. But this function never returns because it will indefinitely check the item availability (due to while True with no break).
    What is does is that the Main Thread (which in PyQt handles the GUI) gets stuck in this method after the buttons gets clicked.

    Indeed, you should use a thread for that : the auxiliary thread will get stuck in a loop, but the Main Thread will still be free to run and handle the events.

    Here is a minimal example of a worker that does not block the main thread :

    import random
    import time
    import typing
    
    from PyQt5 import QtCore, QtGui, QtWidgets
    
    
    class Ui_AmazonStockCheck(object):
            def setupUi(self, AmazonStockCheck):
                AmazonStockCheck.resize(804, 617)
                AmazonStockCheck.setWindowTitle('Stocker')
                self.centralwidget = QtWidgets.QWidget(AmazonStockCheck)
                self.urlBox = QtWidgets.QLineEdit(self.centralwidget)
                self.urlBox.setGeometry(QtCore.QRect(50, 90, 161, 51))
                self.startbutton = QtWidgets.QPushButton("Start", self.centralwidget)
                self.startbutton.setGeometry(QtCore.QRect(320, 160, 141, 41))
                self.startbutton.clicked.connect(lambda: self.onclick(self.urlBox.text()))
                QtCore.QMetaObject.connectSlotsByName(AmazonStockCheck)
    
                layout = QtWidgets.QVBoxLayout()
                layout.addWidget(self.urlBox)
                layout.addWidget(self.startbutton)
                self.centralwidget.setLayout(layout)
                AmazonStockCheck.setCentralWidget(self.centralwidget)
    
            def onclick(self, url):
                print(f"starting to check availability for {url!r}")
                self.checker = AvailabilityChecker(url, self.centralwidget)
                print("checker has started")
                self.checker.availability_update.connect(
                    lambda is_available: print(f"received update: available={is_available}")
                )
    
    
    class AvailabilityChecker(QtCore.QObject):
        def __init__(self, url, parent: QtCore.QObject):
            super().__init__(None)  # a QObject with a parent can not be moveToThread'd
            self.url = url
    
            self._thread = QtCore.QThread(parent)
            self.moveToThread(self._thread)
            assert QtCore.QThread.currentThread() != self._thread  # we are still running in the caller's thread
            self._thread.started.connect(self.check_indefinitely)
            self._thread.start()
    
        def check_indefinitely(self) -> typing.NoReturn:
            assert QtCore.QThread.currentThread() == self._thread  # we are now running in our dedicated thread
            while True:
                is_available = (random.randint(0, 10) == 9)
                if is_available:
                    print("AVAILABLE")
                else:
                    print("NOT AVAILABLE")
                self.availability_update.emit(is_available)
                time.sleep(2)
    
        availability_update = QtCore.pyqtSignal([bool])
    
    
    if __name__ == "__main__":
        import sys
        app = QtWidgets.QApplication(sys.argv)
        main_window = QtWidgets.QMainWindow()
        ui = Ui_AmazonStockCheck()
        ui.setupUi(main_window)
        main_window.show()
        sys.exit(app.exec_())
    

    You can see that you can still use the GUI while the worker, in its own thread, endlessly checks for the availability (I just random for convenience).

    I simply connected a lambda to the signal that the worker emits, but you should connect it to a method of your GUI to update its state (change a color, a text, whatever).

    Also, the thread runs endlessly, so your application does not exit cleanly. You should consider having a slot on your worker to change a boolean should_stop, and change the loop into while not should_stop, so that you can call the slot to make your thread exit shortly after.

    But I hope you can understand the gist of my answer. Yes, use an object in a thread (a "worker"). Use signals to communicate between your worker and the GUI. There are a lot of examples and documentation about how to do that.

    If it suits you, I found that making the worker class also inherit QRunnable (in addition to QObject for signal/slots) makes possible to delegate all the threading to a QThreadPool, which simplifies much of the code.