Skip to main content

Simple, Subscribable, Custom Events.

Project description

Contributors Forks Stargazers Issues MIT License


Hair Trigger

Simple, Subscribable, Custom Events
Explore the docs »

Report Bug · Request Feature

Table of Contents
  1. About The Project
  2. Getting Started
  3. Usage
  4. License
  5. Contact
  6. Acknowledgments

About The Project

Hair Trigger offers custom, subscribable events in the style of Luau events that allow for decoupled access between objects.

(back to top)

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.

(back to top)

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.

(back to top)

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.

(back to top)

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 self inside the callback must shadow the self of 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.

(back to top)

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)

(back to top)

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.

(back to top)

(back to top)

License

Distributed under the MIT License. See LICENSE.txt for more information.

(back to top)

Contact

Better Built Fool - betterbuiltfool@gmail.com

Bluesky - @betterbuiltfool.bsky.social

Project Link: https://github.com/BetterBuiltFool/hair_trigger

(back to top)

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

hair_trigger-0.2.1.tar.gz (16.8 kB view details)

Uploaded Source

Built Distribution

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

hair_trigger-0.2.1-py3-none-any.whl (11.9 kB view details)

Uploaded Python 3

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

Hashes for hair_trigger-0.2.1.tar.gz
Algorithm Hash digest
SHA256 5220369b5c9716de98dc7ee3ac8d646f7ce705dd69e3bbb1d08853edf4c7f967
MD5 dd672bafba29e9b83c7ec9bcdbb0723c
BLAKE2b-256 6d677c4fe95533f2cad65dbec33367bd288f4ac34d8668957798cd01dc18ca10

See more details on using hashes here.

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

Hashes for hair_trigger-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 8a370817132bf20d97d1b7607b12675a6e714760c2111c784a17db7179227908
MD5 e3179c7f95ecb786b0835a17db7d221f
BLAKE2b-256 07be16bf606885a20e937345e33e7dd0b7e91419a0fa4dab9b9c279cd76ad16f

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