Made the behavior of oneshot handlers uniform across execution modes. Added more tests and notes to README

This commit is contained in:
nocturn9x 2021-12-26 15:52:47 +01:00
parent 5ba185c11b
commit 91bca1cab0
5 changed files with 61 additions and 23 deletions

View File

@ -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
- 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
- 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
all event handlers have run. If the non-blocking mode is used, handlers are started according to their priority,
but there's no telling on how they will be further scheduled to run and that depends entirely on the underlying
asyncio event loop
## Must Read
- Deterministic event handling can only occur in blocking mode, i.e. when a call to `emit()` blocks until
all event handlers have run. If the non-blocking mode is used, handlers are started according to their priority,
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

View File

@ -8,6 +8,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import functools
import threading
from asyncevents import events, errors
from typing import Any, Callable, Coroutine, Optional
@ -19,7 +20,7 @@ local_storage: threading.local = threading.local()
AsyncEventEmitter = events.AsyncEventEmitter
def get_current_emitter():
def get_current_emitter() -> AsyncEventEmitter:
"""
Returns the currently active
emitter in the current thread
@ -60,7 +61,7 @@ async def wait(event: Optional[str] = None):
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)
@ -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):
"""
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)
@functools.wraps
async def wrapper(*args, **kwargs):
return await corofunc(*args, **kwargs)
return wrapper
return decorator

View File

@ -108,23 +108,17 @@ class AsyncEventEmitter:
# and runs the handlers in the background
await self._check_event(event)
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]:
# We use heappop because we want the first
# by priority and the heap queue only has
# the guarantee we need for heap[0]
temp.append(heappop(self.handlers[event]))
task = asyncio.create_task(temp[-1][-2](self, event))
if temp[-1][-1]:
task.add_done_callback(
partial(
# The extra argument is the future asyncio passes us,
# which we don't care about
lambda s, ev, corofunc, _: s.unregister_handler(ev, corofunc),
self,
event,
temp[-1][-2],
)
)
t = temp[-1]
if t[-1]:
# It won't be re-scheduled
temp.pop()
task = asyncio.create_task(t[-2](self, event))
self._tasks.append((event, asyncio.create_task(self._handle_errors_in_awaitable(event, task))))
# We push back the elements
for t in temp:

View File

@ -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())

View File

@ -1,5 +1,5 @@
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
@ -7,11 +7,21 @@ async def hello(_, event: str):
print(f"Hello {event!r}!")
@on_event("hi", oneshot=True)
async def hi(_, event: str):
print(f"Hi {event!r}!")
async def main():
print("Firing blocking event 'hello'")
await emit("hello")
print("Handlers for event 'hello' have exited")
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__":