Source code for rjgtoys.tkthread

"""

.. autoclass:: EventQueue

.. autoclass:: EventGenerator

"""

import os
import queue
import threading

import tkinter as tk

import logging

log = logging.getLogger(__name__)


[docs]class EventQueue(queue.Queue): """This is a subclass of the standard library :class:`queue.Queue`. An :class:`~rjgtoys.tkthread.EventQueue` feeds any objects sent to it into a handler function that is called from the main Tk event loop. **NOTE**: The constructor for :class:`EventQueue` must be called from the main Tk thread. ``handler`` A callable that will be called to handle a process. It is called as ``handler(event)`` where ``event`` is a value that has previously been :meth:`put` to the :class:`EventQueue`. The ``handler`` is called from the tkinter event loop and may interact with tkinter objects, however note that it is *not* passed the ``widget`` that was passed to the :class:`EventQueue` constructor. ``widget`` A tkinter widget, or ``None`` to use the default root widget. This widget reference is used to create a Tk event handler; it doesn't really have to be associated with the events that are to be generated or handled. ``maxsize`` The maximum size of the queue. If ``maxsize <= 0`` the size is not limited. TODO: talk about exceptions from handler, and how to feed events in. An :class:`EventQueue` implements the context manager protocol, which means it can be used like this:: with EventQueue(handler=handle_event) as q: invoke_process_to_feed_events_to(q) The context manager exit operation calls :meth:`drain` on the queue, so all events have been processed by the time the ``with`` completes. .. automethod:: put .. automethod:: put_nowait .. automethod:: drain """ def __init__(self, handler, widget=None, maxsize=0): super().__init__(maxsize) self._pipe_r, self._pipe_w = os.pipe() self._handler = handler widget = widget or tk._default_root widget.tk.createfilehandler(self._pipe_r, tk.READABLE, self._readable)
[docs] def drain(self): """Close the queue for further events, and process any that are waiting.""" # Close the pipe for p in (self._pipe_r, self._pipe_w): try: os.close(p) except OSError: pass # Process all pending events while True: try: event = self.get(block=False) except queue.Empty: break try: self._handler(event) except Exception as e: log.exception("Exception raised by event handler")
def __enter__(self): return self def __exit__(self, typ, val, tbk): self.drain() def _readable(self, what, how): _ = os.read(self._pipe_r, 1) try: event = self.get(block=False) except queue.Empty: return try: self._handler(event) except Exception as e: log.exception("Exception raised by event handler")
[docs] def put(self, event, block=True, timeout=None): """Add an event to the queue. ``block`` If ``True`` (the default), then wait if the queue is full, otherwise raise :exc:`queue.Full` immediately. ``timeout`` If ``block`` is ``True``, specifies the maximum time to wait, in seconds, before raising :exc:``queue.Full`` if the queue remains full. A value of ``None`` means wait indefinitely. Ignored if ``block`` is ``False``. """ super().put(event, block=block, timeout=timeout) os.write(self._pipe_w, b"x")
[docs] def put_nowait(self, event): """Add an event to the queue without waiting. Either puts the event, or raises :exc:`queue.Full` immediately. """ return self.put(event, block=False)
""" target is the callable object to be invoked by the run() method. Defaults to None, meaning nothing is called. name is the thread name. By default, a unique name is constructed of the form “Thread-N” where N is a small decimal number. args is the argument tuple for the target invocation. Defaults to (). kwargs is a dictionary of keyword arguments for the target invocation. Defaults to {}. If a subclass overrides the constructor, it must make sure to invoke the base class constructor (Thread.__init__()) before doing anything else to the thread. """ # Name construction stuff copied from threading.py from itertools import count as _count # Helper to generate new EventGenerator names _counter = _count().__next__ _counter() # Consume 0 so first non-main thread has id 1. def _newname(template="EventGenerator-%d"): return template % _counter()
[docs]class EventGenerator(threading.Thread): """ An :class:`~rjgtoys.tkthread.EventGenerator` is a subclass of :class:`threading.Thread`. The constructor creates and optionally starts a thread that fetches values from an iterable, and feeds each into a :class:`~rjgtoys.tkthread.EventQueue` which in turn will make callbacks to a handler function, to process the events in the main Tk thread. ``generator`` An iterable that will provide the events to be processed. It will be called from a new thread, and each value that it generates will be put into a queue. ``queue`` The queue into which to put the generated events. If `None` is passed, a new :class:`~rjgtoys.tkthread.EventQueue` is created, using the ``handler``, ``widget`` and ``maxsize`` parameters - see :class:`~rjgtoys.tkthread.EventQueue` for descriptions of those. ``start`` A boolean that indicates whether the thread should be started. The default is to start the thread immediately. This saves having to write an explicit call to :meth:`start`. ``group`` Should be ``None``. This is reserved for a future extension to :class:`threading.Thread`. ``name`` A name for the thread. If ``None`` is passed, a name of the form ``EventGenerator-N`` is used, where ``N`` is an integer. Most of the above parameters will rarely be needed. The most typical pattern is expected to be:: EventGenerator( generator=source_of_events, handler=handler_of_events, widget=my_toplevel_widget ) This creates a (notionally) unlimited sized :class:`~rjgtoys.tkthread.EventQueue` which handles events put into it by calling ``handler_of_events`` from an event handler associated with ``my_toplevel_widget``. The events are generated by a thread that calls ``source_of_events`` until it is exhausted. :class:`~rjgtoys.tkthread.EventGenerator` implements the context manager protocol. If used as a context manager, exiting the context implies calling :meth:`drain` and so exit is delayed until the generator is exhausted and the queue drained - i.e. until all events have been processed. .. py:method:: start() Starts the event collection thread (a loop that calls the ``generator`` specified in the constructor). This call is unnecessary if ``start=True`` was passed (or defaulted) to the constructor. .. py:method:: join(timeout=None) Waits (for up to ``timeout`` seconds, or indefinitely if ``timeout is None``) for completion of the event generator. Note: does *not* drain the queue. .. automethod:: drain .. automethod:: run """ def __init__( self, *, generator=None, handler=None, start=True, queue=None, widget=None, group=None, name=None, maxsize=0, ): name = str(name or _newname()) super().__init__(group=group, name=name, daemon=True) self._generator = generator self._queue = queue or EventQueue(handler=handler, widget=widget, maxsize=maxsize) if start: self.start()
[docs] def run(self): """Consumes the generator iterable and sends each value to the queue. Terminates when and if the generator terminates. You might want to override this in a subclass if you want to use generators that have unusual ways of signalling (early?) completion. """ for work in self._generator: self._queue.put(work)
def __enter__(self): return self def __exit__(self, typ, val, tbk): self.drain()
[docs] def drain(self, timeout=None): """Wait until the generator and queue have been processed. Calls ``join(timeout)`` and then calls ``drain`` on the queue (even if the ``join`` timed out). """ self.join(timeout=timeout) self._queue.drain()