Package: simplevent
SimpleventSummarySimplevent is a simple Event framework for Python, loosely based on the Observer design pattern. The package is minimal:
it defines theEventbase class and theSignedEventandNamedEventsubclasses. An instance of either encapsulates
alistbutwill also itself behave somewhat like alist; this is essentially anindirection.Observer PatternSimplevent's tiny framework can be seen as a variation on theObserver Pattern:When you instantiate anEvent, that instance's context (scope) is thesubject.When you subscribe an object to anEvent, that object is anobserver.When youinvokeanEventinstance, you're notifying allobserversthat thesubjecthas executed an important action.MotivationSimplevent 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 TypesThere are two types ofEventin Simplevent:StrEventandRefEvent. Both share a few similarities:Subscribers are encapsulated in alist, 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 todocstringsfor more information.Str EventAnStrEventis anEventthat stores a "callback name" as astring. Once invoked, it will go through all of itssubscribers, looking for a method name that matches the storedstring.Here's an example where a video-game Character
is supposed to stop moving after a Timer has reached zero, with simplified code:ExamplefromsimpleventimportStrEventclassTimer:def__init__(self,init_time:float=60):"""Initialized the timer.:param init_time: The initial time, in seconds."""self._time_left=init_timeself.time_is_up=StrEvent("on_time_is_up")# The event is defined here.defstart_timer(self):"""Starts the timer."""coroutine.start(self.decrease_time,loop_time=1,delay_time=0)defstop_timer(self):"""Stops the timer."""coroutine.stop(self.decrease_time)defdecrease_time(self):"""Decreases the time by 1."""self._time_left-=1ifself._time_left<=0:self.stop_timer()self.time_is_up()# Sugar syntax; same as `self._time_is_up.invoke()`classPlayerCharacter(ControllableGameObject):def__init__(self):self._is_input_enabled=TrueGameMode.get_global_timer().time_is_up+=self# Sugar syntax; same as `self._time_is_up.add(self)`# Other code ...# ...defenable_input(self):"""Enabled user input (e.g. movement, etc)."""self._is_input_enabled=Truedefdisable_input(self):"""Disables user input (e.g. movement, etc)."""self._is_input_enabled=Falsedefon_time_is_up(self):"""Called automatically when the global timer has reached zero."""self.disable_input()# Other code ...# ...Ref EventSubscribersof aRefEventmust beCallableobjects. In other words, theSubscriberhas to be afunction, amethod,
or a "functor-like"object(anobjectwith the__call__magic method overloaded). That's because aRefEvent- unlike
anStrEvent- will call itsSubscribersdirectlyinsteadof looking for amethodof a certain name.Here's the same example as inStrEvent- a video-game Character that is supposed to stop moving after a Timer has reached
zero - but usingRefEventinstead, again with simplified code:ExamplefromsimpleventimportRefEventclassTimer:def__init__(self,init_time:float=60):"""Initialized the timer.:param init_time: The initial time, in seconds."""self._time_left=init_timeself.time_is_up=RefEvent()# The event is defined here.defstart_timer(self):"""Starts the timer."""coroutine.start(self.decrease_time,loop_time=1,delay_time=0)defstop_timer(self):"""Stops the timer."""coroutine.stop(self.decrease_time)defdecrease_time(self):"""Decreases the time by 1."""self._time_left-=1ifself._time_left<=0:self.stop_timer()self.time_is_up()# Sugar syntax; same as `self._time_is_up.invoke()`classPlayerCharacter(ControllableGameObject):def__init__(self):self._is_input_enabled=TrueGameMode.get_global_timer().time_is_up+=self.disable_input# Sugar syntax; same as `self._time_is_up.add(self.disable_input)`# Other code ...# ...defenable_input(self):"""Enabled user input (e.g. movement, etc)."""self._is_input_enabled=Truedefdisable_input(self):"""Disables user input (e.g. movement, etc)."""self._is_input_enabled=False# Other code ...# ...Important NotesNo Reference ManagementSimplevent'sEventinstances do not automatically manage references to theirSubscribers. That means it is up to the
developer to manage references. Here are a couple of examples:Null Subscriberso(anobject) becomes aSubscriberofe(anEvent).ois destroyed viadelbefore being unsubscribed frome.The above is a problem becauseewill still attempt to callowhen invoked, which will result in anError(likely
aTypeError,AttributeError, or similar).Persistent Subscriberso(anobject) becomes aSubscriberofe(anEvent).ois unreferenced everywhere in code, except ine(as asubscriber).oexists insideeas a reference. Python's garbage collection willnot destroyountil all references to itcease to exist -including the one insidee, which representsoas aSubscriber. The developer must be very
careful and ensure thatois unsubscribed fromewhenever needed.