0

What is the correct way of communicating (passing strings) between two different python scripts?

I have a ui.py script which utilizes PySide6 to generate a GUI, and I have another bot.py script which listens discord/telegram conversations and catching some keywords using async functions. Both scripts are at the same directory.

I have put Asyncio event loop code in my bot.py file to a function named runscript(), and using multiprocessing.Process from ui.py I run that function after I click a PySide6 QPushButton.

So the issue here is I want to display that keywords bot.py catches in my GUI so I need to pass that strings to ui.py (there will be need of passing strings the other way -from ui.py to bot.py- in the future) but I don't know how to this. I have tried multiprocessing.Pipe but that blocks my code because script fetches messages from discord/telegram when a new message arrives (using Asyncio) and I can not wait that to happen.

#bot.py

# do other stuff above here
@discord_client.event
async def on_message(message):
    if message.channel.id in discord_channel_list:
        discord_message = message.content
        selected_symbol = message_analyzer(discord_message)
        print(selected_symbol)

async def discord_connection():
    await discord_client.start(discord_token)

def runscript():
    connection = asyncio.get_event_loop()
    connection.create_task(binance_connection())
    connection.create_task(discord_connection())
    connection.create_task(telegram_connection())
    connection.create_task(connection_check())

    try:
        connection.run_forever()
    except KeyboardInterrupt:
        print("\nShutting down...")
    except:
        print("\nWARN! Shutting down...")

For example I need to get value of selected_symbol and transfer it to the ui.py

#ui.py

import bot

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.start_button = QPushButton("Start")
        self.start_button.clicked.connect(self.run)

    def run(self):
        bot_process = Process(target=bot.runscript)
        bot_process.daemon = True
        bot_process.start()

What is the correct way to achieve this? Thanks in advance.

1 Answers1

1

In general Qt is not process-safe so you should not update the GUI from another process. An alternative is to create a QThread (or threading.Thread) that only parses the information of the Queue and emits a signal with the information to update the GUI. Another option is to use a QTimer that does the above: monitor the Queue.

class Worker(QObject):
    messageChanged = Signal(str)

def monitoring(p, worker):
    while True:
        try:
            msg = p.recv()
        except EOFError:
            break
        else:
            worker.messageChanged.emit(msg)
r, w = Pipe(duplex=False)
p = Process(target=foo, args=(w,))

worker = Worker()
worker.messageChanged.connect(self.some_slot)

threading.Thread(target=monitoring, args=(r, worker), daemon=True).start()
p.start()

But using multiprocessing can add unnecessary complexity, instead you can use qasync(python -m pip install qasync) and then use asyncio:

import asyncio
from functools import cached_property
import sys

import discord
from PySide6 import QtCore, QtWidgets
from qasync import QEventLoop, asyncSlot


class DiscordManager(QtCore.QObject):
    connected = QtCore.Signal()
    disconnected = QtCore.Signal()
    messageChanged = QtCore.Signal(discord.message.Message)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.client.event(self.on_message)
        self.client.event(self.on_connect)
        self.client.event(self.on_disconnect)

    @cached_property
    def client(self):
        return discord.Client()

    async def on_message(self, message):
        self.messageChanged.emit(message)

    async def start(self):
        await self.client.start(
            "<TOKEN>"
        )

    async def close(self):
        await self.client.close()

    async def on_connect(self):
        self.connected.emit()

    async def on_disconnect(self):
        self.disconnected.emit()


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.button = QtWidgets.QPushButton("Start")
        self.label = QtWidgets.QLabel(alignment=QtCore.Qt.AlignCenter)

        central_widget = QtWidgets.QWidget()
        lay = QtWidgets.QVBoxLayout(central_widget)
        lay.addWidget(self.button)
        lay.addWidget(self.label)
        self.setCentralWidget(central_widget)

        self.button.clicked.connect(self.handle_clicked)

        self.manager.connected.connect(self.handle_connected)
        self.manager.disconnected.connect(self.handle_disconnected)
        self.manager.messageChanged.connect(self.handle_message)

    @cached_property
    def manager(self):
        return DiscordManager()

    @asyncSlot()
    async def handle_clicked(self):
        if self.button.text() == "Start":
            await self.manager.start()
        else:
            await self.manager.close()

    def handle_message(self, message):
        self.label.setText(message.content)

    def handle_connected(self):
        self.button.setText("Stop")

    def handle_disconnected(self):
        self.button.setText("Start")


def main():
    app = QtWidgets.QApplication(sys.argv)
    loop = QEventLoop(app)
    asyncio.set_event_loop(loop)
    w = MainWindow()
    w.show()
    loop.run_forever()


if __name__ == "__main__":
    main()
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Tried this now and works quite well, however I need to start() discord.Client when I press a button on the GUI, not when script initializes. Is it possible to do such thing? If it is this is a good start me. After this I need to do same thing for telethon and implement other methods like message analyzer. – Alperen Öztürk Mar 03 '21 at 11:32
  • @AlperenÖztürk Update the example with that functionality – eyllanesc Mar 03 '21 at 11:54
  • @AlperenÖztürk That is another problem so you have to create another post. If you do not know the rules then read [ask] and check the [tour], here we solve specific problems, we do not guide anyone in their project as it is too much task nor do we implement what is not in the question. We won't do all your work – eyllanesc Mar 03 '21 at 12:08
  • Of course, I don't have any purpose like that. Sorry for my lack of knowledge. I am just new to stackoverflow and how this community works. Deleted the comment. – Alperen Öztürk Mar 03 '21 at 12:19
  • @AlperenÖztürk Precisely to avoid having that type of discussion or making that kind of excuses I have provided you with those links that I hope you read before publishing a new post. Also if you can read [answer] – eyllanesc Mar 03 '21 at 12:20
  • @AlperenÖztürk Please do not modify your post because if you do my answer would lose meaning, as I have already pointed out: If you have a new problem (in your case the integration with telegram) then create a **new** post. – eyllanesc Mar 03 '21 at 12:22
  • Anyway, after testing your new answer which include client.close() and client.start() with button push, I can say that it is working but not quite perfect. After closing the client and starting it again, program fails to update label text. And if I try to click start again and (just to make sure) it gives me "Unclosed client session" and "Unclosed connector" errors. By the way there is mis typo in you answer: you emit connected signal both from on_connect and on_disconnect methods. You may want to fix this as emit disconnected. – Alperen Öztürk Mar 03 '21 at 13:14