Metadata-Version: 2.1
Name: tkthread
Version: 0.4.0
Summary: Easy multithreading with Tkinter
Home-page: http://github.com/serwy/tkthread
Author: Roger D. Serwy
Author-email: roger.serwy@gmail.com
License: Apache Version 2.0
Keywords: tkinter threading
Platform: Windows
Platform: Linux
Platform: Solaris
Platform: Mac OS-X
Platform: Unix
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Software Development :: User Interfaces
Description-Content-Type: text/markdown
License-File: LICENSE

# tkthread

Easy multithreading with Tkinter on CPython 2.7/3.x and PyPy 2.7/3.x.

    import tkthread; tkthread.tkinstall()   # do this before importing tkinter

## Background

The Tcl/Tk language that comes with Python follows a different threading
model than Python itself which can raise obtuse errors when mixing Python
threads with Tkinter, such as:

    RuntimeError: main thread is not in main loop
    RuntimeError: Calling Tcl from different apartment
    NotImplementedError: Call from another thread

Tcl can have many isolated interpreters, and each are tagged to the
its particular OS thread when created. Python's `_tkinter` module checks
if the calling Python thread is different than the Tcl/Tk thread, and if so,
[waits one second][WaitForMainloop] for the Tcl/Tk main loop to begin
dispatching. If there is a timeout, a RuntimeError is raised. On PyPy,
a [NotImplementedError][PyPyNotImplemented] is raised.

For non-Tk calls into Tcl, Python will raise an apartment RuntimeError
when calling a Tcl interpreter from a different thread.

A common approach to avoid these errors involves using `.after` to set up
[periodic polling][PollQueue] of a [message queue][PollRecipe] from
the Tcl/Tk main loop, which can slow the responsiveness of the GUI.

The current approach used in `tkthread` is to use the Tcl/Tk `thread::send`
messaging to notify the Tcl/Tk main loop of a call for execution.
This interrupt-style architecture has lower latency and better
CPU utilization than periodic polling. This works with CPython and PyPy.

The experimental approach used in `tkthread` is to use `tkthread.tkinstall()`
to patch Tkinter when make calls into Tcl/Tk. This only works on CPython and
it does not require the `Thread` package in Tcl.


## Usage on CPython/PyPy

The `tkthread` module provides the `TkThread` class, which can
synchronously interact with the main thread.

    from tkthread import tk, TkThread

    root = tk.Tk()        # create the root window
    tkt = TkThread(root)  # make the thread-safe callable

    import threading, time
    def run(func):
        threading.Thread(target=func).start()

    run(lambda:     root.wm_title('FAILURE'))
    run(lambda: tkt(root.wm_title,'SUCCESS'))

    root.update()
    time.sleep(2)  # _tkinter.c:WaitForMainloop fails
    root.mainloop()

The `tkt` instance is callable, and will wait for the Tcl/Tk main loop
to execute and compute a result which is then passed back for
return in the calling thread. A non-synchronous version also exists that
does not block:

    tkt.nosync(root.wm_title, 'ALSO SUCCESS')

There is an optional `tkt.install()` method which intercepts Python-to-Tk
calls. This must be called on the default root, before the creation of child
widgets. If installed, then wrapping Tk widget calls in threaded code with
`tkt` is not necessary. There is, however, a slight performance penalty for
Tkinter widgets that operate only on the main thread because of the
thread-checking indirection.

The `root` Tcl/Tk interpreter must be the primary interpreter on the
main thread. If it is not, then you will receive a TclError of the form:

    _tkinter.TclError: invalid command name "140520536224520_call_from"

For example, creating several `Tk()` instances and then using TkThread
on those will cause this error.

A good practice is to create a root window and then call `root.withdraw()`
to keep the primary Tcl/Tk interpreter active. Future Toplevel windows
use `root` as the master.


## Experimental Usage on CPython (simpler)

For CPython 2.7/3.x, `tkhread.tkinstall()` can be called first,
and will patch Tkinter to re-route threaded calls to the Tcl interpreter
using the "willdispatch" internal API call.

    import tkthread; tkthread.tkinstall()
    import tkinter as tk

    root = tk.Tk()

    import threading
    def thread_run(func): threading.Thread(target=func).start()

    @thread_run
    def func():
        root.wm_title(threading.current_thread())

        @tkthread.main(root)
        @tkthread.current(root)
        def testfunc():
            tk.Label(text=threading.current_thread()).pack()

    root.mainloop()


## Install

    pip install tkthread


## Known Errors

You may receive this error when using `tkthread.TkThread(root)`:

    _tkinter.TclError: can't find package Thread

This means that Python's Tcl/Tk libraries do not include the `Thread` package,
which is needed by `TkThread`.

On Debian/Ubuntu:

	`apt install tcl-thread`

On Windows, you'll need to manually update your Tcl installation to include
the `Thread` package.

The simpler solution is to use `tkthread.tkinstall()` instead.


## License

Licensed under the Apache License, Version 2.0 (the "License")

## See Also

These libraries offer similar functionality, using periodic polling:
* https://github.com/RedFantom/mtTkinter
* https://github.com/abarnert/mttkinter
* https://pypi.org/project/threadsafe-tkinter

[PollQueue]: http://effbot.org/zone/tkinter-threads.htm
[PollRecipe]: https://www.oreilly.com/library/view/python-cookbook/0596001673/ch09s07.html
[WaitForMainloop]: https://github.com/python/cpython/blob/38df97a03c5102e717a110ab69bff8e5c9ebfd08/Modules/_tkinter.c#L342
[PyPyNotImplemented]: https://bitbucket.org/pypy/pypy/src/d19ac6eec77b4e1859ab3dd8a5843989c4d4df99/lib_pypy/_tkinter/app.py?fileviewer=file-view-default#app.py-281


