hair-trigger 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BetterBuiltFool
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,459 @@
1
+ Metadata-Version: 2.4
2
+ Name: hair_trigger
3
+ Version: 0.2.1
4
+ Summary: Simple, Subscribable, Custom Events.
5
+ Author-email: BetterBuiltFool <betterbuiltfool@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/BetterBuiltFool/hair_trigger
8
+ Project-URL: Issues, https://github.com/BetterBuiltFool/hair_trigger/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Topic :: Utilities
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.12.4
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Dynamic: license-file
17
+
18
+ <!-- Improved compatibility of back to top link: See: https://github.com/othneildrew/Best-README-Template/pull/73 -->
19
+ <a id="readme-top"></a>
20
+ <!--
21
+ *** Thanks for checking out the Best-README-Template. If you have a suggestion
22
+ *** that would make this better, please fork the repo and create a pull request
23
+ *** or simply open an issue with the tag "enhancement".
24
+ *** Don't forget to give the project a star!
25
+ *** Thanks again! Now go create something AMAZING! :D
26
+ -->
27
+
28
+
29
+
30
+ <!-- PROJECT SHIELDS -->
31
+ <!--
32
+ *** I'm using markdown "reference style" links for readability.
33
+ *** Reference links are enclosed in brackets [ ] instead of parentheses ( ).
34
+ *** See the bottom of this document for the declaration of the reference variables
35
+ *** for contributors-url, forks-url, etc. This is an optional, concise syntax you may use.
36
+ *** https://www.markdownguide.org/basic-syntax/#reference-style-links
37
+ -->
38
+ [![Contributors][contributors-shield]][contributors-url]
39
+ [![Forks][forks-shield]][forks-url]
40
+ [![Stargazers][stars-shield]][stars-url]
41
+ [![Issues][issues-shield]][issues-url]
42
+ [![MIT License][license-shield]][license-url]
43
+ <!--
44
+ [![LinkedIn][linkedin-shield]][linkedin-url]
45
+ -->
46
+
47
+
48
+
49
+ <!-- PROJECT LOGO -->
50
+ <br />
51
+ <!--
52
+ <div align="center">
53
+ <a href="https://github.com/BetterBuiltFool/hair_trigger">
54
+ <img src="images/logo.png" alt="Logo" width="80" height="80">
55
+ </a>
56
+ -->
57
+
58
+ <h3 align="center">Hair Trigger</h3>
59
+
60
+ <p align="center">
61
+ Simple, Subscribable, Custom Events
62
+ <br />
63
+ <a href="https://github.com/BetterBuiltFool/hair_trigger"><strong>Explore the docs »</strong></a>
64
+ <br />
65
+ <br />
66
+ <!--
67
+ <a href="https://github.com/BetterBuiltFool/hair_trigger">View Demo</a>
68
+ ·
69
+ -->
70
+ <a href="https://github.com/BetterBuiltFool/hair_trigger/issues/new?labels=bug&template=bug-report---.md">Report Bug</a>
71
+ ·
72
+ <a href="https://github.com/BetterBuiltFool/hair_trigger/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
73
+ </p>
74
+ </div>
75
+
76
+
77
+
78
+ <!-- TABLE OF CONTENTS -->
79
+ <details>
80
+ <summary>Table of Contents</summary>
81
+ <ol>
82
+ <li>
83
+ <a href="#about-the-project">About The Project</a>
84
+ </li>
85
+ <li>
86
+ <a href="#getting-started">Getting Started</a>
87
+ <ul>
88
+ <li><a href="#installation">Installation</a></li>
89
+ </ul>
90
+ </li>
91
+ <li><a href="#usage">Usage</a></li>
92
+ <ul>
93
+ <li><a href="#defining-events">Defining Events</a></li>
94
+ <li><a href="#assigning-instances">Assigning Instances</a></li>
95
+ <li><a href="#subscribing-to-events">Subscribing to Events</a></li>
96
+ <li><a href="#triggering-events">Triggering Events</a></li>
97
+ <li><a href="#configuration">Configuration</a></li>
98
+ </ul>
99
+ <!-- <li><a href="#roadmap">Roadmap</a></li> -->
100
+ <!--<li><a href="#contributing">Contributing</a></li>-->
101
+ <li><a href="#license">License</a></li>
102
+ <li><a href="#contact">Contact</a></li>
103
+ <li><a href="#acknowledgments">Acknowledgments</a></li>
104
+ </ol>
105
+ </details>
106
+
107
+
108
+
109
+ <!-- ABOUT THE PROJECT -->
110
+ ## About The Project
111
+
112
+ <!--
113
+ [![Product Name Screen Shot][product-screenshot]](https://example.com)
114
+ -->
115
+
116
+ Hair Trigger offers custom, subscribable events in the style of Luau events that allow for decoupled access between objects.
117
+
118
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
119
+
120
+
121
+ <!-- GETTING STARTED -->
122
+ ## Getting Started
123
+
124
+ Hair Trigger is written in pure python, with no system dependencies, and should be OS-agnostic.
125
+
126
+ ### Installation
127
+
128
+ Hair Trigger can be installed from the [PyPI][pypi-url] using [pip][pip-url]:
129
+
130
+ ```sh
131
+ pip install hair_trigger
132
+ ```
133
+
134
+ and can be imported for use with:
135
+ ```python
136
+ import hair_trigger
137
+ ```
138
+
139
+ Hair Trigger has no dependencies beyond python itself.
140
+
141
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
142
+
143
+
144
+
145
+ <!-- USAGE EXAMPLES -->
146
+ ## Usage
147
+
148
+ Hair Trigger supplies no events by default, they must be custom made.
149
+
150
+ As an example, we'll make a simple event for detecting when an object is enabled.
151
+
152
+ ### Defining Events
153
+
154
+
155
+ ```python
156
+ from typing import Any
157
+ import hair_trigger
158
+
159
+ class OnEnable(hair_trigger.Event):
160
+ """
161
+ Called whenever the owner becomes enabled.
162
+
163
+ :param this: The object being enabled.
164
+ """
165
+
166
+ def trigger(self, this: Any) -> None:
167
+ return super().trigger(this)
168
+ ```
169
+
170
+ 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.
171
+ It is recommended to put the docstring describing `trigger`'s parameters in the class docstring, so that it is visible to users.
172
+
173
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
174
+
175
+ ### Assigning Instances
176
+
177
+
178
+ Now we'll need an object to have the event.
179
+
180
+ ```python
181
+
182
+ class Foo:
183
+ def __init__(self, enabled: bool = False) -> None:
184
+ self.OnEnable = OnEnable()
185
+ self._enabled = enabled
186
+
187
+ ```
188
+
189
+ 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.
190
+
191
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
192
+
193
+ ### Subscribing to Events
194
+
195
+
196
+ 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.
197
+
198
+
199
+ ```python
200
+ foo = Foo()
201
+
202
+ @foo.OnEnable
203
+ def do_the_thing(this: Foo) -> None:
204
+ print(f"{this} has been enabled")
205
+
206
+
207
+ # For simple expressions, a lambda is also okay
208
+ # For this though, we do not use it as a decorator.
209
+
210
+ foo.OnEnable(lambda this: print(f"{this} has been enabled"))
211
+
212
+ ```
213
+
214
+ 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.
215
+
216
+ ```python
217
+
218
+ class FooListener:
219
+
220
+ def __init__(self, foo: Foo) -> None:
221
+
222
+ @foo.OnEnable(self)
223
+ def _(self, this: Foo) -> None:
224
+ # Note: `self` here will shadow the `self` of init. This is important!
225
+ print(f"{self} noticed {this} is now enabled")
226
+ ```
227
+
228
+ 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.
229
+
230
+ ```python
231
+
232
+ class FooListener:
233
+
234
+ def __init__(self, foo:Foo) -> None:
235
+
236
+ foo.OnEnable(self.listen_in)
237
+
238
+ def listen_in(self, this: Foo) -> None:
239
+ print(f"{self} noticed {this} is now enabled")
240
+ ```
241
+
242
+ Both versions have the same behavior, and if we have multiple of `FooListener` with the same `Foo`, the message will be printed once each.
243
+
244
+ Important notes:
245
+ - The callback must be subscribed in a method, not the class definition.
246
+ - The init method is a great candidate for callback subscription, but it can be done elsewhere if needed. Get creative!
247
+ - For new callbacks created inside the init/equivalent:
248
+ - The callback does not need a name, "_" is fine.
249
+ - 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.
250
+ - Other than the `self`, the signature take all parameters as the trigger method of the event. Unused parameters can be caught with *args.
251
+
252
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
253
+
254
+ ### Triggering Events
255
+
256
+ 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.
257
+
258
+ ```python
259
+
260
+ class Foo:
261
+ # init definition as above
262
+
263
+ @property
264
+ def enabled(self) -> bool:
265
+ return self._enabled
266
+
267
+ @enabled.setter
268
+ def enabled(self, enabled: bool) ->:
269
+ self._enabled = enabled
270
+ if enabled:
271
+ self.OnEnable.trigger(self)
272
+ ```
273
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
274
+
275
+
276
+ ### Configuration
277
+
278
+ 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.
279
+
280
+ #### Synchronous vs Asynchronous
281
+
282
+ 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:
283
+
284
+ - ThreadRunner: Uses the Python threading module to run callbacks in new threads, good for general purpose multithreading.
285
+ - AsyncioRunner: Uses Python's asyncio module, useful for when threading must be async-aware, such as in WASM deployments.
286
+
287
+ ```python
288
+ import hair_trigger
289
+ from hair_trigger.runner import AsyncioRunner, ThreadRunner
290
+
291
+ # Run standard threads
292
+ hair_trigger.config(runner=ThreadRunner())
293
+
294
+
295
+ # Run async-aware
296
+ hair_trigger.config(runner=AsyncioRunner())
297
+ ```
298
+
299
+ #### Scheduling Modes
300
+
301
+ 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.
302
+
303
+ Included are:
304
+
305
+ - StackScheduler: New events are put onto a stack, so that the newest event resolve before olderone resolve.
306
+ - QueueScheduler: New events are put into a queue, so events resolve in the order they are triggered.
307
+
308
+ The deferred schedulers must be triggered manually, using `hair_trigger.scheduler.pump_events()`.
309
+
310
+ ```python
311
+ import hair_trigger
312
+ import hair_trigger.scheduler
313
+ from hair_trigger.scheduler import QueueScheduler
314
+
315
+ hair_trigger.config(scheduler=QueueScheduler())
316
+
317
+ # Do things to trigger events
318
+
319
+ hair_trigger.scheduler.pump_events()
320
+
321
+ ```
322
+
323
+ #### Custom Runners and Schedulers
324
+
325
+ Runners and schedulers are protocols, so custom one can be created to get specific behaviors.
326
+
327
+ For example:
328
+
329
+ ```python
330
+
331
+ class LoggingThreadRunner:
332
+
333
+ def schedule(self, func: Callable[..., Any], *args, **kwds) -> None:
334
+ print(f"Calling function {func}")
335
+ threading.Thread(target=func, args=args, kwargs=kwds).start()
336
+
337
+
338
+ hair_trigger.config(LoggingThreadRunner())
339
+ ```
340
+
341
+ This will log the function before starting the thread.
342
+
343
+
344
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
345
+
346
+ <!-- ROADMAP -->
347
+ <!-- ## Roadmap
348
+
349
+ - [ ] (Eternal) Improve the renderer. Faster rendering means more renderables! -->
350
+
351
+ <!--
352
+ - [ ] Feature 2
353
+ - [ ] Feature 3
354
+ - [ ] Nested Feature
355
+ -->
356
+
357
+ <!-- See the [open issues](https://github.com/BetterBuiltFool/hair_trigger/issues) for a full list of proposed features (and known issues). -->
358
+
359
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
360
+
361
+
362
+
363
+ <!-- CONTRIBUTING -->
364
+ <!--
365
+ ## Contributing
366
+
367
+ Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
368
+
369
+ If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
370
+ Don't forget to give the project a star! Thanks again!
371
+
372
+ 1. Fork the Project
373
+ 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
374
+ 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
375
+ 4. Push to the Branch (`git push origin feature/AmazingFeature`)
376
+ 5. Open a Pull Request
377
+
378
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
379
+
380
+ ### Top contributors:
381
+
382
+ <a href="https://github.com/BetterBuiltFool/hair_trigger/graphs/contributors">
383
+ <img src="https://contrib.rocks/image?repo=BetterBuiltFool/hair_trigger" alt="contrib.rocks image" />
384
+ </a>
385
+ -->
386
+
387
+
388
+
389
+ <!-- LICENSE -->
390
+ ## License
391
+
392
+ Distributed under the MIT License. See `LICENSE.txt` for more information.
393
+
394
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
395
+
396
+
397
+
398
+ <!-- CONTACT -->
399
+ ## Contact
400
+
401
+ Better Built Fool - betterbuiltfool@gmail.com
402
+
403
+ Bluesky - [@betterbuiltfool.bsky.social](https://bsky.app/profile/betterbuiltfool.bsky.social)
404
+ <!--
405
+ - [@twitter_handle](https://twitter.com/twitter_handle)
406
+ -->
407
+
408
+ Project Link: [https://github.com/BetterBuiltFool/hair_trigger](https://github.com/BetterBuiltFool/hair_trigger)
409
+
410
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
411
+
412
+
413
+
414
+ <!-- ACKNOWLEDGMENTS -->
415
+ <!--## Acknowledgments
416
+
417
+ * []()
418
+ * []()
419
+ * []()
420
+
421
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
422
+ -->
423
+
424
+
425
+ <!-- MARKDOWN LINKS & IMAGES -->
426
+ <!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
427
+ [contributors-shield]: https://img.shields.io/github/contributors/BetterBuiltFool/hair_trigger.svg?style=for-the-badge
428
+ [contributors-url]: https://github.com/BetterBuiltFool/hair_trigger/graphs/contributors
429
+ [forks-shield]: https://img.shields.io/github/forks/BetterBuiltFool/hair_trigger.svg?style=for-the-badge
430
+ [forks-url]: https://github.com/BetterBuiltFool/hair_trigger/network/members
431
+ [stars-shield]: https://img.shields.io/github/stars/BetterBuiltFool/hair_trigger.svg?style=for-the-badge
432
+ [stars-url]: https://github.com/BetterBuiltFool/hair_trigger/stargazers
433
+ [issues-shield]: https://img.shields.io/github/issues/BetterBuiltFool/hair_trigger.svg?style=for-the-badge
434
+ [issues-url]: https://github.com/BetterBuiltFool/hair_trigger/issues
435
+ [license-shield]: https://img.shields.io/github/license/BetterBuiltFool/hair_trigger.svg?style=for-the-badge
436
+ [license-url]: https://github.com/BetterBuiltFool/hair_trigger/blob/main/LICENSE
437
+ [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555
438
+ [linkedin-url]: https://linkedin.com/in/linkedin_username
439
+ [product-screenshot]: images/screenshot.png
440
+ [Next.js]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white
441
+ [Next-url]: https://nextjs.org/
442
+ [python.org]: https://img.shields.io/badge/python-3670A0?style=for-the-badge&logo=python&logoColor=ffdd54
443
+ [python-url]: https://www.python.org/
444
+ [React.js]: https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB
445
+ [React-url]: https://reactjs.org/
446
+ [Vue.js]: https://img.shields.io/badge/Vue.js-35495E?style=for-the-badge&logo=vuedotjs&logoColor=4FC08D
447
+ [Vue-url]: https://vuejs.org/
448
+ [Angular.io]: https://img.shields.io/badge/Angular-DD0031?style=for-the-badge&logo=angular&logoColor=white
449
+ [Angular-url]: https://angular.io/
450
+ [Svelte.dev]: https://img.shields.io/badge/Svelte-4A4A55?style=for-the-badge&logo=svelte&logoColor=FF3E00
451
+ [Svelte-url]: https://svelte.dev/
452
+ [Laravel.com]: https://img.shields.io/badge/Laravel-FF2D20?style=for-the-badge&logo=laravel&logoColor=white
453
+ [Laravel-url]: https://laravel.com
454
+ [Bootstrap.com]: https://img.shields.io/badge/Bootstrap-563D7C?style=for-the-badge&logo=bootstrap&logoColor=white
455
+ [Bootstrap-url]: https://getbootstrap.com
456
+ [JQuery.com]: https://img.shields.io/badge/jQuery-0769AD?style=for-the-badge&logo=jquery&logoColor=white
457
+ [JQuery-url]: https://jquery.com
458
+ [pypi-url]: https://pypi.org/project/hair_trigger
459
+ [pip-url]: https://pip.pypa.io/en/stable/