1

I have made a program that takes the price of bitcoins (by using beautifulsoup) and displays it to the user. However, I want the price to get updated every 30 seconds or so, so I used the "threading" module and used its Timer. No matter how many seconds I type into the timer parameter, the program calls itself 5 times a second no matter what the seconds parameter is. Here is the code:

from bs4 import BeautifulSoup
from tkinter import *
import requests
import threading

root = Tk()

def bitcoinPrice():
    url = 'http://www.coindesk.com/price/'
    r = requests.get(url)
    soup = BeautifulSoup(r.text, 'html.parser')
    btcPrice = soup.find('div', attrs=
    {'class' : 'bpi-value bpiUSD'}
                 )
    btcIndexDown = soup.find('span', attrs=
    {'class' : 'bpi-change changeUSD data-down'}
                     )
    btcIndexUp = soup.find('span', attrs=
    {'class' : 'bpi-change changeUSD data-up'}
                     )
    if(btcIndexDown is None):

        return btcPrice.text + "(" + btcIndexUp.text + ")"

    else:

        return btcPrice.text + "(" + btcIndexDown.text + ")"




def bitcoinLabel():

    theLabel = Label(root, text = "-")
    theLabel.config(font = 'bold')
    theLabel.pack()
    updateBtcPrice(theLabel)




def updateBtcPrice(theLabel):
    if '-' in theLabel.cget("text"):
        theLabel.config(fg = 'red')
    else:
        theLabel.config(fg = 'green')

    theLabel.configure(text = bitcoinPrice())
    root.update()
    print("Hello")
    threading.Timer(5.0, updateBtcPrice(theLabel)).start()

try:
    bitcoinLabel()
except:
    pass
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
DanZoe
  • 111
  • 1
  • 3
  • 15
  • to run a function that doesn't block in Tkinter, you could [use `.after()` method (see how `tick()` is implemented)](http://stackoverflow.com/a/26609843/4279). To run a blocking function such as `bitcointPrice()`, you could [use a *single* thread to run a function periodically instead of spawning a new one for each call](http://stackoverflow.com/a/22498708/4279). Avoid calling tkinter's methods from other threads, arrange your code to call GUI code only in the main thread. – jfs Dec 12 '15 at 12:12
  • a safe way is to use a queue to communicate between threads so that only GUI thread runs GUI code, [here's a background thread reads subprocess output and passes it to GUI using a queue](https://gist.github.com/zed/42324397516310c86288) – jfs Dec 12 '15 at 12:23

2 Answers2

3

I think the problem is that you used the Timer interface incorrectly. Try instead:

threading.Timer(5.0, updateBtcPrice, theLabel).start()

The difference is that, contrary to your code, this version does not actually call updateBtcPrice when the event is scheduled.

mfred
  • 177
  • 6
  • Hi @mfred . It runs once then I get the following error: `self.function(*self.args, **self.kwargs) TypeError: updateBtcPrice() argument after * must be a sequence, not Label` – DanZoe Dec 12 '15 at 02:10
  • @DanZoe: you might need to pass `[theLabel]` instead of `theLabel` here. – jfs Dec 12 '15 at 12:06
  • Hi, @J.F.Sebastian it no says the following `RuntimeError: main thread is not in main loop` – DanZoe Dec 12 '15 at 13:19
  • @DanZoe: it is a separate issue. There are multiple issues in your code. @ mfred's answer fixes one of the issues but not all of them. ["avoid calling tkinter's method from other threads"](http://stackoverflow.com/questions/34235125/timer-not-working-for-method-recursively-calling-itself-every-n-seconds#comment56221504_34235125) – jfs Dec 12 '15 at 13:50
  • @J.F.Sebastian Hi I fixed the issue completely. I put the mainloop and the creation of theLabel in the global scope and then put the threading.timer method inside bitcoinLabel. It now updataes every 30 seconds. I just have a problem. Because the thread runs every 30 seconds, it takes 30 seconds before the thread quits after I click the X-button on the tkinter window. Is there any way of killing all threads immediately when exiting the tkinter window? – DanZoe Dec 12 '15 at 20:26
  • @DanZoe: the correct approach is shown in the links that I've provided in the comments to your question above. – jfs Dec 12 '15 at 20:41
  • @J.F.Sebastian The links you've shown me shows how to use the root.after method and calling a function periodically. I have fixed the issue for calling a method x amount of times. I just need a way of killing the threads (exiting the program completely) when I click the X-button on tkinter. Now I have to wait 30 seconds for the method to be called again and know that the tkinter window has been closed. – DanZoe Dec 12 '15 at 20:44
  • @DanZoe: there are three links – jfs Dec 12 '15 at 21:19
0

The problem I initally had was that the threading.Timer Method did not work properly. This was because of improper use of the threading.Timer() method. Instead of threading.Timer(5.0, updateBtcPrice(theLabel).start() I had to write it like threading.Timer(5.0, updateBtcPrice, theLabel).start(). (Mentioned by mfred). This fixed one issue but revelead a lot of others. I now got a RuntimeError main thread is not in main loop because my thread was not using the main loop after the first call to the method. J.F Sebastian pointed this out, so I fixed this by declaring and initializing some of the variables globally and put my mainloop in the global scope. Here is the program so far:

from bs4 import BeautifulSoup
from tkinter import *
import requests
import threading
from threading import Event, Thread
from timeit import default_timer
from tkinter import messagebox

def bitcoinPrice():
    url = 'http://www.coindesk.com/price/'
    r = requests.get(url)
    soup = BeautifulSoup(r.text, 'html.parser')
    btcPrice = soup.find('div', attrs=
    {'class' : 'bpi-value bpiUSD'}
                 )
    btcIndexDown = soup.find('span', attrs=
    {'class' : 'bpi-change changeUSD data-down'}
                     )
    btcIndexUp = soup.find('span', attrs=
    {'class' : 'bpi-change changeUSD data-up'}
                     )

    if(btcIndexDown is None):
        return btcPrice.text + "(" + btcIndexUp.text + ")"

    else:
        return btcPrice.text + "(" + btcIndexDown.text + ")"

def bitcoinLabel():
    try:
        theLabel.configure(text = bitcoinPrice())
        if '-' in theLabel.cget("text"):
            theLabel.config(fg = 'red')
        else:
            theLabel.config(fg = 'green')

        tm = threading.Timer(30, bitcoinLabel).start()

        if root is None:
            tm.stop()
    except RuntimeError:
    sys.exit(0)

def on_closing():
    root.destroy()
    sys.exit(0)

root = Tk()
theLabel = Label(root, text = "")
theLabel.config(font = 'bold')
theLabel.pack()

bitcoinLabel()
root.wm_protocol("WM_DELETE_WINDOW", on_closing)
root.mainloop()

Although the functionality of the code is working (and the initial problem I had was solved) there is room for improvement. One improvement that could be done is killing the thread immediately when you exit the tkinter gui. Instead it now takes 30 seconds (the interval between each time the thread calls the method) before it realises the gui is gone and the program exits. Other than that, it now works.

DanZoe
  • 111
  • 1
  • 3
  • 15
  • Your approach is wrong. You could fix the immediate problem if you call `tm.cancel()` (use `tm = Timer(...);tm.start()`) on WM_DELETE_WINDOW` event. The correct approach would call GUI code only from the GUI thread. You could use a queue to communicate with the background thread that makes http requests. – jfs Dec 12 '15 at 21:24