What signal to use to trigger methods from a thread in pyGtk?

1

I have a Tree View that I need popular with data obtained in a thread, but if I do it apart from it the program has several random problems and errors. Searching I found that the ideal is to trigger a signal from within the thread to be called a function that populates the tree view. So I created a button and connected the 'clicked' signal to the function that spares the tree view, and within the thread I 'emit' the clicked signal of that button. It worked perfectly, but I think it's a gambiarra 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 issued without the need to create a widget like I did? Here is 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()
    
asked by anonymous 10.01.2018 / 22:40

1 answer

1

Correct is to call the GObject.idle_add or GObject.timeout_add functions from the other thread. In this way the function will be called directly by the GTK, without depending on the processing of the signals. (Documentation here: link )

Just this would make your program run 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. And also, this program only makes sense if you keep updating the quotations. As each call is delayed, I made it so that with each API call, the quotation 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()

As this code is, you can create more parallel calls to the API simply by creating more threads with% 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 data already retrieved into an internal data structure, which allows the values to be updated, and inserted a minimal error handling in the API calls.

Python requests may already return the content in json decoded (and above all, in your previous code, you would not have to call self.thread more than once)

Happy Trading!

    
11.01.2018 / 16:00