teek

Teek is a pythonic alternative to tkinter.


Keywords
pythonic, tk, tcl, tkinter, gui, beginner
License
MIT
Install
pip install teek==0.6

Documentation

Teek

Build Status Documentation Status Coverage Status

Teek is a pythonic and user-friendly alternative to tkinter. It doesn't come with Python so you need to install it yourself, but it's nice and light-weight.

Documentation: https://teek.rtfd.org/

Teek is Pythonic

If you have worked with tkinter a lot, you know that it's kind of annoying. Almost everything is represented as strings in Tcl. Tkinter is dumb and it doesn't try to do things like they would be usually done in Python; instead, tkinter users need to deal with many inconveniences themselves. On the other hand, Teek is pythonic; it does things like they are best done in Python, not how they are done in Tcl.

Ttk

Never heard of Ttk before? Shame on you. Ttk is the new way to write GUIs in Tk, and you should be already using it in tkinter. Ttk GUIs look a lot better than non-Ttk GUIs on most platforms. For example, this GUI has a Ttk button and a non-Ttk button. Guess which is which:

good and bad button

The problem is that Tk's windows (in tkinter, tkinter.Toplevel and tkinter.Tk) are not Ttk widgets. If you add Ttk widgets into them, the GUI looks messy on some systems, like my linux system with MATE desktop. The solution is to add a big Ttk frame that fills the window, and then add all widgets into that frame.

Tkinter:

# this is a well-done hello world, and tbh, most people don't use tkinter "well"
import tkinter
from tkinter import ttk

root = tkinter.Tk()
big_frame = ttk.Frame(root)
big_frame.pack(fill='both', expand=True)   # make sure it fills the root window
ttk.Label(big_frame, text="Hello World!").pack()
root.mainloop()

Teek:

import teek

window = teek.Window("Hello")
teek.Label(window, "Hello World!").pack()
window.on_delete_window.connect(teek.quit)
teek.run()

All teek widgets are Ttk, so you don't need to do a separate import to use ttk widgets. Also, when you create a teek Window, the big ttk frame is created and packed automatically for you, and you don't need to think about it at all; you just create a Window and add stuff into it.

Threads

Here time.sleeps represent blocking things. In real life you could e.g. do network requests, run a subprocess or perform CPU-sensitive computations in the thread.

Tkinter:

import queue
import threading
import time
import tkinter

root = tkinter.Tk()
root.title("Thread Demo")
text = tkinter.Text(root)
text.pack()

message_queue = queue.Queue()

def queue_poller():
    while True:
        try:
            message = message_queue.get(block=False)
        except queue.Empty:
            break
        text.insert('end', message)

    root.after(50, queue_poller)

def thread_target():
    message_queue.put('doing things...\n')
    time.sleep(1)
    message_queue.put('doing more things...\n')
    time.sleep(2)
    message_queue.put('done')

threading.Thread(target=thread_target).start()
queue_poller()
root.mainloop()

Teek:

import threading
import time
import teek

text = teek.Text(teek.Window("Thread Demo"))
text.pack()

def thread_target():
    text.insert(text.end, 'doing things...\n')
    time.sleep(1)
    text.insert(text.end, 'doing more things...\n')
    time.sleep(2)
    text.insert(text.end, 'done')

teek.init_threads()
threading.Thread(target=thread_target).start()
window.on_delete_window.connect(teek.quit)
teek.run()

This is not a joke. Using threads with tkinter is a horrible mess, but teek works with threads nicely. All you need is teek.init_threads(), and then you can do teek things from threads. See concurrency docs for details.

Debuggability

Tkinter:

label = ttk.Label(some_widget, text="hello world")
print(label)        # prints something like '.140269016152776', which is confusing
print(repr(label))  # somewhat better: <tkinter.ttk.Label object .140269016152776>

Teek:

label = teek.Label(some_widget, "hello world")
print(label)    # <teek.Label widget: text='hello world'>

Text Widget Indexes

The 4th character of the 3rd line of a text widget is the string '3.4' in Tcl and tkinter. This is not only confusing because 3.4 looks like a float even though treating it as a float messes things up, but this makes code look messy.

Tkinter:

# figure out where the cursor is
line, column = textwidget.index('insert').split('.')
line = int(line)
column = int(column)

# same thing, more concise
line, column = map(int, textwidget.index('insert').split('.'))

Teek:

line, column = textwidget.marks['insert']

textwidget.marks is a dictionary-like object with mark names as keys and text index namedtuples as values. Teek represents text indexes as namedtuples that have line and column attributes, which is useful if you only need the line. In tkinter, you need to parse the 'line.column' string with .split('.') and take the first element of the split result.

Tkinter:

cursor_lineno = int(textwidget.index('insert').split('.')[0])

Teek:

cursor_lineno = textwidget.marks['insert'].line

In tkinter you also need to construct the 'line.column' strings yourself, but in teek you can use (line, column) tuples.

Tkinter:

textwidget.mark_set('insert', '{}.{}'.format(new_cursor_line, new_cursor_column))

Teek:

textwidget.marks['insert'] = (new_cursor_line, new_cursor_column)

Tcl uses strings like 3.4 + 5 chars to denote the position that is 5 characters after the position 3.4. Teek's text position namedtuples have a pythonic forward() method that returns a new text position.

Tkinter:

# textwidget.index always returns the position as 'line.column'
new_position = textwidget.index('{}.{} + 5 chars - 1 line'.format(line, column))
# now new_position is a string, and you may need to parse it back to
# separate line and column

Teek:

new_position = textwidget.index(line, column).forward(chars=5).back(lines=1)
# new_position is now a text position namedtuple

Timeouts

In tkinter, any_widget.after(1000, func) runs func() after 1 second, and the any_widget can be any tkinter widget. That's right, you need a widget for scheduling timeouts. This can be a problem in library code. But what if during that 1 second of waiting time, you decide that you don't want to run the timeout after all? You can cancel the timeout, but as usual, teek makes it easier.

Tkinter:

widget = get_some_widget_from_somewhere()
timeout_id = widget.after(1000, my_function)
...
# debugging
print(timeout_id)       # prints 'after#0'... very useful, eh??
...
if we_actually_dont_want_to_timeout():
    widget.after_cancel(timeout_id)
    print(timeout_id)   # still prints 'after#0'

Teek:

timeout_object = teek.after(1000, my_function)
...
# debugging
print(timeout_object)   # prints <pending 'my_function' timeout 'after#0'>
...
if we_actually_dont_want_to_timeout():
    timeout.cancel()
    print(timeout)      # prints <cancelled 'my_function' timeout 'after#0'>

Developing teek

This section contains the commands I use when working on teek. If you use windows, replace python3 with py.

  • python3 -m pip install --user sphinx pytest pytest-cov flit installs everything you need for developing teek.
  • python3 -m pytest runs tests (they are in the tests subdirectory). It is normal to get lots of tiny windows on the screen while running the tests. I use these pytest options:
    • --skipslow makes the tests run faster by skipping tests that are decorated with @pytest.mark.slow.
    • --durations=10 prints the list of 10 slowest tests at the end of the test run. This is a good way to figure out which tests to mark slow.
    • --cov=teek runs the tests under coverage. Run python3 -m coverage html and open htmlcov/index.html to view the results. Coverage results from travis builds go to coveralls.
  • cd docs followed by py -m sphinx . _build builds documentation locally. You can view it by opening docs/_build/index.html in your browser. readthedocs builds the docs when you push to master, but it's best to make sure that everything's fine first.
  • Sphinx seems to only build parts of the documentation if you change some of it, but sometimes it doesn't detect your changes. Run cd docs followed by rm -r _build to make it build everything next time.
  • I don't usually lint the files on my system. I push to GitHub (to any branch), and if the travis build fails, I know I did something badly. If you want to lint things yourself, find the correct command from .travis.yml.
  • flit publish uploads to PyPI. You can ask me to run this after I have merged something to master.