Skip to main content

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

Project description

Simplevent

Summary

Simplevent is a simple Event framework for Python, based on the Observer design pattern. The package is minimal: it defines the Event base class and the StrEvent and RefEvent 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 minimalist 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. In other words, duplicates are not supported.
  • Some sugar syntax is available: += (subscribe), -= (unsubscribe), and () (invoke).
  • Some magic method compatibility is available; e.g. len.

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

Str Event

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

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 ...
    # ...

For events that broadcast data (information about the event), StrEvent supports named parameters. Here is a small snippet:

from simplevent import StrEvent
energy_restored = StrEvent("on_energy_restored", "amount_restored")
# some subscriptions would happen here ...
# the expected signature is func(amount_restored: float) for direct access or func(**kwargs) for dictionary access
energy_restored(25.4)  # the event will call on_energy_restored on all subscribers and pass 25.4 or {"amount_restored": 25.4}

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 ...
    # ...

For events that broadcast data (information about the event), RefEvent supports type-safe signed parameters. Type safety is ensured via type hints, which are fetched at runtime via the inspect built-in module. Here is a small snippet:

from simplevent import RefEvent
energy_restored = RefEvent(float)
# some subscriptions would happen here ...
energy_restored(25.4)  # the event will call all subscribers and pass 25.4 (the amount of energy restored)

When defining events using RefEvents with parameters, make sure to document them, preferably with docstrings. This will ensure clarity and readability, specially with a long parameter list.

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

simplevent-2.2.0.tar.gz (10.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

simplevent-2.2.0-py3-none-any.whl (7.9 kB view details)

Uploaded Python 3

File details

Details for the file simplevent-2.2.0.tar.gz.

File metadata

  • Download URL: simplevent-2.2.0.tar.gz
  • Upload date:
  • Size: 10.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.11.9

File hashes

Hashes for simplevent-2.2.0.tar.gz
Algorithm Hash digest
SHA256 220515c4e0c5558636f23dd59b7f097f778e6c87542bbc69029e575b2a98a7d1
MD5 c9b13ad707105762f9e691ee828aeeed
BLAKE2b-256 03df192ce7d85040858171990f2010b0c8004c25d8043907f4a36df3fb6d57cd

See more details on using hashes here.

File details

Details for the file simplevent-2.2.0-py3-none-any.whl.

File metadata

  • Download URL: simplevent-2.2.0-py3-none-any.whl
  • Upload date:
  • Size: 7.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.11.9

File hashes

Hashes for simplevent-2.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ad6abf339ac2d9732dc321ef6d20988786465cf1f92d1b492151cdac12ea6bbc
MD5 8b4dff066f27cabe09fcb4a52e4a8cce
BLAKE2b-256 a385bc0991ec491e68a567eb6e0828b2e22c4b3328b455a9b3a8c42372982d05

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page