""" Implementation for all giambio traps, which are hooks into the event loop and allow it to switch tasks. These coroutines are the one and only way to interact with the event loop from the user's perspective, and the entire library is based on them. These low-level primitives should not be used on their own unless you know what you're doing: the library already abstracts them away with more complex object wrappers and functions Copyright (C) 2020 nocturn9x Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, 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 types import inspect from giambio.task import Task from typing import Any, Union, Iterable, Coroutine, Callable from giambio.exceptions import GiambioError @types.coroutine def create_trap(method, *args): """ Creates and yields a trap. This is the lowest-level method to interact with the event loop """ return (yield method, *args) async def suspend() -> Any: """ Suspends the current task. The task can still be woken up by any pending timers, I/O or cancellations """ return await create_trap("suspend") async def create_task(coro: Callable[[Any, Any], Coroutine[Any, Any, Any]], pool, *args, **kwargs): """ Spawns a new task in the current event loop from a bare coroutine function. All extra positional arguments are passed to the function This trap should *NOT* be used on its own, it is meant to be called from internal giambio machinery """ if inspect.iscoroutine(coro): raise GiambioError( "Looks like you tried to call pool.create_task(your_func(arg1, arg2, ...)), that is wrong!" "\nWhat you wanna do, instead, is this: pool.create_task(your_func, arg1, arg2, ...)" ) elif inspect.iscoroutinefunction(coro): return await create_trap("create_task", coro, pool, *args, **kwargs) else: raise TypeError("coro must be a coroutine function") async def sleep(seconds: Union[int, float]): """ Pause the execution of an async function for a given amount of seconds. This function is functionally equivalent to time.sleep, but can be used within async code without blocking everything else. This function is also useful as a sort of checkpoint, because it returns control to the scheduler, which can then switch to another task. If your code doesn't have enough calls to async functions (or 'checkpoints') this might prevent the scheduler from switching tasks properly. If you feel like this happens in your code, try adding a call to await giambio.sleep(0) somewhere. This will act as a checkpoint without actually pausing the execution of your function, but it will allow the scheduler to switch tasks :param seconds: The amount of seconds to sleep for :type seconds: int """ assert seconds >= 0, "The time delay can't be negative" await create_trap("sleep", seconds) async def io_release(resource): """ Notifies the event loop to release the resources associated to the given socket and stop listening on it """ await create_trap("io_release", resource) async def current_task(): """ Gets the currently running task in an asynchronous fashion """ return await create_trap("get_current_task") async def current_loop(): """ Gets the currently running loop in an asynchronous fashion """ return await create_trap("get_current_loop") async def current_pool(): """ Gets the currently active task pool in an asynchronous fashion """ return await create_trap("get_current_pool") async def join(task): """ Awaits a given task for completion :param task: The task to join :type task: :class: Task """ return await create_trap("join", task) async def cancel(task): """ Cancels the given task. The concept of cancellation is tricky, because there is no real way to 'stop' a task if not by raising an exception inside it and ignoring whatever it returns (and also hoping that the task won't cause collateral damage). It is highly recommended that when you write async code you take into account that it might be cancelled at any time. You might think to just ignore the cancellation exception and be done with it, but doing so *will* break your code, so if you really wanna do that be sure to re-raise it when done! """ await create_trap("cancel", task) assert task.done(), f"Task ignored CancelledError" async def want_read(stream): """ Notifies the event loop that a task wants to read from the given resource :param stream: The resource that needs to be read """ await create_trap("register_sock", stream, "read") async def want_write(stream): """ Notifies the event loop that a task wants to write on the given resource :param stream: The resource that needs to be written """ await create_trap("register_sock", stream, "write") async def notify_closing(stream): """ Notifies the event loop that a given stream needs to be closed. This makes all callers waiting on want_read or want_write crash with a ResourceClosed exception, but it doesn't actually close the socket object itself """ await create_trap("notify_closing", stream) async def schedule_tasks(tasks: Iterable[Task]): """ Schedules a list of tasks for execution. Usuaully used to unsuspend them after they called suspend() """ await create_trap("schedule_tasks", tasks) async def event_wait(event): """ Notifies the event loop that the current task has to wait for the given event to trigger. This trap returns immediately if the event has already been set """ if event.set: return task = await current_task() event.waiters.add(task) return await suspend() async def event_set(event, value): """ Sets the given event and reawakens its waiters """ event.set = True event.value = value loop = await current_loop() for waiter in event.waiters: loop._data[waiter] = event.value await schedule_tasks(event.waiters)