What signal to use for firing methods from a pygtk thread?

Asked

Viewed 98 times

1

I have a Tree View that I need popular with data obtained in a thread, but if I do it from it the program presents several random problems and errors. Researching found that the ideal is to trigger a signal from within the thread so that it is called a function that populates the Tree view. Then I created a button and connected the 'clicked' signal to the function that spares Tree view, and inside the thread I 'send' the 'clicked' signal from that button. It worked perfectly, but it seems like a scam and I’m not happy about it. Is there a more appropriate way to achieve the same result? Or is there a signal that can be sent without the need to create a widget like I did? Follow a functional summary of my code:

#!/usr/bin/env python3
#-*- coding: utf-8 -*-

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
import requests
import json
import threading

class App(Gtk.Window):
    def __init__(self):
        super(App, self).__init__()
        self.button = Gtk.Button() #botão criado apenas pra usar o sinal 'clicked na thread'
        self.button.connect('clicked', self.populate)

        self.tree_model = Gtk.ListStore(str, str, float)
        treeView = Gtk.TreeView(model = self.tree_model)
        cell = Gtk.CellRendererText() 
        column1 = Gtk.TreeViewColumn('Name', cell, text = 0)
        treeView.append_column(column1)
        column2 = Gtk.TreeViewColumn('Symbol', cell, text = 1)
        treeView.append_column(column2)
        column3 = Gtk.TreeViewColumn('Price $', cell, text = 2)
        treeView.append_column(column3)
        scrolled = Gtk.ScrolledWindow(hexpand = True)
        scrolled.add(treeView)
        self.set_default_size(500,200)
        self.connect('delete-event', Gtk.main_quit)
        self.add(scrolled)
        self.thread() #Iniciando a thread
        self.show_all()

    def populate(self, widget):
        self.tree_model.append([self.name, self.symbol, self.price])

    def get_data(self):
        coins = ('streamr-datacoin', 'ereal', 'burst')
        for coin in coins:
            response = requests.get('https://api.coinmarketcap.com/v1/ticker/{}'.format(coin))
            self.name = json.loads(response.text)[0]['name']
            self.symbol = json.loads(response.text)[0]['symbol']
            self.price = float(json.loads(response.text)[0]['price_usd'])
            self.button.emit('clicked') #emitindo sinal para chamar a função populate

    def thread(self):
        self.th1 = threading.Thread(target=self.get_data)
        self.th1.start()

App()
Gtk.main()

1 answer

1


The correct is to call the functions GObject.idle_add or GObject.timeout_add from the other thread. This way the function will be called directly by GTK, without relying on signal processing. (Documentation here: https://developer.gnome.org/gdk3/stable/gdk3-Threads.html)

Just that would make your program work minimally, but I’ve made some other improvements - things like passing thrad data on class attributes that can be overwritten are wrong in the sense of being prone to a rece condition. Also, this program only makes sense if you keep updating your quotations. Like every call takes time, I’ve made sure that every call to the API, the quote is updated, and not only when it has the value of all currencies.

#!/usr/bin/env python3
#-*- coding: utf-8 -*-

from itertools import cycle
import threading
import time
import sys

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import GObject
import requests

class App(Gtk.Window):
    def __init__(self):
        super().__init__()

        self.tree_model = Gtk.ListStore(str, str, float)
        treeView = Gtk.TreeView(model = self.tree_model)
        cell = Gtk.CellRendererText()
        column1 = Gtk.TreeViewColumn('Name', cell, text = 0)
        treeView.append_column(column1)
        column2 = Gtk.TreeViewColumn('Symbol', cell, text = 1)
        treeView.append_column(column2)
        column3 = Gtk.TreeViewColumn('Price $', cell, text = 2)
        treeView.append_column(column3)
        treeView.hexpand=True
        scrolled = Gtk.ScrolledWindow(hexpand = True)
        scrolled.add(treeView)
        self.set_default_size(500,200)
        self.connect('delete-event', Gtk.main_quit)
        self.add(scrolled)
        self.input_data = list()
        self.data = dict()
        self.coins = cycle(('streamr-datacoin', 'ereal', 'burst'))
        # GObject.timeout_add(50, self.get_data)
        # GObject.idle_add(self.populate)
        self.thread()
        self.show_all()

    def populate(self, *args):
        if self.input_data:
            data = self.input_data.pop(0)
            name = data["name"]
            if name not in self.data:
                self.data[name] = {"position": len(self.data),  "symbol": data["symbol"], "price": data["price"]}
                self.tree_model.append([name, data["symbol"], data["price"]])
            else:
                self.data[name]["symbol"] = data["symbol"]
                self.data[name]["price"] = data["price"]
                self.tree_model[self.data[name]["position"]] = ([name, data["symbol"], data["price"]])


    def get_data(self):
        while True:
            coin = next(self.coins)
            data = {}
            try:
                url = 'https://api.coinmarketcap.com/v1/ticker/{}'.format(coin)
                response = requests.get(url)
            except Exception as error:
                print("Error on request to ", url, file=sys.stderr)
            response_data = response.json()
            print(response_data)
            if not isinstance(response_data, list):
                print("Error on request to {}. response: {} ".format(url, response_data), file=sys.stderr)
            else:
                for field in "name symbol price_usd".split():
                    if response_data:
                        data[field] = response_data[0].get(field, "-")
                data["price"] = float(data.pop("price_usd"))
                self.input_data.append(data)
            GObject.idle_add(self.populate)
            # Next coin fetch every 2 seconds
            time.sleep(2)

    def thread(self):
        self.th1 = threading.Thread(target=self.get_data)
        self.th1.start()

App()
Gtk.main()

The way this code looks, you can create more calls parallel to the API by simply creating more threads with the self.thread as a target.

The trick is to use itertools.Cycle, which returns the next item in a list with "next", and returns to the beginning. It is also easy to see that I have organized the recovered data into an internal data structure, which allows updating the values, and inserted a minimum error handling in the calls to the API.

Python requests can also return decoded json content (and especially, in your previous code, you wouldn’t need to call json.loads more than once)

Happy Trading!

  • Simply a lesson! I only had one question, wouldn’t it be ideal to close the thread before closing Gtk.main()? In my application I overwritten the do_delete_event method, to change a variable inside while to false and close the loop.

  • Yes - doing something like this to end the thread was missing there.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.