A recurring problem in (not only) GUI programming are event cycles, i.e. the receiving of events oneself has triggered. These can quickly lead to event cycles, where change triggers change triggers change, until something gives out—usually the stack.
A particularly cheap way of breaking cycles are simple boolean flags:
class Foo(object): listen = True def some_method(self): self.listen = False try: # do stuff that triggers some_event finally: self.listen = True def on_some_event(self): if not self.listen: return # handle event normally
Boolean flags are great. They’re simple, they’ll make your code harder and half of the time, you’ll forget to set them to True after you’re done. Or you forget the try...finally... block. Or you forget to check them in the event handler. What could possibly go wrong.
Fortunately, some GUI toolkits provide ways to temporarily disable event handling for specific events, like GObject’s handler_block_by_func. This approach has two problems:
- You have to know the object (or objects) that emits the event.
- It only works for GObject events (signals).
Since I do have classes with their own event handling mechanism, in order to be independent of GObject and since it’s not really difficult to implement, I wanted a cross-event-framework way of temporarily blocking event handling. Or, maybe I just wanted to write another decorator/context manager very badly. Maybe a little bit of both.
class Foo(object): def some_method(self): with self.on_some_event.suspend(): # do stuff that triggers some_event @suspendable def on_some_event(self): # handle event normally
The event handlers are wrapped inside another method which swallows the event when the context manager is in suspended mode. The code remains blissfully ignorant of threading issues for now and also breaks down if the event handlers have meaningful result values. I’ve never encountered this so far, but adding default return values to the decorator is a simple extension. The methods are still proper bound methods and docstrings etc. are all conserved.
Without further ado, the code:
class Suspender(object): def __init__(self): self.is_suspended = 0 @contextmanager def __call__(self): self.is_suspended += 1 try: yield finally: self.is_suspended -= 1 @classmethod def suspendable(cls, meth): suspend_manager = cls() wrapper = suspend_manager.add_suspendable(meth) wrapper.suspend = suspend_manager wrapper.add_suspendable = suspend_manager.add_suspendable return wrapper def add_suspendable(self, meth): @wraps(meth) def suspended_wrapper(*args): if self.is_suspended == 0: meth(*args) return suspended_wrapper suspendable = Suspender.suspendable
It’s possible to have several event handlers block by a single context manager using the add_suspendable decorator added to suspendable methods:
@suspendable def on_some_event(self): ... @on_some_event.add_suspendable def on_misc_event(self): ...
Calling the suspend() context manager for on_some_event will also block on_misc_event.
The context manager does not prevent the event from being propagated, so it’s not a speed optimization; due to the added boolean check event + call indirection, handling actually becomes slightly slower. It likely won’t make a difference. If it does, event cycles are the least of your problems.