Made the behavior of oneshot handlers uniform across execution modes. Added more tests and notes to README
This commit is contained in:
parent
5ba185c11b
commit
91bca1cab0
20
README.md
20
README.md
|
@ -9,12 +9,24 @@ asyncevents is a small library to help developers perform asynchronous event han
|
||||||
- The public API is fully type hinted for those sweet, sweet editor suggestions
|
- The public API is fully type hinted for those sweet, sweet editor suggestions
|
||||||
- Public API is fully documented (and some private stuff too): you could write this from scratch in a couple of hours (like I did)
|
- Public API is fully documented (and some private stuff too): you could write this from scratch in a couple of hours (like I did)
|
||||||
- Very small (~200 CLOC), although it can't fit on a postcard
|
- Very small (~200 CLOC), although it can't fit on a postcard
|
||||||
|
- Oneshot events (i.e. fired only once)
|
||||||
|
|
||||||
|
|
||||||
__Note__: Deterministic event handling can only occur in blocking mode, i.e. when a call to `emit()` blocks until
|
## Must Read
|
||||||
all event handlers have run. If the non-blocking mode is used, handlers are started according to their priority,
|
- Deterministic event handling can only occur in blocking mode, i.e. when a call to `emit()` blocks until
|
||||||
but there's no telling on how they will be further scheduled to run and that depends entirely on the underlying
|
all event handlers have run. If the non-blocking mode is used, handlers are started according to their priority,
|
||||||
asyncio event loop
|
but there's no telling on how they will be further scheduled to run and that depends entirely on the underlying
|
||||||
|
asyncio event loop
|
||||||
|
- When using `oneshot=True`, the handler is unscheduled _before_ it is first run: this ensures a consistent behavior
|
||||||
|
between blocking and non-blocking mode.
|
||||||
|
- An exception in one event handler does not cause the others to be cancelled when using non-blocking mode. In blocking
|
||||||
|
mode, if an error occurs in one handler, and it propagates, then the handlers after it are not started
|
||||||
|
- Exceptions in custom exception handlers are not caught
|
||||||
|
- When using non-blocking mode, exceptions are kind of a mess due to how asyncio works: They are only delivered once
|
||||||
|
you `wait()` for an event (same for log messages). This allows spawning many events and only having to worry about
|
||||||
|
exceptions in a single point in your code, but it also means you have less control over how the handlers run: if an
|
||||||
|
error occurs in one handler, it will be raised once you call `wait()` but any other error in other handlers will
|
||||||
|
be silently dropped
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
import functools
|
||||||
import threading
|
import threading
|
||||||
from asyncevents import events, errors
|
from asyncevents import events, errors
|
||||||
from typing import Any, Callable, Coroutine, Optional
|
from typing import Any, Callable, Coroutine, Optional
|
||||||
|
@ -19,7 +20,7 @@ local_storage: threading.local = threading.local()
|
||||||
AsyncEventEmitter = events.AsyncEventEmitter
|
AsyncEventEmitter = events.AsyncEventEmitter
|
||||||
|
|
||||||
|
|
||||||
def get_current_emitter():
|
def get_current_emitter() -> AsyncEventEmitter:
|
||||||
"""
|
"""
|
||||||
Returns the currently active
|
Returns the currently active
|
||||||
emitter in the current thread
|
emitter in the current thread
|
||||||
|
@ -60,7 +61,7 @@ async def wait(event: Optional[str] = None):
|
||||||
|
|
||||||
async def emit(event: str, block: bool = True):
|
async def emit(event: str, block: bool = True):
|
||||||
"""
|
"""
|
||||||
Shorthand for get_current_emitter().emit(event)
|
Shorthand for get_current_emitter().emit(event, block)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
await get_current_emitter().emit(event, block)
|
await get_current_emitter().emit(event, block)
|
||||||
|
@ -68,17 +69,16 @@ async def emit(event: str, block: bool = True):
|
||||||
|
|
||||||
def on_event(event: str, priority: int = 0, emitter: AsyncEventEmitter = get_current_emitter(), oneshot: bool = False):
|
def on_event(event: str, priority: int = 0, emitter: AsyncEventEmitter = get_current_emitter(), oneshot: bool = False):
|
||||||
"""
|
"""
|
||||||
Decorator shorthand of emitter.register_event(event, f, priority)
|
Decorator shorthand of emitter.register_event(event, f, priority, oneshot)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(corofunc: Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]]):
|
def decorator(corofunc: Callable[[AsyncEventEmitter, str], Coroutine[Any, Any, Any]]):
|
||||||
emitter.register_event(event, corofunc, priority, oneshot)
|
emitter.register_event(event, corofunc, priority, oneshot)
|
||||||
|
|
||||||
|
@functools.wraps
|
||||||
async def wrapper(*args, **kwargs):
|
async def wrapper(*args, **kwargs):
|
||||||
return await corofunc(*args, **kwargs)
|
return await corofunc(*args, **kwargs)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -108,23 +108,17 @@ class AsyncEventEmitter:
|
||||||
# and runs the handlers in the background
|
# and runs the handlers in the background
|
||||||
await self._check_event(event)
|
await self._check_event(event)
|
||||||
temp: List[Tuple[int, float, Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]], bool]] = []
|
temp: List[Tuple[int, float, Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]], bool]] = []
|
||||||
|
t: Tuple[int, float, Callable[["AsyncEventEmitter", str], Coroutine[Any, Any, Any]], bool]
|
||||||
while self.handlers[event]:
|
while self.handlers[event]:
|
||||||
# We use heappop because we want the first
|
# We use heappop because we want the first
|
||||||
# by priority and the heap queue only has
|
# by priority and the heap queue only has
|
||||||
# the guarantee we need for heap[0]
|
# the guarantee we need for heap[0]
|
||||||
temp.append(heappop(self.handlers[event]))
|
temp.append(heappop(self.handlers[event]))
|
||||||
task = asyncio.create_task(temp[-1][-2](self, event))
|
t = temp[-1]
|
||||||
if temp[-1][-1]:
|
if t[-1]:
|
||||||
task.add_done_callback(
|
# It won't be re-scheduled
|
||||||
partial(
|
temp.pop()
|
||||||
# The extra argument is the future asyncio passes us,
|
task = asyncio.create_task(t[-2](self, event))
|
||||||
# which we don't care about
|
|
||||||
lambda s, ev, corofunc, _: s.unregister_handler(ev, corofunc),
|
|
||||||
self,
|
|
||||||
event,
|
|
||||||
temp[-1][-2],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self._tasks.append((event, asyncio.create_task(self._handle_errors_in_awaitable(event, task))))
|
self._tasks.append((event, asyncio.create_task(self._handle_errors_in_awaitable(event, task))))
|
||||||
# We push back the elements
|
# We push back the elements
|
||||||
for t in temp:
|
for t in temp:
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import asyncio
|
||||||
|
from asyncevents import on_event, emit
|
||||||
|
|
||||||
|
|
||||||
|
@on_event("test")
|
||||||
|
async def hello(_, event: str):
|
||||||
|
print(f"Hello {event!r}!")
|
||||||
|
|
||||||
|
|
||||||
|
@on_event("test")
|
||||||
|
async def hi(_, event: str):
|
||||||
|
print(f"Hi {event!r}!")
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
print("Firing blocking event 'test'")
|
||||||
|
await emit("test")
|
||||||
|
print("Handlers for event 'test' have exited")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
|
@ -1,5 +1,5 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
from asyncevents import on_event, emit
|
from asyncevents import on_event, emit, wait
|
||||||
|
|
||||||
|
|
||||||
@on_event("hello", oneshot=True) # The handler is removed after it fires once
|
@on_event("hello", oneshot=True) # The handler is removed after it fires once
|
||||||
|
@ -7,11 +7,21 @@ async def hello(_, event: str):
|
||||||
print(f"Hello {event!r}!")
|
print(f"Hello {event!r}!")
|
||||||
|
|
||||||
|
|
||||||
|
@on_event("hi", oneshot=True)
|
||||||
|
async def hi(_, event: str):
|
||||||
|
print(f"Hi {event!r}!")
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
print("Firing blocking event 'hello'")
|
print("Firing blocking event 'hello'")
|
||||||
await emit("hello")
|
await emit("hello")
|
||||||
print("Handlers for event 'hello' have exited")
|
print("Handlers for event 'hello' have exited")
|
||||||
await emit("hello") # Nothing happens
|
await emit("hello") # Nothing happens
|
||||||
|
print("Firing non-blocking event 'hi'")
|
||||||
|
await emit("hi", block=False)
|
||||||
|
await wait("hi")
|
||||||
|
print("Handlers for event 'hi' have exited")
|
||||||
|
await emit("hi") # Nothing happens
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
Loading…
Reference in New Issue