tkmilan.bg#

Model the background Event Loop processing.

Module Attributes

NORMAL_PRIORITY

Normal priority for Event Loop Requests.

Classes

ELCallback(widget, callback[, idle, afterMS])

Event Loop Callback definition.

ELReq()

Event Loop Request.

ELReq_Priority(request, priority, count)

Event Loop Request, with priority.

ELRes()

Event Loop Response.

EventLoop(*[, qinput, qoutput, wcallback, ...])

Event Loop Object

class tkmilan.bg.ELCallback(widget: Union[tk.Widget, tk.Tk], callback: Callable[[EventLoop, int | None, ELRes | bool], Any], idle: bool = True, afterMS: int = 0)#

Bases: object

Event Loop Callback definition.

Parameters:
  • widget (Union[tk.Widget, tk.Tk]) – Widget to attach the callback to

  • callback (Callable[[EventLoop, int | None, ELRes | bool], Any]) – Function to call with the response.

  • idle (bool) – Choose after_idle over after. Defaults to True. See Tcl after and after idle documentation.

  • afterMS (int) – If idle is False, delay execution. See Tcl after documentation.

trigger(eventloop: EventLoop, ridx: int | None, response: ELRes | bool) str#

Trigger the callback using the given response.

See EventLoop for the kinds of responses produced.

Parameters:
  • eventloop (EventLoop) – The corresponding EventLoop object.

  • ridx (int | None) – The response index, or None for signals. See also EventLoop.

  • response (ELRes | bool) – The response payload. For signals, this is a bool indicating if the EventLoop is starting or stopping. See also EventLoop.

Returns:

Return the after/after idle ID.

Return type:

str

class tkmilan.bg.ELReq#

Bases: object

Event Loop Request.

Usually represents a task to be performed on an EventLoop.

See EventLoop to see how to use this.

class tkmilan.bg.ELReq_Priority(request: ~tkmilan.bg.ELReq, priority: int = 0, count: int = <factory>)#

Bases: object

Event Loop Request, with priority.

This is the actual object which is included on the EventLoop input queue. Wraps the ELReq object ignoring the ordering, so that it can be included on the PriorityQueue considering only the priority, and count to maintain the original ordering.

Parameters:
  • request (ELReq) – The Event Loop request object.

  • priority (int) – The request priority. Optional, see NORMAL_PRIORITY for the default value.

  • count (int) –

    The request count, used to maintain the order when these objects are pushed to a PriorityQueue.

    See the Python PriorityQueue implementation notes. documentation. The default value does The Right Thing.

See also

This is to be used on a PriorityQueue, see the Python documentation.

class tkmilan.bg.ELRes#

Bases: object

Event Loop Response.

See EventLoop to see how to use this.

Note

If using an Event Bus, this should use a single subclass level, that is this should only be subclassed once, and those subclasses should not be subclassed further. See RootWindow.register_eventbus_response for more information.

class tkmilan.bg.EventLoop(*, qinput: PriorityQueue | None = None, qoutput: Queue | None = None, wcallback: None | Callable[[EventLoop, int | None, ELRes | bool], Any] | ELCallback = None, name: str | None = None, priorities: Mapping[Type[ELReq], int] | None = None, **kwargs: Any)#

Bases: object

Event Loop Object

Represent all Event Loop settings and necessary references.

An event loop is basically a big while True: loop. The tasks to process are ELReq_Priority (ELReq with priority) objects, and that processing produces a stream of responses.

There are two kinds of responses:

  • A “signal”, index is None, payload is bool.

    This signals the Event Loop started or stopped task processing.

  • A “response”, index is int, payload is ELRes.

    The index indicates the count of the current processing task.

Internally uses the threading functions. This means the threading model is cooperative, that is, the task processing functions must yield their time, not hog the processing time for themselves.

The event loop itself is single-threaded, meaning there’s only at most a single task is being executed. The input queue is a PriorityQueue, which means the request order is not fixed, there are only defaults. The requests are processed in priority order (lowest first).

You can choose between two different response parsing behaviours, depending on the arguments you give:

  • qoutput: Define the response output as a queue, possibly given from outside.

  • wcallback: Define some a Tk callback function to be called.

    Even though Tk is not thread-safe, some specific functions are, and this is used to “merge” the event process loop with the existing Tk event loop. See ELCallback for more information on how those functions are defined.

Note that qoutput is the default, when you do not choose one or the other.

Avoid using time.sleep directly when implementing the tasks. Use isleep/usleep for precise control over interruptible behaviour.

Parameters:
  • qinput (PriorityQueue) – PriorityQueue with ELReq_Priority to consume. Optional, creates a new queue when not given.

  • qoutput (Queue | None) – Queue with ELRes to produce. Optional, creates a new queue when not given. See above for the interaction with wcallback.

  • wcallback (ELCallback | None) – Callback Configuration object. Optional. See above for the interaction with qoutput.

  • name (str | None) – The Event Loop thread name. Optional, overrides name if given. See tName for the final name.

  • priorities (Mapping[Type[ELReq], int] | None) – Define Event Loop request default priorities. Optional, overrides priorities if given.

All other keyword arguments are passed to the setup_eventloop function.

Note

This object should be created on the Main Thread, not from any sub-thread. This is enforced on debug mode.

Note

In case qoutput is chosen, the main Tk processing loop for responses must implement something like this, using an Interval:

import tkmilan
from tkmilan.model import Interval

class EventLoop(tkmilan.bg.EventLoop):
    # Implement

class RW(tkmilan.RootWindow):
    def setup_eventloops(self):
        return {'sleep': EventLoop(name='sleep')}

    def setup_adefaults(self):
        self.start_eventloops()
        for _, el in self.get_eventloops(bus=False):
            logger.debug('# EventLoop tName=%s', eventloop.tName)
            # Process Events every second
            Interval(self, lambda: self.onProcessEventsEL(el), 1000, immediate=True)

    def onProcessEventsEL(self, eventloop: bg.EventLoop):
        for ridx, response in eventloop.responses(chunk=10):  # Or other amounts
            if ridx is None:
                logger.debug('eventloop=%s   Signal: %s', eventloop.tName, response)
            else:
                logger.debug('eventloop=%s Response: %r', eventloop.tName, response)
cntRequests() int#

Return the approximate size of the request queue.

This indicates unprocessed tasks. See the upstream function qsize.

Note

This should be called from the Main Thread, not from any sub-thread. It can be called from any thread, including the Event Loop Thread, but that is usually a logic error.

cntResponses() int | None#

Return the approximate size of the response queue, if exists.

This indicates unprocessed responses. See the upstream function qsize.

Returns:

Returns None if there is no output queue (connected to a Tk event loop directly).

Return type:

int | None

Note

This should be called from the Main Thread, not from any sub-thread. It can be called from any thread, including the Event Loop Thread, but that is usually a logic error.

is_paused() bool#

Check if the event loop is paused.

This means a dead event loop that has some requests to be processed.

This means the corresponding thread was alive at some point, but no longer. It can be restarted once again, to keep processing the remaining events.

See also

The is_running function should be used on most situations.

is_running() bool#

Check if the Event Loop is running.

This means the thread is ready to process tasks.

Once the thread is stopped, it stops running, but the thread itself might remain alive for a while for cleanup.

isleep(duration: float) bool#

Suspend the thread execution for a while, interruptible.

This suspension can be interrupted by calling stop.

Note

This is called from the Event Loop Thread, when implementing the processing functions. See process.

Do not use this in other contexts.

See also

For a non-interuptible thread suspension, see usleep.

process(task: ELReq, priority: int)#

Process a single task, with a priority.

Defaults to doing nothing, subclasses should redefine this. See singledispatchmethod for a possible solution for polymorphism based on ELReq subclasses.

The cntProcessed counter is unique for each task during this call, and it is attached to the responses queued during this call. This means it can be used to match request and respond, even though everything happens asynchronously. To do that, save that result on the response, and compare it with ridx.

Do not use this directly, see queue.

Note

This is called from the Event Loop Thread, automatically.

queue(task: ELReq, *, priority: int | None = None)#

Queue a new task on the event loop, optionally with a priority.

Append a new task to the Event Loop. You can set a priority for the task, or leave it with the default priority: priorities and the ultimate NORMAL_PRIORITY fallback.

It is possible to queue tasks into a paused event loop. This will be processed once the event loop restarts.

Parameters:

Note

This should be called from the Main Thread, not from any sub-thread. It can be called from any thread, including the Event Loop Thread, but that is usually a logic error.

respond(response: ELRes)#

Add a new response on the output.

Append a new response on the Event Loop output queue, or equivalent.

Note

This is called from the Event Loop Thread, when implementing the processing functions. See process.

Do not use this in other contexts.

responses(chunk: int = 10) Iterable[Tuple[int | None, bool | ELRes]]#

Retrieve responses from the event loop.

Parameters:

chunk (int) – Retrive at most this amount of responses.

Returns:

This is a generator that produces data tuples. See EventLoop for the kinds of responses produced here, the format is a tuple (index, payload).

Return type:

Iterable[Tuple[int | None, bool | ELRes]]

Note

This should be called from the Main Thread, not from any sub-thread. It can be called from any thread, including the Event Loop Thread, but that is usually a logic error.

run()#

Run the EventLoop.

This is the target function for the thread object.

Do not use this directly, see start.

setup_eventloop(**kwargs) None#

Setup the EventLoop object, with the other keyword arguments.

Defaults to doing nothing, subclasses should redefine this.

setup_process_respond(response_type: Type[ELRes]) Callable[[ELReq, int], None]#

Setup a basic process function, with static responses.

Setup a process function with a simple log, with a response response_type().

Note

This is mostly useful for testing, the correct way to implement this is to subclass the EventLoop class.

signal(signal: bool)#

Signal a new internal event on the output.

Should append a bool signal on the output queue, or equivalent.

Note

This is called from the Event Loop Thread, when implementing the processing functions. See process.

Do not use this in other contexts.

start(*, daemon: bool = True) Thread#

Start executing the Event Loop, in a background thread.

There will be a single thread processing event at a time. If you try to “start” a running Event Loop, this raises an exception.

Parameters:

daemon (bool) – Set the daemon option for thread creation. Defaults to True.

Note

This should be called from the Main Thread, not from any sub-thread. It can be called from any thread, including the Event Loop Thread, but that is usually a logic error.

stop(*, join: bool = False)#

Stop execution the Event Loop, ASAP.

If you try to “stop” a non-running Event Loop, this raises an exception.

Since the threading model is cooperative, the currently executing task (if any) is not interrupted. The only exception is the isleep process, that is interrupted.

Parameters:

join (bool) – Wait for the thread to finish. Defaults to False, fully asynchronous behaviour.

Note

This should be called from the Main Thread, not from any sub-thread. It can be called from any thread, including the Event Loop Thread, but that is usually a logic error.

toggle() Thread | None#

Toggle Event Loop execution.

If is_running, stop it, otherwise start it. Useful to have a single button for controlling the Event Loop execution state.

Returns:

Returns None if the Event Loop was stopped, or the next Event Loop Thread Object otherwise.

Return type:

Thread | None

usleep(duration: float) bool#

Suspend the thread execution for a while, interruptible.

This suspension cannot be interrupted; when calling stop, the event loop will only stop after this suspension ends.

Note

This is called from the Event Loop Thread, when implementing the processing functions. See process.

Do not use this in other contexts.

See also

For an interuptible thread suspension, see isleep.

cntProcessed: int#

Counter for correctly processed ELReq tasks.

This is incremented right after the processing function run successfully. It can be safely used from within the process function, in the Event Loop Thread, but not from other threads (no locking is implemented).

name: str | None = None#

Thread Name, for all instances in the class.

This is the per-class setting, can be overriden. See tName for the per-instance name.

priorities: Mapping[Type[ELReq], int] | None = None#

Event Loop Request priorities.

Optional. This is the per-class setting, can be overriden.

tName: str#

Thread Name

thread: Thread | None#

Event Loop Thread Object.

None when the event loop is not alive, points to the Event Loop Thread otherwise. See is_running.

Automatically managed by start/stop.

tkmilan.bg.NORMAL_PRIORITY: int = 0#

Normal priority for Event Loop Requests.

See also

See ELReq_Priority and EventLoop.queue for usage.