2

Want to describe my task firstly. My aim is to create Discord bot that sends a particular message exported from .txt in chat with a time interval of N seconds therefore I'm currently trying to use libraries such as discord, pyqt6 and asyncio.

I've tried to use time library but the problem is that time.sleep() delays all processes so the GUI becomes unusable and I cannot implement other constructions due to my lack of experience (I swear I tried my best). Hence I tried using asyncio and I found it suitable because it will not delay GUI.

My code includes a class with different methods and finally executes in main() but I cannot comprehend how am I supposed to execute method async def func() whereas every other functions does not demand async execution. Here is my code. I will be super grateful for every advise or connected literature because I've been browsing the internet for a couple days now and I still found barely anything to comprehend. Thank you.

class DiscordBot(QtWidgets.QMainWindow, widget.Ui_Form):

    def __init__(self):
        super().__init__()
        self.setupUi(self) 
        self.start_button.clicked.connect(self.printer)
    
    def __getitem__(self, item):
        file = open('phrases.txt')
        a = file.read().split('\n')
        for item in range(len(a)):
            yield a[item]
        file.close()
    
    async def printer(self):
        self.listWidget.clear()
        i = 0
        a = DiscordBot.__getitem__(self, item=i)
        for i in a:
            self.listWidget.addItem(DiscordBot.__getitem__(self, item=i))
            await asyncio.sleep(5)

def main():
    app = QtWidgets.QApplication(sys.argv)
    window = DiscordBot() 
    window.show()  
    app.exec()  
    asyncio.run(window.printer())

if __name__ == '__main__':  
    main()
4hdrey
  • 21
  • 4
  • Should `asyncio.run(DiscordBot.printer(DiscordBot()))` be `asyncio.run(window.printer())`? At the very least it should be `asyncio.run(DiscordBot().printer())`, but this is creating a new bot, and you've already shown `window` so presumably you want to run it on `window`. – Kraigolas Jan 24 '22 at 15:27
  • 1
    Hi! Thanks for your attention! I thought that if I go with asyncio.run(DiscordBot().printer()) it will result in warning so I tried adding something to it to work and it resulted it present code. Thats my lack of experience. You are completely right and I've already made changes. Thanks once again. – 4hdrey Jan 24 '22 at 16:36
  • If you just need to perform a simple operation at regular intervals and that is *not* potentially blocking, use [QTimer](https://doc.qt.io/qt-5/qtimer.html). – musicamante Jan 24 '22 at 17:28

2 Answers2

1

You're not completely understanding magic methods use cases and python classes as well, I suppose. You don't have to call methods on class, self is much more readable. __getitem__ is a method called when you access class using brackets: self[index] or class_instance[index].

Now to threading. PyQt provides QThread implementation for this purposes, you can create new thread, move worker to it and start the thread. Worker is any QObject you like.

So you can rewrite the code like this:

from PyQt5.QtWidgets import (QMainWindow, QApplication, QWidget,
                             QPushButton, QVBoxLayout, QListWidget)
from PyQt5.QtCore import QObject, QThread, pyqtSignal
import time
import sys


class Printer(QObject):
    ''' Worker for long-running task

    This worker will run in separate thread and perform required action
    '''

    sent = pyqtSignal(str)

    @property  # omit parentheses, allow easier caching (if you want)
    def phrases(self):
        ''' Data to send '''
        # context manager will close file for you
        with open('/tmp/phrases.txt') as file:
            # This way you can skip reading whole file into memory
            # (it's important if file is large)
            for line in file:  # file is already iterable!
                yield line.strip('\n')

    def start_printer(self):
        ''' Write text '''
        for phrase in self.phrases:  # iterate over your generator
            self.sent.emit(phrase)
            time.sleep(5)


class DiscordBot(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setupUi()
        self.start_button.clicked.connect(self.run_task)

    def setupUi(self):
        # I don't have your layout, so wrote something simple to reproduce.
        # delete it when you're ready.

        self.resize(300, 150)
        self.centralWidget = QWidget()
        self.setCentralWidget(self.centralWidget)
        self.start_button = QPushButton("Start", self)
        self.listWidget = QListWidget(self)
        layout = QVBoxLayout()
        layout.addWidget(self.start_button)
        layout.addWidget(self.listWidget)
        self.centralWidget.setLayout(layout)

    def run_task(self):
        ''' Start your thread '''
        self.thread = QThread()  # new thread
        self.worker = Printer()  # initialize worker
        self.worker.moveToThread(self.thread)  # run in separate thread

        self.thread.started.connect(self.worker.start_printer)  # entry point
        self.thread.started.connect(self.listWidget.clear)
        self.worker.sent.connect(self.listWidget.addItem)

        self.thread.start()  # actually start the job


def main():
    app = QApplication(sys.argv)
    window = DiscordBot()
    window.show()
    app.exec()
    # no need to do smth else, app will handle everything for you


if __name__ == '__main__':
    main()

If you want more details and deeper knowledge (signals handlng, for instance, to communicate with thread and report progress), here's a good article.

Thanks to @musicamante: fixed thread-safety. We really should use signals to affect widgets instead of operating directly using reference, as user might do something with UI at the same time.

STerliakov
  • 4,983
  • 3
  • 15
  • 37
  • Thanks for your answer! I'm amazed that my question has been really worked on by you. You used PyQt5 and I had to make some changes to your code to make it work but finally it worked. However my IDE signaled a warning that it couldn't find reference 'connect' in 'function|function' but after some research I'd ignored it and now the task is solved. Thanks once more! – 4hdrey Jan 24 '22 at 18:15
  • @SUTerliakov note that those lambdas are both unnecessary and wrong: 1. both pairs have compatible signals (`started` and `clear` have no arguments, and `sent` and `addItem` use strings); 2. using lambdas between threads is highly discouraged as Qt uses thread affinity to understand if sender and receiver are on different threads (and eventually queue the signal), while lambdas prevent that detection, with the result that the UI may still be accessed from the other thread. Change to `self.thread.started.connect(self.listWidget.clear)` and `self.worker.sent.connect(self.listWidget.addItem)`. – musicamante Jan 24 '22 at 18:22
  • @4hdrey those warnings were probably caused by the issue mentioned in my comment above. While you can ignore those warning, those lambdas are still a problem, so remove them and change the connections as explained. – musicamante Jan 24 '22 at 18:26
  • @musicamante brilliant, thank you! Fixed that, haven't noticed before that signature fits. I've never heard about anonymous functions disadvantages in this use case, could you elaborate a while or point me to documentation saying that? I have seen `signal.connect(lambda data: ...)` in production code a few times. – STerliakov Jan 24 '22 at 18:42
  • @SUTerliakov the problem is lambdas *and* threading. While for simple cases it usually is not a problem, it's generally preferable to avoid lambdas for signals unless really necessary, and specifically avoid it in threads: if the lambda contains a reference to an object that gets deleted while the signal is queued (for instance, the list widget), you'll get an exception which a direct connection will be avoided (when an object is deleted, it's disconnected as well); if the lambda is created in a thread that is different from that it will act upon, you'll probably get problems as well. – musicamante Jan 24 '22 at 19:04
  • @SUTerliakov read [this answer](https://stackoverflow.com/a/20818401). Again, in a simple case like the above, it shouldn't represent an issue (the lambda is created in the main thread), but it still should be done only when *really* knowing what it's being done and how both threading *and* [Py]Qt/[PySide] work, and avoid suggesting it as a common practice that would potentially led people to believe that lambdas could be used with signal anywhere between threads. – musicamante Jan 24 '22 at 19:07
  • @musicamante Thanks! I'll look deeper into it. – STerliakov Jan 24 '22 at 19:08
0

You can send blocking functions to other threads via;

from threading import Thread
Thread(target=myfunc).start()

or asyncio's executors;

import asyncio
from concurrent.futures import ProcessPoolExecutor
executor = ProcessPoolExecutor(5)
loop = asyncio.get_event_loop()
loop.run_in_executor(executor, myfunc)

but keep in mind both methods can not share data as they run on different threads.

CherrieP
  • 21
  • 3
  • Hi! Thank you for your answer. I've read about threading but I found it so complicated so I opt for asyncio library. Can you please specify the threading method using functions described in my post? – 4hdrey Jan 24 '22 at 16:43
  • From the answer by @SUTerliakov, you can use their threading instead which looks much easier to implement. – CherrieP Jan 24 '22 at 17:09