Simple, Subscribable, Custom Events.
Project description
Hair Trigger
Simple, Subscribable, Custom Events
Explore the docs »
Report Bug
·
Request Feature
Table of Contents
About The Project
Hair Trigger offers custom, subscribable events in the style of Luau events that allow for decoupled access between objects.
Getting Started
Hair Trigger is written in pure python, with no system dependencies, and should be OS-agnostic.
Installation
Hair Trigger can be installed from the PyPI using pip:
pip install hair_trigger
and can be imported for use with:
import hair_trigger
Hair Trigger has no dependencies beyond python itself.
Usage
Hair Trigger supplies no events by default, they must be custom made.
As an example, we'll make a simple event for detecting when an object is enabled.
Defining Events
from typing import Any
import hair_trigger
class OnEnable(hair_trigger.Event):
"""
Called whenever the owner becomes enabled.
:param this: The object being enabled.
"""
def trigger(self, this: Any) -> None:
return super().trigger(this)
Naming convention is suggested as On[Event name]. The trigger method must be defined, and must, at a minimum, call the super method. trigger's signature will also define the required signature of subscribing callbacks.
It is recommended to put the docstring describing trigger's parameters in the class docstring, so that it is visible to users.
Assigning Instances
Now we'll need an object to have the event.
class Foo:
def __init__(self, enabled: bool = False) -> None:
self.OnEnable = OnEnable()
self._enabled = enabled
When a Foo is created, a new instance of OnEnable is created for it, as well. It is recommended that the event attribute breaks normal snake_case style and uses PascalCase to make it clear that this is an event object rather than a method or a typical attribute.
Subscribing to Events
Let's say we want to print something when a Foo is enabled. Subscribing is done primarily by using the event instance as a decorator.
foo = Foo()
@foo.OnEnable
def do_the_thing(this: Foo) -> None:
print(f"{this} has been enabled")
# For simple expressions, a lambda is also okay
# For this though, we do not use it as a decorator.
foo.OnEnable(lambda this: print(f"{this} has been enabled"))
Additionally, objects can subscribe to an event as well. It uses the event as a decorator, too, but requires an additional parameter, the subscribing object. Subscribers need an owner so they don't tie up garbage collection.
class FooListener:
def __init__(self, foo: Foo) -> None:
@foo.OnEnable(self)
def _(self, this: Foo) -> None:
# Note: `self` here will shadow the `self` of init. This is important!
print(f"{self} noticed {this} is now enabled")
Alternatively, we can subscribe to a bound method directly, by using the event as a regular function. This doesn't require the subscribing object to be passed, it is extracted from the bound method.
class FooListener:
def __init__(self, foo:Foo) -> None:
foo.OnEnable(self.listen_in)
def listen_in(self, this: Foo) -> None:
print(f"{self} noticed {this} is now enabled")
Both versions have the same behavior, and if we have multiple of FooListener with the same Foo, the message will be printed once each.
Important notes:
- The callback must be subscribed in a method, not the class definition.
- The init method is a great candidate for callback subscription, but it can be done elsewhere if needed. Get creative!
- For new callbacks created inside the init/equivalent:
- The callback does not need a name, "_" is fine.
- The
selfinside the callback must shadow theselfof the init. This allows the callback to use the object, but won't prevent garbage collection due to a closure. - Other than the
self, the signature take all parameters as the trigger method of the event. Unused parameters can be caught with *args.
Triggering Events
Now that we have a listener, we'll need to actually to something to trigger the event. To do this, we'll simply need to call the trigger method of the event.
class Foo:
# init definition as above
@property
def enabled(self) -> bool:
return self._enabled
@enabled.setter
def enabled(self, enabled: bool) ->:
self._enabled = enabled
if enabled:
self.OnEnable.trigger(self)
Configuration
By default, Hair Trigger will attempt to run callbacks immediately, in syncronous mode. If asynchronous behavior is needed, or events need to be run manually or in a particular order, this can be changed using the config function.
Synchronous vs Asynchronous
The default system will run callback synchronously, so any blocking that occurs will block the entire thread. If that's undesireable, you can also use:
- ThreadRunner: Uses the Python threading module to run callbacks in new threads, good for general purpose multithreading.
- AsyncioRunner: Uses Python's asyncio module, useful for when threading must be async-aware, such as in WASM deployments.
import hair_trigger
from hair_trigger.runner import AsyncioRunner, ThreadRunner
# Run standard threads
hair_trigger.config(runner=ThreadRunner())
# Run async-aware
hair_trigger.config(runner=AsyncioRunner())
Scheduling Modes
Without config, triggering an event instantly begins notifying the event's subscribers, and if those trigger additional events, they'll take over mid-call. Instead, you can use a deferred scheduler.
Included are:
- StackScheduler: New events are put onto a stack, so that the newest event resolve before olderone resolve.
- QueueScheduler: New events are put into a queue, so events resolve in the order they are triggered.
The deferred schedulers must be triggered manually, using hair_trigger.scheduler.pump_events().
import hair_trigger
import hair_trigger.scheduler
from hair_trigger.scheduler import QueueScheduler
hair_trigger.config(scheduler=QueueScheduler())
# Do things to trigger events
hair_trigger.scheduler.pump_events()
Custom Runners and Schedulers
Runners and schedulers are protocols, so custom one can be created to get specific behaviors.
For example:
class LoggingThreadRunner:
def schedule(self, func: Callable[..., Any], *args, **kwds) -> None:
print(f"Calling function {func}")
threading.Thread(target=func, args=args, kwargs=kwds).start()
hair_trigger.config(LoggingThreadRunner())
This will log the function before starting the thread.
License
Distributed under the MIT License. See LICENSE.txt for more information.
Contact
Better Built Fool - betterbuiltfool@gmail.com
Bluesky - @betterbuiltfool.bsky.social
Project Link: https://github.com/BetterBuiltFool/hair_trigger
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.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hair_trigger-0.2.1.tar.gz.
File metadata
- Download URL: hair_trigger-0.2.1.tar.gz
- Upload date:
- Size: 16.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5220369b5c9716de98dc7ee3ac8d646f7ce705dd69e3bbb1d08853edf4c7f967
|
|
| MD5 |
dd672bafba29e9b83c7ec9bcdbb0723c
|
|
| BLAKE2b-256 |
6d677c4fe95533f2cad65dbec33367bd288f4ac34d8668957798cd01dc18ca10
|
File details
Details for the file hair_trigger-0.2.1-py3-none-any.whl.
File metadata
- Download URL: hair_trigger-0.2.1-py3-none-any.whl
- Upload date:
- Size: 11.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8a370817132bf20d97d1b7607b12675a6e714760c2111c784a17db7179227908
|
|
| MD5 |
e3179c7f95ecb786b0835a17db7d221f
|
|
| BLAKE2b-256 |
07be16bf606885a20e937345e33e7dd0b7e91419a0fa4dab9b9c279cd76ad16f
|