Tkinter and Asyncio

Thu 18 February 2021
Asynchronous process results waiting (Photo credit: Wikipedia)

Graphical interfaces are typically the kind of object that can take advantage of asynchrounous programming as a GUI spend lot of time waiting for user input.

Tkinter <https://docs.python.org/3/library/tkinter.html#module-tkinter>_ is a kind of standard for graphical interface on python. It was designed long before python3.5 and the introduction of asyncio in the standard library. Thus, it heavily rely on threading to allow non-blocking operations.

Asyncio refresh

This is a quick refresh on how asyncio works in python. This is not meant to be a tutorial.

Concurrent programming can be handled with threads. This mechanism works well in many compiled languages (such as C). In python (and in many other languages such as Ruby), only one thread can be executed simultaneously. More precisely, only one thread can hold the GIL and only a thread holding the GIL can execute python code.

When using thread, the system decides when a thread is interrupted and if another one is allow to run some python code before resuming. Thus, even if you have only one python thread running at a time, a global variable can be modified between python instructions (and you will always be unlucky if you don't use synchronisation mechanism such as mutex, semaphores, ...)

On the other hand, asyncio runs an event loop into which are put tasks. I won't go into the differences between awaitable, tasks, coroutines and futures. A tasks explicitly states when it can be interrupted waiting for the result of another tasks or operation (keyword await). The event loop decides which task is the next to run. To do so, it keeps a list of tasks with their respective states (pending and ready to run, sleeping and waiting for an external resources --usually an IO--, finished with the returned value or an error code, cancelled, and running)

A simple example is provided by the official documentation. It uses asyncio.sleep() to simulate the wait for an external resource that takes ate least the indicated delay.

import asyncio
import time


async def say_after(delay, what):
    await asyncio.sleep(delay)  # interrupt the task here waiting the result of asyncio.sleep()
    print(what)


async def main(delay, what):
    print(f"started at {time.strftime('%X')}")
    await say_after(1, "hello")  # interrupt the task here waiting for the result of "say_after"
    await say_after(2, "world")
    print(f"finished at {time.strftime('%X')}")


asyncio.run(main())  # start the event loop putting main() inside

To add a bit of complexity and randomness, we can use faker to decide what to say and random.randint to fix delays.

To fix the arguments of say_after:

from random import randint

import faker

word_generator = faker.Faker()
words = word_generator.words(7)
arguments = [(randint(0, 5), word) for word in words]

And now let's create and run tasks:

tasks = [
    asyncio.create_task(say_after(delay, what))
    for delay, what in arguments
]
done, pending = await asyncio.wait(tasks)

Tkinter refresh

Tkinter uses Frames and a new window can be build using tk.Toplevel(). I have few experience with this library, thus I decided to use a very limited set of widgets. For instance, I use tk.Button() just to display text on an area:

import tkinter as tk

class EmptyFrame(tk.Frame):
    def __init__(self, parent, message):
        super().__init__(parent)
        self.root = parent
        self.message = message
        self.create_entry()
        self.pack()

    def create_entry(self):
        self.area = tk.Button(self, text=self.message)
        print("Message: ", self.message)
        self.area.pack()

To create a new window with a message:

parent = tk.Toplevel(self.master)
EmptyFrame(parent, msg)

All together

The solution I found to use asyncio with Tkinter is to start a thread in charge of running the asyncio event loop. The other thread (the first one started by python) is here only to display the first window and start the asyncio thread.

To do so, I added lots of levels of indirections. I coded the following methods:

  • async_process(): the main asyncio process (equivalent to the main() in the example above).
  • run(): start the asyncio loop, calling async_process()
  • action(): handles the thread running the asyncio loop. Its main job is to call run()

Putting all together (a file is available here)

import asyncio
import threading
import tkinter as tk
from datetime import datetime
from random import randint

import faker


class Application(tk.Frame):
    """main frame"""

    def __init__(self, master=None):
        super().__init__(master)
        self.master = master
        self.pack()

        word_generator = faker.Faker()
        words = word_generator.words(5)
        self.messages = [(randint(0, 5), word) for word in words]
        print(self.messages)

        self.add_async_process()

    def add_async_process(self):
        """Add button to start the action
        """
        self.btn = tk.Button(self, text="Big button", command=self.action)
        self.btn.pack()

    def action(self):
        """initialize threading"""
        threading.Thread(target=self.run).start()

    def run(self):
        """start asyncio loop"""
        asyncio.run(self.async_process())

    async def async_process(self):
        self.start_time = datetime.now()
        self.new_area = tk.Button(
            self,
            text="\n".join(
                "\t".join((str(delay), message)) for delay, message in self.messages
            ),
        )
        self.new_area.pack()
        tasks = [
            asyncio.create_task(self.display_after(delay, what))
            for delay, what in self.messages
        ]
        done, pending = await asyncio.wait(tasks)

    async def display_after(self, delay, what):
        await asyncio.sleep(delay)
        now = datetime.now()
        msg = f"{now} \t {what}"
        parent = tk.Toplevel(self.master)
        EmptyFrame(parent, msg)


class EmptyFrame(tk.Frame):
    def __init__(self, parent, message):
        super().__init__(parent)
        self.root = parent
        self.message = message
        self.create_entry()
        self.pack()

    def create_entry(self):
        self.area = tk.Button(self, text=self.message)
        print("Message: ", self.message)
        self.area.pack()


if __name__ == "__main__":
    root = tk.Tk()
    app = Application(master=root)
    app.mainloop()

This is not the cleanest way of doing it, neither the minimal one, but it works and cover most of the cases I want.

Category: howto Tagged: python asyncio


Latex generator using Jinja

Wed 11 November 2020
Kawasukune-jinja (Photo creadit: Wikipedia)

The goal is to generate a PDF file using python. I decided to generate \(\LaTeX\).

Pipeline

I decided to use jinja as its documentation mention it.

\begin{equation*} \boxed{\text{Jinja template}} \xrightarrow[\text{python}]{} \boxed{\LaTeX} \xrightarrow[\text{pdflatex}]{} \boxed{\text{PDF}} \end{equation*}

The …

Category: LaTeX Tagged: python LaTeX Jinja

Read More

Travis setup

Tue 12 May 2020
One job in continuous integration pipeline (Photo credit: Wikipedia)

The goal is to setup a CI pipeline based on Travis with external dependencies integrated to a Github repository

Travis basics

To enable Travis integration in Github, one must edit ./.travis.yml file.

I won't go into detail. The setup is …

Category: how to Tagged: travis ci how to

Read More

Wikidata crawling

Sun 26 April 2020
Graph database representation (Photo credit: Wikipedia)

I wish to have reliable data about vehicles. I decided to rely on one large source, namely Wikipedia. I chose it because it is reviewable and most of the time reviewed, and regularly updated and completed.

Wikipedia - Wikidata relationship

Wikidata items are made to …

Category: how to Tagged: python wikipedia wikidata html

Read More

Differential equation in python

Sat 04 April 2020
Second order differential equation (Photo credit: Wikipedia)

In python, differential equations can be numerically solved thanks to scipy [1]. Is usage is not as intuitive as I expected.

Simple equation

Let's start small. The first equation will be really simple:

\begin{equation*} \frac{\partial{f}}{\partial{t}} = a \times f …

Category: maths Tagged: python maths equation

Read More

Zombie propagation

Sat 21 March 2020
Zombie favorite food warning (Photo credit: wikipedia)

I recently read a paper [1] trying to model a disease propagation. I wanted to play with this model.

The model

The model is know as "SIR" as it divide the population into 3 groups:

  • S: suceptible to become a zombie
  • I: infected …

Category: maths Tagged: python maths zombie

Read More

Python virtualenv: quick reference

Sun 21 July 2019
Virtual environement (Photo credit: wikipedia)

To isolate python developments, I use virtualenv. This allow me to forget about the specific python version used for each project, avoid interferences with the default python installation and between my projects, is relatively light, and may have other advantages I cannot imagine with my …

Category: programming Tagged: python tools code

Read More
Page 1 of 11

Next »