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
|
||||
- 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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
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__":
|
||||
|
|
Loading…
Reference in New Issue