A simple framework for event-driven programming, based on the Observer design pattern.
Reason this release was yanked:
Version 2 has major changes in how the package works
Project description
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 thesubject
. - When you subscribe an object to an
Event
, that object is anobserver
. - When you
invoke
anEvent
instance, you're notifying allobservers
that thesubject
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 theEvent
. - 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
(anobject
) becomes aSubscriber
ofe
(anEvent
).o
is destroyed viadel
before being unsubscribed frome
.
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
(anobject
) becomes aSubscriber
ofe
(anEvent
).o
is unreferenced everywhere in code, except ine
(as asubscriber
).
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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.