simplevent

A simple framework for event-driven programming, based on the Observer design pattern.


Keywords
event, observer, listener, subscription, subscriber, subject, design, pattern, callback
License
MIT
Install
pip install simplevent==1.0.1

Documentation

Simplevent

Summary

Simplevent is a simple Event framework for Python, loosely based on the Observer design pattern. The package is minimal: it defines the Event base class and the SignedEvent and NamedEvent subclasses. An instance of either encapsulates a list but will also itself behave somewhat like a list; this is essentially an indirection.

Observer Pattern

Simplevent's tiny framework can be seen as a variation on the Observer Pattern:

  • When you instantiate an Event, that instance's context (scope) is the subject.
  • When you subscribe an object to an Event, that object is an observer.
  • When you invoke an Event instance, you're notifying all observers that the subject has executed an important action.

Motivation

Simplevent was a creation inspired by C# and its event framework that's already built into the language. The lack of a similar system in Python can hinder event-driven designs. Designing a framework - even one as simple as Simplevent - can be time-consuming. This package provides an easy, small-scale solution for event-driven programming.

Event Types

There are two types of Event in Simplevent: StrEvent and RefEvent. Both share a few similarities:

  • Subscribers are encapsulated in a list, which is encapsulated by the Event.
  • Subscribing the same object twice is not allowed by default (but this can be changed).
  • Some sugar syntax is available: += (subscribe), -= (unsubscribe), and () (invoke).
  • Some magic method compatibility is available: len (currently the only one).

Each type can be customized/configured via their respective constructor. Refer to docstrings for more information.

Str Event

An StrEvent is an Event that stores a "callback name" as a string. Once invoked, it will go through all of its subscribers, looking for a method name that matches the stored string.

Here's an example where a video-game Character is supposed to stop moving after a Timer has reached zero, with simplified code:

Example

from simplevent import StrEvent

class Timer:
    
    def __init__(self, init_time: float = 60):
        """
        Initialized the timer.
        :param init_time: The initial time, in seconds.
        """
        self._time_left = init_time
        self.time_is_up = StrEvent("on_time_is_up")  # The event is defined here.
    
    def start_timer(self):
        """Starts the timer."""
        coroutine.start(self.decrease_time, loop_time=1, delay_time=0)
        
    def stop_timer(self):
        """Stops the timer."""
        coroutine.stop(self.decrease_time)
    
    def decrease_time(self):
        """Decreases the time by 1."""
        self._time_left -= 1
        if self._time_left <= 0:
            self.stop_timer()
            self.time_is_up()  # Sugar syntax; same as `self._time_is_up.invoke()`

class PlayerCharacter(ControllableGameObject):
    
    def __init__(self):
        self._is_input_enabled = True
        GameMode.get_global_timer().time_is_up += self  # Sugar syntax; same as `self._time_is_up.add(self)`
        # Other code ...
        # ...
        
    def enable_input(self):
        """Enabled user input (e.g. movement, etc)."""
        self._is_input_enabled = True

    def disable_input(self):
        """Disables user input (e.g. movement, etc)."""
        self._is_input_enabled = False
        
    def on_time_is_up(self):
        """Called automatically when the global timer has reached zero."""
        self.disable_input()

    # Other code ...
    # ...

Ref Event

Subscribers of a RefEvent must be Callable objects. In other words, the Subscriber has to be a function, a method, or a "functor-like" object (an object with the__call__magic method overloaded). That's because a RefEvent - unlike an StrEvent- will call its Subscribers directly instead of looking for a method of a certain name.

Here's the same example as in StrEvent - a video-game Character that is supposed to stop moving after a Timer has reached zero - but using RefEvent instead, again with simplified code:

Example

from simplevent import RefEvent

class Timer:
    
    def __init__(self, init_time: float = 60):
        """
        Initialized the timer.
        :param init_time: The initial time, in seconds.
        """
        self._time_left = init_time
        self.time_is_up = RefEvent()  # The event is defined here.
    
    def start_timer(self):
        """Starts the timer."""
        coroutine.start(self.decrease_time, loop_time=1, delay_time=0)
        
    def stop_timer(self):
        """Stops the timer."""
        coroutine.stop(self.decrease_time)
    
    def decrease_time(self):
        """Decreases the time by 1."""
        self._time_left -= 1
        if self._time_left <= 0:
            self.stop_timer()
            self.time_is_up()  # Sugar syntax; same as `self._time_is_up.invoke()`

class PlayerCharacter(ControllableGameObject):
    
    def __init__(self):
        self._is_input_enabled = True
        GameMode.get_global_timer().time_is_up += self.disable_input  # Sugar syntax; same as `self._time_is_up.add(self.disable_input)`
        # Other code ...
        # ...
        
    def enable_input(self):
        """Enabled user input (e.g. movement, etc)."""
        self._is_input_enabled = True

    def disable_input(self):
        """Disables user input (e.g. movement, etc)."""
        self._is_input_enabled = False

    # Other code ...
    # ...

Important Notes

No Reference Management

Simplevent's Event instances do not automatically manage references to their Subscribers. That means it is up to the developer to manage references. Here are a couple of examples:

Null Subscribers

  • o (an object) becomes a Subscriber of e (an Event).
  • o is destroyed via del before being unsubscribed from e.

The above is a problem because e will still attempt to call o when invoked, which will result in an Error (likely a TypeError,AttributeError, or similar).

Persistent Subscribers

  • o (an object) becomes a Subscriber of e (an Event).
  • o is unreferenced everywhere in code, except in e (as a subscriber).

o exists inside e as a reference. Python's garbage collection will not destroy o until all references to it
cease to exist - including the one inside e, which represents o as a Subscriber. The developer must be very careful and ensure that o is unsubscribed from e whenever needed.