diff --git a/python-ecosys/pymitter/LICENSE b/python-ecosys/pymitter/LICENSE new file mode 100644 index 000000000..f7f1222ba --- /dev/null +++ b/python-ecosys/pymitter/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2014-2021, Marcel Rieger +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/python-ecosys/pymitter/MANIFEST.in b/python-ecosys/pymitter/MANIFEST.in new file mode 100644 index 000000000..054117d01 --- /dev/null +++ b/python-ecosys/pymitter/MANIFEST.in @@ -0,0 +1,2 @@ +include pymitter.py setup.py requirements.txt README.md LICENSE .flake8 +global-exclude *.py[cod] __pycache__ diff --git a/python-ecosys/pymitter/README.md b/python-ecosys/pymitter/README.md new file mode 100644 index 000000000..279705d8b --- /dev/null +++ b/python-ecosys/pymitter/README.md @@ -0,0 +1,183 @@ +# pymitter + +This is a fork of the [original pymitter project](https://pypi.org/project/pymitter/) by Marcel Rieger. +Sources are from the legacy/py2 branch which is a frozen v0.3.2 of that project. +At this state, the implementation is compatible to Python >= v2.7 including +MicroPython with a language level v3.4. + +Later versions of that project make use of type hints, which were introduced +in Python 3.5. Type hints are currently not supported by MicroPython. + + +## Features + +- Namespaces with wildcards +- Times to listen (TTL) +- Usage via decorators or callbacks +- Lightweight implementation, good performance + + +## Installation + +*pymitter* is a registered [MicroPython module](https://github.com/olimaye/micropython-lib), +so the installation with *mip* is quite easy: + +```console +mpremote mip install pymitter +``` + + +## Examples + +### Basic usage + +```python +from pymitter import EventEmitter + + +ee = EventEmitter() + + +# decorator usage +@ee.on("myevent") +def handler1(arg): + print("handler1 called with", arg) + + +# callback usage +def handler2(arg): + print("handler2 called with", arg) + + +ee.on("myotherevent", handler2) + + +# emit +ee.emit("myevent", "foo") +# -> "handler1 called with foo" + +ee.emit("myotherevent", "bar") +# -> "handler2 called with bar" +``` + + +### TTL (times to listen) + +```python +from pymitter import EventEmitter + + +ee = EventEmitter() + + +@ee.once("myevent") +def handler1(): + print("handler1 called") + + +@ee.on("myevent", ttl=10) +def handler2(): + print("handler2 called") + + +ee.emit("myevent") +# -> "handler1 called" +# -> "handler2 called" + +ee.emit("myevent") +# -> "handler2 called" +``` + + +### Wildcards + +```python +from pymitter import EventEmitter + + +ee = EventEmitter(wildcard=True) + + +@ee.on("myevent.foo") +def handler1(): + print("handler1 called") + + +@ee.on("myevent.bar") +def handler2(): + print("handler2 called") + + +@ee.on("myevent.*") +def hander3(): + print("handler3 called") + + +ee.emit("myevent.foo") +# -> "handler1 called" +# -> "handler3 called" + +ee.emit("myevent.bar") +# -> "handler2 called" +# -> "handler3 called" + +ee.emit("myevent.*") +# -> "handler1 called" +# -> "handler2 called" +# -> "handler3 called" +``` + +## API + + +### ``EventEmitter(wildcard=False, delimiter=".", new_listener=False, max_listeners=-1)`` + +EventEmitter constructor. **Note**: always use *kwargs* for configuration. When *wildcard* is +*True*, wildcards are used as shown in [this example](#wildcards). *delimiter* is used to seperate +namespaces within events. If *new_listener* is *True*, the *"new_listener"* event is emitted every +time a new listener is registered. Functions listening to this event are passed +``(func, event=None)``. *max_listeners* defines the maximum number of listeners per event. Negative +values mean infinity. + +- #### ``on(event, func=None, ttl=-1)`` + Registers a function to an event. When *func* is *None*, decorator usage is assumed. *ttl* + defines the times to listen. Negative values mean infinity. Returns the function. + +- #### ``once(event, func=None)`` + Registers a function to an event with ``ttl = 1``. When *func* is *None*, decorator usage is + assumed. Returns the function. + +- #### ``on_any(func=None)`` + Registers a function that is called every time an event is emitted. When *func* is *None*, + decorator usage is assumed. Returns the function. + +- #### ``off(event, func=None)`` + Removes a function that is registered to an event. When *func* is *None*, decorator usage is + assumed. Returns the function. + +- #### ``off_any(func=None)`` + Removes a function that was registered via ``on_any()``. When *func* is *None*, decorator usage + is assumed. Returns the function. + +- #### ``off_all()`` + Removes all functions of all events. + +- #### ``listeners(event)`` + Returns all functions that are registered to an event. Wildcards are not applied. + +- #### ``listeners_any()`` + Returns all functions that were registered using ``on_any()``. + +- #### ``listeners_all()`` + Returns all registered functions. + +- #### ``emit(event, *args, **kwargs)`` + Emits an event. All functions of events that match *event* are invoked with *args* and *kwargs* + in the exact order of their registeration. Wildcards might be applied. + + +## Development + +- Source hosted at [GitHub](https://github.com/riga/pymitter) +- Python module hostet at [PyPI](https://pypi.python.org/pypi/pymitter) +- Report issues, questions, feature requests on [GitHub Issues](https://github.com/riga/pymitter/issues) diff --git a/python-ecosys/pymitter/examples.py b/python-ecosys/pymitter/examples.py new file mode 100644 index 000000000..810adfc1f --- /dev/null +++ b/python-ecosys/pymitter/examples.py @@ -0,0 +1,53 @@ +# coding: utf-8 + +# python imports +import os +import sys +from pymitter import EventEmitter + + +# create an EventEmitter instance +ee = EventEmitter(wildcard=True, new_listener=True, max_listeners=-1) + + +@ee.on("new_listener") +def on_new(func, event=None): + print("added listener", event, func) + + +@ee.on("foo") +def handler_foo1(arg): + print("foo handler 1 called with", arg) + + +@ee.on("foo") +def handler_foo2(arg): + print("foo handler 2 called with", arg) + + +@ee.on("foo.*", ttl=1) +def handler_fooall(arg): + print("foo.* handler called with", arg) + + +@ee.on("foo.bar") +def handler_foobar(arg): + print("foo.bar handler called with", arg) + + +@ee.on_any() +def handler_any(*args, **kwargs): + print("called every time") + + +print("emit foo") +ee.emit("foo", "test") +print(10 * "-") + +print("emit foo.bar") +ee.emit("foo.bar", "test") +print(10 * "-") + +print("emit foo.*") +ee.emit("foo.*", "test") +print(10 * "-") diff --git a/python-ecosys/pymitter/manifest.py b/python-ecosys/pymitter/manifest.py new file mode 100644 index 000000000..fc7f39e54 --- /dev/null +++ b/python-ecosys/pymitter/manifest.py @@ -0,0 +1,7 @@ +metadata( + description="Event subscription and publishing tools.", + version="0.3.2", + pypi="pymitter", +) + +module("pymitter.py") diff --git a/python-ecosys/pymitter/pymitter.py b/python-ecosys/pymitter/pymitter.py new file mode 100644 index 000000000..2f057ee15 --- /dev/null +++ b/python-ecosys/pymitter/pymitter.py @@ -0,0 +1,284 @@ +# coding: utf-8 + +""" +Python port of the extended Node.js EventEmitter 2 approach providing namespaces, wildcards and TTL. +""" + +__author__ = "Marcel Rieger" +__author_email__ = "github.riga@icloud.com" +__copyright__ = "Copyright 2014-2022, Marcel Rieger" +__credits__ = ["Marcel Rieger"] +__contact__ = "https://github.com/riga/pymitter" +__license__ = "BSD-3-Clause" +__status__ = "Development" +__version__ = "0.3.2" +__all__ = ["EventEmitter", "Listener"] + + +import time + + +class EventEmitter(object): + """ + The EventEmitter class, ported from Node.js EventEmitter 2. + + When *wildcard* is *True*, wildcards in event names are taken into account. When *new_listener* + is *True*, a ``"new_listener"`` event is emitted every time a new listener is registered with + arguments ``(func, event=None)``. *max_listeners* configures the maximum number of event + listeners. A negative numbers means that this number is unlimited. Event names have namespace + support with each namspace being separated by a *delimiter* which defaults to ``"."``. + """ + + CB_KEY = "__callbacks" + WC_CHAR = "*" + + def __init__(self, wildcard=False, new_listener=False, max_listeners=-1, delimiter="."): + super(EventEmitter, self).__init__() + + self.wildcard = wildcard + self.delimiter = delimiter + self.new_listener = new_listener + self.max_listeners = max_listeners + + self._tree = self._new_branch() + + @classmethod + def _new_branch(cls): + """ + Returns a new branch. Essentially, a branch is just a dictionary with a special item + *CB_KEY* that holds registered functions. All other items are used to build a tree + structure. + """ + return {cls.CB_KEY: []} + + def _find_branch(self, event): + """ + Returns a branch of the tree structure that matches *event*. Wildcards are not applied. + """ + parts = event.split(self.delimiter) + + if self.CB_KEY in parts: + return None + + branch = self._tree + for p in parts: + if p not in branch: + return None + branch = branch[p] + + return branch + + @classmethod + def _remove_listener(cls, branch, func): + """ + Removes a listener given by its function *func* from a *branch*. + """ + listeners = branch[cls.CB_KEY] + + indexes = [i for i, l in enumerate(listeners) if l.func == func] + + for i in indexes[::-1]: + listeners.pop(i) + + def on(self, event, func=None, ttl=-1): + """ + Registers a function to an event. *ttl* defines the times to listen. Negative values mean + infinity. When *func* is *None*, decorator usage is assumed. Returns the function. + """ + + def on(func): + if not callable(func): + return func + + parts = event.split(self.delimiter) + if self.CB_KEY in parts: + return func + + branch = self._tree + for p in parts: + branch = branch.setdefault(p, self._new_branch()) + + listeners = branch[self.CB_KEY] + if 0 <= self.max_listeners <= len(listeners): + return func + + listener = Listener(func, event, ttl) + listeners.append(listener) + + if self.new_listener: + self.emit("new_listener", func, event) + + return func + + return on(func) if func else on + + def once(self, event, func=None): + """ + Registers a function to an event that is called once. When *func* is *None*, decorator usage + is assumed. Returns the function. + """ + return self.on(event, func=func, ttl=1) + + def on_any(self, func=None, ttl=-1): + """ + Registers a function that is called every time an event is emitted. *ttl* defines the times + to listen. Negative values mean infinity. When *func* is *None*, decorator usage is assumed. + Returns the function. + """ + + def on_any(func): + if not callable(func): + return func + + listeners = self._tree[self.CB_KEY] + if 0 <= self.max_listeners <= len(listeners): + return func + + listener = Listener(func, None, ttl) + listeners.append(listener) + + if self.new_listener: + self.emit("new_listener", func) + + return func + + return on_any(func) if func else on_any + + def off(self, event, func=None): + """ + Removes a function that is registered to an event. When *func* is *None*, decorator usage is + assumed. Returns the function. + """ + + def off(func): + branch = self._find_branch(event) + if branch is None: + return func + + self._remove_listener(branch, func) + + return func + + return off(func) if func else off + + def off_any(self, func=None): + """ + Removes a function that was registered via :py:meth:`on_any`. When *func* is *None*, + decorator usage is assumed. Returns the function. + """ + + def off_any(func): + self._remove_listener(self._tree, func) + + return func + + return off_any(func) if func else off_any + + def off_all(self): + """ + Removes all registered functions. + """ + self._tree = self._new_branch() + + def listeners(self, event): + """ + Returns all functions that are registered to an event. Wildcards are not applied. + """ + branch = self._find_branch(event) + if branch is None: + return [] + + return [listener.func for listener in branch[self.CB_KEY]] + + def listeners_any(self): + """ + Returns all functions that were registered using :py:meth:`on_any`. + """ + return [listener.func for listener in self._tree[self.CB_KEY]] + + def listeners_all(self): + """ + Returns all registered functions. + """ + listeners = list(self._tree[self.CB_KEY]) + + branches = list(self._tree.values()) + for b in branches: + if not isinstance(b, dict): + continue + + branches.extend(b.values()) + + listeners.extend(b[self.CB_KEY]) + + return [listener.func for listener in listeners] + + def emit(self, event, *args, **kwargs): + """ + Emits an *event*. All functions of events that match *event* are invoked with *args* and + *kwargs* in the exact order of their registration. Wildcards might be applied. + """ + parts = event.split(self.delimiter) + + if self.CB_KEY in parts: + return + + listeners = list(self._tree[self.CB_KEY]) + branches = [self._tree] + + for p in parts: + _branches = [] + for branch in branches: + for k, b in branch.items(): + if k == self.CB_KEY: + continue + if k == p: + _branches.append(b) + elif self.wildcard and self.WC_CHAR in (p, k): + _branches.append(b) + branches = _branches + + for b in branches: + listeners.extend(b[self.CB_KEY]) + + # sort listeners by registration time + listeners = sorted(listeners, key=lambda listener: listener.time) + + # call listeners in the order of their registration time + for listener in sorted(listeners, key=lambda listener: listener.time): + listener(*args, **kwargs) + + # remove listeners whose ttl value is 0 + for listener in listeners: + if listener.ttl == 0: + self.off(listener.event, func=listener.func) + + +class Listener(object): + """ + A simple event listener class that wraps a function *func* for a specific *event* and that keeps + track of the times to listen left. + """ + + def __init__(self, func, event, ttl): + super(Listener, self).__init__() + + self.func = func + self.event = event + self.ttl = ttl + + # store the registration time + self.time = time.time() + + def __call__(self, *args, **kwargs): + """ + Invokes the wrapped function when ttl is non-zero, decreases the ttl value when positive and + returns whether it reached zero or not. + """ + if self.ttl != 0: + self.func(*args, **kwargs) + + if self.ttl > 0: + self.ttl -= 1 + + return self.ttl == 0 diff --git a/python-ecosys/pymitter/requirements.txt b/python-ecosys/pymitter/requirements.txt new file mode 100644 index 000000000..e69de29bb diff --git a/python-ecosys/pymitter/requirements_test.txt b/python-ecosys/pymitter/requirements_test.txt new file mode 100644 index 000000000..169d64c77 --- /dev/null +++ b/python-ecosys/pymitter/requirements_test.txt @@ -0,0 +1,5 @@ +flake8<4;python_version<="2.7" +flake8>=4.0.1;python_version>="3.0" +flake8-commas>=2 +flake8-quotes>=3,<3.3;python_version<="2.7" +flake8-quotes>=3;python_version>="3.0" diff --git a/python-ecosys/pymitter/setup.py b/python-ecosys/pymitter/setup.py new file mode 100644 index 000000000..1ca235ffa --- /dev/null +++ b/python-ecosys/pymitter/setup.py @@ -0,0 +1,62 @@ +# coding: utf-8 + + +import os +from setuptools import setup + +import pymitter + + +this_dir = os.path.dirname(os.path.abspath(__file__)) + +keywords = [ + "event", + "emitter", + "eventemitter", + "wildcard", + "node", + "nodejs", +] + +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Development Status :: 5 - Production/Stable", + "Operating System :: OS Independent", + "License :: OSI Approved :: BSD License", + "Intended Audience :: Developers", +] + +# read the readme file +with open(os.path.join(this_dir, "README.md"), "r") as f: + long_description = f.read() + +# load installation requirements +with open(os.path.join(this_dir, "requirements.txt"), "r") as f: + install_requires = [line.strip() for line in f.readlines() if line.strip()] + +setup( + name=pymitter.__name__, + version=pymitter.__version__, + author=pymitter.__author__, + author_email=pymitter.__author_email__, + description=pymitter.__doc__.strip().split("\n")[0].strip(), + license=pymitter.__license__, + url=pymitter.__contact__, + keywords=keywords, + classifiers=classifiers, + long_description=long_description, + long_description_content_type="text/markdown", + install_requires=install_requires, + python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4", + zip_safe=False, + py_modules=[pymitter.__name__], +) diff --git a/python-ecosys/pymitter/tests.py b/python-ecosys/pymitter/tests.py new file mode 100644 index 000000000..9429eefb9 --- /dev/null +++ b/python-ecosys/pymitter/tests.py @@ -0,0 +1,126 @@ +# coding: utf-8 + + +import unittest + +from pymitter import EventEmitter + + +class AllTestCase(unittest.TestCase): + def __init__(self, *args, **kwargs): + super(AllTestCase, self).__init__(*args, **kwargs) + + self.ee1 = EventEmitter() + self.ee2 = EventEmitter(wildcard=True) + self.ee3 = EventEmitter(wildcard=True, delimiter=":") + self.ee4 = EventEmitter(new_listener=True) + self.ee5 = EventEmitter(max_listeners=1) + + def test_1_callback_usage(self): + stack = [] + + def handler(arg): + stack.append("1_callback_usage_" + arg) + + self.ee1.on("1_callback_usage", handler) + + self.ee1.emit("1_callback_usage", "foo") + self.assertTrue(stack[-1] == "1_callback_usage_foo") + + def test_1_decorator_usage(self): + stack = [] + + @self.ee1.on("1_decorator_usage") + def handler(arg): + stack.append("1_decorator_usage_" + arg) + + self.ee1.emit("1_decorator_usage", "bar") + self.assertTrue(stack[-1] == "1_decorator_usage_bar") + + def test_1_ttl_on(self): + stack = [] + + @self.ee1.on("1_ttl_on", ttl=1) + def handler(arg): + stack.append("1_ttl_on_" + arg) + + self.ee1.emit("1_ttl_on", "foo") + self.assertTrue(stack[-1] == "1_ttl_on_foo") + + self.ee1.emit("1_ttl_on", "bar") + self.assertTrue(stack[-1] == "1_ttl_on_foo") + + def test_1_ttl_once(self): + stack = [] + + @self.ee1.once("1_ttl_once") + def handler(arg): + stack.append("1_ttl_once_" + arg) + + self.ee1.emit("1_ttl_once", "foo") + self.assertTrue(stack[-1] == "1_ttl_once_foo") + + self.ee1.emit("1_ttl_once", "bar") + self.assertTrue(stack[-1] == "1_ttl_once_foo") + + def test_2_on_all(self): + stack = [] + + @self.ee2.on("2_on_all.*") + def handler(): + stack.append("2_on_all") + + self.ee2.emit("2_on_all.foo") + self.assertTrue(stack[-1] == "2_on_all") + + def test_2_emit_all(self): + stack = [] + + @self.ee2.on("2_emit_all.foo") + def handler(): + stack.append("2_emit_all.foo") + + self.ee2.emit("2_emit_all.*") + self.assertTrue(stack[-1] == "2_emit_all.foo") + + def test_3_delimiter(self): + stack = [] + + @self.ee3.on("3_delimiter:*") + def handler(): + stack.append("3_delimiter") + + self.ee3.emit("3_delimiter:foo") + self.assertTrue(stack[-1] == "3_delimiter") + + def test_4_new(self): + stack = [] + + @self.ee4.on("new_listener") + def handler(func, event=None): + stack.append((func, event)) + + def newhandler(): + pass + + self.ee4.on("4_new", newhandler) + + self.assertTrue(stack[-1] == (newhandler, "4_new")) + + def test_5_max(self): + stack = [] + + @self.ee5.on("5_max") + def handler1(): + stack.append("5_max_1") + + @self.ee5.on("5_max") + def handler2(): + stack.append("5_max_2") + + self.ee5.emit("5_max") + self.assertTrue(stack[-1] == "5_max_1") + + +if __name__ == "__main__": + unittest.main()