From d60a372af5480ee19e97399b99481a54dca1adfe Mon Sep 17 00:00:00 2001 From: nocturn9x Date: Wed, 18 Nov 2020 12:13:46 +0100 Subject: [PATCH] Updated README, added debugging utility and simplified example --- README.md | 263 ++++++++++++++++++++++++++++++--------- giambio/__init__.py | 5 +- giambio/context.py | 6 +- giambio/core.py | 66 ++++++---- giambio/objects.py | 1 + giambio/run.py | 28 +++-- giambio/util/__init__.py | 15 +++ giambio/util/debug.py | 197 +++++++++++++++++++++++++++++ pyvenv.cfg | 8 ++ tests/count.py | 42 ------- tests/server.py | 2 +- tests/sleep.py | 72 +++++++++++ 12 files changed, 567 insertions(+), 138 deletions(-) create mode 100644 giambio/util/__init__.py create mode 100644 giambio/util/debug.py create mode 100644 pyvenv.cfg delete mode 100644 tests/count.py create mode 100644 tests/sleep.py diff --git a/README.md b/README.md index 4ecbb08..de5568d 100644 --- a/README.md +++ b/README.md @@ -190,44 +190,34 @@ To demonstrate this, have a look a this example ```python import giambio -async def countdown(n: int): - print(f"Counting down from {n}!") - while n > 0: - print(f"Down {n}") - n -= 1 - await giambio.sleep(1) - print("Countdown over") - return 0 +async def child(): + print("[child] Child spawned! Sleeping for 2 seconds") + await giambio.sleep(2) + print("[child] Had a nice nap!") + +async def child1(): + print("[child 1] Child spawned! Sleeping for 2 seconds") + await giambio.sleep(2) + print("[child 1] Had a nice nap!") -async def countup(stop: int): - print(f"Counting up to {stop}!") - x = 0 - while x < stop: - print(f"Up {x}") - x += 1 - await giambio.sleep(2) - print("Countup over") - return 1 async def main(): start = giambio.clock() async with giambio.create_pool() as pool: - pool.spawn(countdown, 10) - pool.spawn(countup, 5) - print("Children spawned, awaiting completion") - print(f"Task execution complete in {giambio.clock() - start:2f} seconds") + pool.spawn(child) + pool.spawn(child1) + print("[main] Children spawned, awaiting completion") + print(f"[main] Children execution complete in {giambio.clock() - start:.2f} seconds") + if __name__ == "__main__": giambio.run(main) - ``` There is a lot going on here, and we'll explain every bit of it step by step: -- First, we imported giambio and defined two async functions: `countup` and `countdown` -- These two functions do exactly what their name suggests, but for the purposes of -this tutorial, `countup` will be running twice as slow as `countdown` (see the call -to `await giambio.sleep(2)`?) +- First, we imported giambio and defined two async functions: `child` and `child1` +- These two functions will just print something and then sleep for 2 seconds - Here comes the real fun: `async with`? What's going on there? As it turns out, Python 3.5 didn't just add async functions, but also quite a bit of related new syntax. One of the things that was added is asynchronous context managers. @@ -261,34 +251,18 @@ exceptions in giambio always behave as expected Ok, so, let's try running this snippet and see what we get: ``` -Children spawned, awaiting completion -Counting down from 10! -Down 10 -Counting up to 5! -Up 0 -Down 9 -Up 1 -Down 8 -Down 7 -Up 2 -Down 6 -Down 5 -Up 3 -Down 4 -Down 3 -Up 4 -Down 2 -Down 1 -Countup over -Countdown over -Task execution complete in 10.07 seconds +[child] Child spawned!! Sleeping for 2 seconds +[child 1] Child spawned!! Sleeping for 2 seconds +[child] Had a nice nap! +[child 1] Had a nice nap! +[main] Children execution complete in 2.01 seconds ``` (Your output might have some lines swapped compared to this) -You see how `countup` and `countdown` both start and finish -together? Moreover, even though each function slept for about 10 -seconds (therefore 20 seconds total), the program just took 10 +You see how `child` and `child1` both start and finish +together? Moreover, even though each function slept for about 2 +seconds (therefore 4 seconds total), the program just took 2 seconds to complete, so our children are really running at the same time. If you've ever done thread programming, this will feel like home, and that's good: @@ -358,20 +332,191 @@ then giambio will switch less frequently, hurting concurrency. It turns out that for that is calling `await giambio.sleep(0)`; This will implicitly let giambio kick in and do its job, and it will reschedule the caller almost immediately, because the sleep time is 0. -### Mix and match? No thanks -You may wonder whether you can mix async libraries: for instance, can we call `trio.sleep` in a -giambio application? The answer is no, we can't, and there's a reason for that. Giambio wraps all -your asynchronous code in its event loop, which is what actually runs the tasks. When you call -`await giambio.something()`, what you're doing is sending "commands" to the event loop asking it -to perform a certain thing in a given task, and to communicate your intent to the loop, the -primitives (such as `giambio.sleep`) talk a language that only giambio's event loop can understand. +### A closer look + +In the above section we explained the theory behind async functions, but now we'll inspect the magic behind +`giambio.run()` and its event loop to demistify _how_ giambio makes this whole async thing happen. Luckily for us, +giambio has some useful tooling that lets us sneak peak inside the machinery of the library to better help us +understand what's going on, located at `giambio.debug.BaseDebugger`. That's an abstract class that we can customize +for our purposes and that communicates with the event loop about everything it's going, so let's code it: + +```python +class Debugger(giambio.debug.BaseDebugger): + """ + A simple debugger for this test + """ + + def on_start(self): + print("## Started running") + + def on_exit(self): + print("## Finished running") + + def on_task_schedule(self, task, delay: int): + print(f">> A task named '{task.name}' was scheduled to run in {delay:.2f} seconds") + + def on_task_spawn(self, task): + print(f">> A task named '{task.name}' was spawned") + + def on_task_exit(self, task): + print(f"<< Task '{task.name}' exited") + + def before_task_step(self, task): + print(f"-> About to run a step for '{task.name}'") + + def after_task_step(self, task): + print(f"<- Ran a step for '{task.name}'") + + def before_sleep(self, task, seconds): + print(f"# About to put '{task.name}' to sleep for {seconds:.2f} seconds") + + def after_sleep(self, task, seconds): + print(f"# Task '{task.name}' slept for {seconds:.2f} seconds") + + def before_io(self, timeout): + print(f"!! About to check for I/O for up to {timeout:.2f} seconds") + + def after_io(self, timeout): + print(f"!! Done I/O check (timeout {timeout:.2f} seconds)") + + def before_cancel(self, task): + print(f"// About to cancel '{task.name}'") + + def after_cancel(self, task): + print(f"// Cancelled '{task.name}'") +``` + +To use our debugger class, we need to pass it to `giambio.run()` using +the `debugger` keyword argument, like so: + +```python +... +if __name__ == "__main__": + giambio.run(main, debugger=Debugger()) +``` + +__Note__: Note that we passed an _instance_ (see the parentheses?) **not** a class + +Running that modified code will produce a lot of output, and it should look something like this: + +``` +## Started running +-> About to run a step for 'main' +>> A task named 'child' was spawned +>> A task named 'child1' was spawned +[main] Children spawned, awaiting completion +<- Ran a step for 'main' +-> About to run a step for 'child' +[child] Child spawned!! Sleeping for 2 seconds +<- Ran a step for 'child' +# About to put 'child' to sleep for 2.00 seconds +-> About to run a step for 'child1' +[child 1] Child spawned!! Sleeping for 2 seconds +<- Ran a step for 'child1' +# About to put 'child1' to sleep for 2.00 seconds +[... 2 seconds pass ...] +# Task 'child' slept for 2.01 seconds +# Task 'child1' slept for 2.01 seconds +!! About to check for I/O for up to 0.00 seconds +!! Done I/O check (timeout 0.00 seconds) +-> About to run a step for 'child' +[child] Had a nice nap! +<< Task 'child' exited +-> About to run a step for 'child1' +[child 1] Had a nice nap! +<< Task 'child1' exited +-> About to run a step for 'main' +<- Ran a step for 'main' +-> About to run a step for 'main' +[main] Children execution complete in 2.01 seconds +<< Task 'main' exited +## Finished running +``` + +As expected, this prints _a lot_ of stuff, but let's start going trough it: +- First, we start the event loop: That's the call to `giambio.run()` + ``` + ## Started running + ``` +- After that, we start running the `main` function + ``` + -> About to run a step for 'main' + ``` +- When we run `main`, that enters the `async with` block and spawns our children, + as well as execute our call to `print` + ``` + >> A task named 'child' was spawned + >> A task named 'child1' was spawned + [main] Children spawned, awaiting completion + ``` +- After that, we hit the end of the block, so we pause and wait for our children + to complete: That's when we start switching, and `child` can now run + ``` + <- Ran a step for 'main' + -> About to run a step for 'child' + [child] Child spawned!! Sleeping for 2 seconds + ``` +- We're now at `await giambio.sleep(2)` inside `child`, and that puts it to sleep + ``` + <- Ran a step for 'child' + # About to put 'child' to sleep for 2.00 seconds + ``` +- Ok, so now `child` is asleep while `main` is waiting on its children, and `child1` can now execute, + so giambio switches again and runs that + ``` + -> About to run a step for 'child1' + [child 1] Child spawned!! Sleeping for 2 seconds + ``` +- Now we hit the call to `await giambio.sleep(2)` inside `child1`, so that also goes to sleep + ``` + <- Ran a step for 'child1' + # About to put 'child1' to sleep for 2.00 seconds + ``` +- Since there is no other work to do, giambio just waits until it wakes up the two children, + 2 seconds later + ``` + # Task 'child' slept for 2.01 seconds + # Task 'child1' slept for 2.01 seconds + ``` +- Even though we're not doing any I/O here, giambio doesn't know that, so it + does some checks (and finds out there is no I/O to do) + ``` + !! About to check for I/O for up to 0.00 seconds + !! Done I/O check (timeout 0.00 seconds) + ``` +- After 2 seconds have passed giambio wakes up our children and runs them until completion + ``` + -> About to run a step for 'child' + [child] Had a nice nap! + << Task 'child' exited + -> About to run a step for 'child1' + [child 1] Had a nice nap! + << Task 'child1' exited + ``` +- As promised, once all children exit, the parent task resumes and runs until it exits. This also + causes the entire event loop to exit because there is nothing else to do + ``` + -> About to run a step for 'main' + <- Ran a step for 'main' + -> About to run a step for 'main' + [main] Children execution complete in 2.01 seconds + << Task 'main' exited + ## Finished running + ``` + +So, in our example, our children run until they hit a call to `await giambio.sleep`, then execution control +goes back to `giambio.run`, which drives the execution for another step. This works because `giambio.sleep` and +`giambio.run` (as well as many others) work together to make this happen: `giambio.sleep` can pause the execution +of its children task and ask `giambio.run` to wake him up after a given amount of time + +__Note__: You may wonder whether you can mix async libraries: for instance, can we call `trio.sleep` in a +giambio application? The answer is no, we can't, and this section explains why. When you call +`await giambio.sleep` that function talks a language that only `giambio.run` can understand. Other libraries have other private "languages", so mixing them is not possible: doing so will cause giambio to get very confused and most likely just explode spectacularly badly -TODO: I/O - ## Contributing This is a relatively young project and it is looking for collaborators! It's not rocket science, diff --git a/giambio/__init__.py b/giambio/__init__.py index 2ec73b6..f7e7bf5 100644 --- a/giambio/__init__.py +++ b/giambio/__init__.py @@ -22,7 +22,7 @@ from . import exceptions from .traps import sleep, current_task from .objects import Event from .run import run, clock, wrap_socket, create_pool, get_event_loop, new_event_loop - +from .util import debug __all__ = [ "exceptions", @@ -34,5 +34,6 @@ __all__ = [ "create_pool", "get_event_loop", "current_task", - "new_event_loop" + "new_event_loop", + "debug" ] diff --git a/giambio/context.py b/giambio/context.py index f847550..e64af14 100644 --- a/giambio/context.py +++ b/giambio/context.py @@ -44,6 +44,7 @@ class TaskManager: task.parent = self.loop.current_task self.loop.tasks.append(task) self.tasks.append(task) + self.loop.debugger.on_task_spawn(task) def spawn_after(self, func: types.FunctionType, n: int, *args): """ @@ -53,8 +54,10 @@ class TaskManager: assert n >= 0, "The time delay can't be negative" task = Task(func(*args), func.__name__ or str(func)) task.parent = self.loop.current_task + task.sleep_start = self.loop.clock() self.loop.paused.put(task, n) self.tasks.append(task) + self.loop.debugger.on_task_schedule(task, n) async def __aenter__(self): return self @@ -66,5 +69,4 @@ class TaskManager: except BaseException: self.tasks.remove(task) for to_cancel in self.tasks: - await to_cancel.cancel() - print("oof") \ No newline at end of file + await to_cancel.cancel() \ No newline at end of file diff --git a/giambio/core.py b/giambio/core.py index c3fdf84..aa0fc60 100644 --- a/giambio/core.py +++ b/giambio/core.py @@ -24,6 +24,7 @@ from timeit import default_timer from .objects import Task, TimeQueue from socket import SOL_SOCKET, SO_ERROR from .traps import want_read, want_write +from .util.debug import BaseDebugger from collections import deque from .socket import AsyncSocket, WantWrite, WantRead from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE @@ -44,11 +45,16 @@ class AsyncScheduler: A few examples are tasks cancellation and exception propagation. """ - def __init__(self): + def __init__(self, debugger: BaseDebugger = None): """ Object constructor """ + # The debugger object. If it is none we create a dummy object that immediately returns an empty + # lambda every time you access any of its attributes to avoid lots of if self.debugger clauses + if debugger: + assert issubclass(type(debugger), BaseDebugger), "The debugger must be a subclass of giambio.util.BaseDebugger" + self.debugger = debugger or type("DumbDebugger", (object, ), {"__getattr__": lambda *args: lambda *args: None})() # Tasks that are ready to run self.tasks = deque() # Selector object to perform I/O multiplexing @@ -71,10 +77,7 @@ class AsyncScheduler: Returns True if there is work to do """ - if self.selector.get_map() or any([self.paused, - self.tasks, - self.events - ]): + if any([self.paused, self.tasks, self.events, self.selector.get_map()]): return False return True @@ -97,23 +100,25 @@ class AsyncScheduler: while True: try: if self.done(): + # If we're done, which means there is no + # sleeping tasks, no events to deliver, + # no I/O to do and no running tasks, we + # simply tear us down and return to self.start self.shutdown() break elif not self.tasks: - if self.paused: - # If there are no actively running tasks - # we try to schedule the asleep ones - self.awake_sleeping() - if self.selector.get_map(): - # The next step is checking for I/O - self.check_io() - if self.events: - # Try to awake event-waiting tasks - self.check_events() - # While there are tasks to run + # If there are no actively running tasks + # we try to schedule the asleep ones + self.awake_sleeping() + # The next step is checking for I/O + self.check_io() + # Try to awake event-waiting tasks + self.check_events() + # Otherwise, while there are tasks ready to run, well, run them! while self.tasks: # Sets the currently running task self.current_task = self.tasks.popleft() + self.debugger.before_task_step(self.current_task) if self.current_task.cancel_pending: self.do_cancel() if self.to_send and self.current_task.status != "init": @@ -124,6 +129,7 @@ class AsyncScheduler: method, *args = self.current_task.run(data) self.current_task.status = "run" self.current_task.steps += 1 + self.debugger.after_task_step(self.current_task) # Data has been sent, reset it to None if self.to_send and self.current_task != "init": self.to_send = None @@ -136,12 +142,14 @@ class AsyncScheduler: self.current_task.status = "cancelled" self.current_task.cancelled = True self.current_task.cancel_pending = False + self.debugger.after_cancel(self.current_task) self.join() # TODO: Investigate if a call to join() is needed except StopIteration as ret: # Coroutine ends self.current_task.status = "end" self.current_task.result = ret.value self.current_task.finished = True + self.debugger.on_task_exit(self.current_task) self.join() except BaseException as err: self.current_task.exc = err @@ -156,8 +164,10 @@ class AsyncScheduler: """ # TODO: Do we need anything else? + self.debugger.before_cancel(self.current_task) self.current_task.throw(CancelledError) + def get_running(self): """ Returns the current task @@ -187,7 +197,11 @@ class AsyncScheduler: # Sleep until the closest deadline in order not to waste CPU cycles while self.paused[0][0] < self.clock(): # Reschedules tasks when their deadline has elapsed - self.tasks.append(self.paused.get()) + task = self.paused.get() + slept = self.clock() - task.sleep_start + task.sleep_start = None + self.tasks.append(task) + self.debugger.after_sleep(task, slept) if not self.paused: break @@ -196,13 +210,16 @@ class AsyncScheduler: Checks and schedules task to perform I/O """ - if self.tasks or self.events: # If there are tasks or events, never wait + if self.tasks or self.events and not self.selector.get_map(): + # If there are either tasks or events and no I/O, never wait timeout = 0.0 - elif self.paused: # If there are asleep tasks, wait until the closest - # deadline + elif self.paused: + # If there are asleep tasks, wait until the closest deadline timeout = max(0.0, self.paused[0][0] - self.clock()) - else: - timeout = None # If we _only_ have I/O to do, then wait indefinitely + elif self.selector.get_map(): + # If there is *only* I/O, we wait a fixed amount of time + timeout = 1 # TODO: Is this ok? + self.debugger.before_io(timeout) for key in dict(self.selector.get_map()).values(): # We make sure we don't reschedule finished tasks if key.data.finished: @@ -213,6 +230,7 @@ class AsyncScheduler: # Get sockets that are ready and schedule their tasks for key, _ in io_ready: self.tasks.append(key.data) # Resource ready? Schedule its task + self.debugger.after_io(timeout) def start(self, func: types.FunctionType, *args): """ @@ -221,8 +239,10 @@ class AsyncScheduler: entry = Task(func(*args), func.__name__ or str(func)) self.tasks.append(entry) + self.debugger.on_start() self.run() self.has_ran = True + self.debugger.on_exit() if entry.exc: raise entry.exc from None @@ -254,8 +274,10 @@ class AsyncScheduler: Puts the caller to sleep for a given amount of seconds """ + self.debugger.before_sleep(self.current_task, seconds) if seconds: self.current_task.status = "sleep" + self.current_task.sleep_start = self.clock() self.paused.put(self.current_task, seconds) else: self.tasks.append(self.current_task) diff --git a/giambio/objects.py b/giambio/objects.py index 7c49d80..058a857 100644 --- a/giambio/objects.py +++ b/giambio/objects.py @@ -43,6 +43,7 @@ class Task: joined: bool= False cancel_pending: bool = False waiters: list = field(default_factory=list) + sleep_start: int = None def run(self, what=None): """ diff --git a/giambio/run.py b/giambio/run.py index f6ce946..ae6fbd9 100644 --- a/giambio/run.py +++ b/giambio/run.py @@ -22,6 +22,7 @@ from .core import AsyncScheduler from .exceptions import GiambioError from .context import TaskManager from .socket import AsyncSocket +from .util.debug import BaseDebugger from types import FunctionType, CoroutineType, GeneratorType @@ -40,7 +41,7 @@ def get_event_loop(): raise GiambioError("no event loop set") from None -def new_event_loop(): +def new_event_loop(debugger: BaseDebugger): """ Associates a new event loop to the current thread and deactivates the old one. This should not be @@ -50,23 +51,25 @@ def new_event_loop(): try: loop = thread_local.loop except AttributeError: - thread_local.loop = AsyncScheduler() + thread_local.loop = AsyncScheduler(debugger) else: if not loop.done(): raise GiambioError("cannot set event loop while running") else: - thread_local.loop = AsyncScheduler() + thread_local.loop = AsyncScheduler(debugger) -def run(func: FunctionType, *args): +def run(func: FunctionType, *args, **kwargs): """ Starts the event loop from a synchronous entry point """ if isinstance(func, (CoroutineType, GeneratorType)): - raise RuntimeError("Looks like you tried to call giambio.run(your_func(arg1, arg2, ...)), that is wrong!" + raise GiambioError("Looks like you tried to call giambio.run(your_func(arg1, arg2, ...)), that is wrong!" "\nWhat you wanna do, instead, is this: giambio.run(your_func, arg1, arg2, ...)") - new_event_loop() + elif not isinstance(func, FunctionType): + raise GiambioError("gaibmio.run() requires an async function as parameter!") + new_event_loop(kwargs.get("debugger", None)) thread_local.loop.start(func, *args) @@ -76,15 +79,20 @@ def clock(): loop """ - return thread_local.loop.clock() + try: + return thread_local.loop.clock() + except AttributeError: + raise GiambioError("Cannot call clock from outside an async context") from None def wrap_socket(sock: socket.socket) -> AsyncSocket: """ Wraps a synchronous socket into a giambio.socket.AsyncSocket """ - - return thread_local.loop.wrap_socket(sock) + try: + return thread_local.loop.wrap_socket(sock) + except AttributeError: + raise GiambioError("Cannot wrap a socket from outside an async context") from None def create_pool(): @@ -95,5 +103,5 @@ def create_pool(): try: return TaskManager(thread_local.loop) except AttributeError: - raise RuntimeError("It appears that giambio is not running, did you call giambio.async_pool()" + raise GiambioError("It appears that giambio is not running, did you call giambio.create_pool()" " outside of an async context?") from None diff --git a/giambio/util/__init__.py b/giambio/util/__init__.py new file mode 100644 index 0000000..d6f0e96 --- /dev/null +++ b/giambio/util/__init__.py @@ -0,0 +1,15 @@ +""" +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 + + http://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. +""" \ No newline at end of file diff --git a/giambio/util/debug.py b/giambio/util/debug.py new file mode 100644 index 0000000..7f9fb56 --- /dev/null +++ b/giambio/util/debug.py @@ -0,0 +1,197 @@ +""" +Tooling for debugging giambio + +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 + + http://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. +""" +from abc import ABC, abstractmethod +from giambio.objects import Task +from typing import Union + + +class BaseDebugger(ABC): + """ + The base for all debugger objects + """ + + @abstractmethod + def on_start(self): + """ + This method is called when the event + loop starts executing + """ + + raise NotImplementedError + + @abstractmethod + def on_exit(self): + """ + This method is called when the event + loop exits entirely (all tasks completed) + """ + + raise NotImplementedError + + @abstractmethod + def on_task_schedule(self, task: Task, delay: Union[int, float]): + """ + This method is called when a new task is + scheduled (not spawned) + + :param task: The Task object representing a + giambio Task and wrapping a coroutine + :type task: class: giambio.objects.Task + :param delay: The delay, in seconds, after which + the task will start executing + :type delay: int + """ + + raise NotImplementedError + + @abstractmethod + def on_task_spawn(self, task: Task): + """ + This method is called when a new task is + spawned + + :param task: The Task object representing a + giambio Task and wrapping a coroutine + :type task: class: giambio.objects.Task + """ + + raise NotImplementedError + + @abstractmethod + def on_task_exit(self, task: Task): + """ + This method is called when a task exits + + :param task: The Task object representing a + giambio Task and wrapping a coroutine + :type task: class: giambio.objects.Task + """ + + raise NotImplementedError + + @abstractmethod + def before_task_step(self, task: Task): + """ + This method is called right before + calling its run() method + + :param task: The Task object representing a + giambio Task and wrapping a coroutine + :type task: class: giambio.objects.Task + """ + + raise NotImplementedError + + @abstractmethod + def after_task_step(self, task: Task): + """ + This method is called right after + calling its run() method + + :param task: The Task object representing a + giambio Task and wrapping a coroutine + :type task: class: giambio.objects.Task + """ + + raise NotImplementedError + + @abstractmethod + def before_sleep(self, task: Task, seconds: Union[int, float]): + """ + This method is called before a task goes + to sleep + + :param task: The Task object representing a + giambio Task and wrapping a coroutine + :type task: class: giambio.objects.Task + :param seconds: The amount of seconds the + task wants to sleep + :type seconds: int + """ + + raise NotImplementedError + + @abstractmethod + def after_sleep(self, task: Task, seconds: Union[int, float]): + """ + This method is called before after a tasks + awakes from sleeping + + :param task: The Task object representing a + giambio Task and wrapping a coroutine + :type task: class: giambio.objects.Task + :param seconds: The amount of seconds the + task actually slept + :type seconds: int + """ + + raise NotImplementedError + + @abstractmethod + def before_io(self, timeout: Union[int, float]): + """ + This method is called right before + the event loop checks for I/O events + + :param timeout: The max. amount of seconds + that the loop will hang when using the select() + system call + :type timeout: int + """ + + raise NotImplementedError + + @abstractmethod + def after_io(self, timeout: Union[int, float]): + """ + This method is called right after + the event loop has checked for I/O events + + :param timeout: The max. amount of seconds + that the loop has hung when using the select() + system call + :type timeout: int + """ + + raise NotImplementedError + + @abstractmethod + def before_cancel(self, task: Task): + """ + This method is called right before a task + gets cancelled + + :param task: The Task object representing a + giambio Task and wrapping a coroutine + :type task: class: giambio.objects.Task + """ + + raise NotImplementedError + + @abstractmethod + def after_cancel(self, task: Task): + """ + This method is called right after a task + gets cancelled + + :param task: The Task object representing a + giambio Task and wrapping a coroutine + :type task: class: giambio.objects.Task + """ + + raise NotImplementedError \ No newline at end of file diff --git a/pyvenv.cfg b/pyvenv.cfg new file mode 100644 index 0000000..7b1c4a3 --- /dev/null +++ b/pyvenv.cfg @@ -0,0 +1,8 @@ +home = /usr +implementation = CPython +version_info = 3.7.3.final.0 +virtualenv = 20.1.0 +include-system-site-packages = false +base-prefix = /usr +base-exec-prefix = /usr +base-executable = /usr/bin/python3 diff --git a/tests/count.py b/tests/count.py deleted file mode 100644 index ef74392..0000000 --- a/tests/count.py +++ /dev/null @@ -1,42 +0,0 @@ -import giambio - - -# A test for context managers - - -async def countdown(n: int): - print(f"Counting down from {n}!") - while n > 0: - print(f"Down {n}") - n -= 1 - await giambio.sleep(1) -# raise Exception("oh no man") # Uncomment to test propagation - print("Countdown over") - return 0 - - -async def countup(stop: int, step: int = 1): - print(f"Counting up to {stop}!") - x = 0 - while x < stop: - print(f"Up {x}") - x += 1 - await giambio.sleep(step) - print("Countup over") - return 1 - - -async def main(): - start = giambio.clock() - try: - async with giambio.create_pool() as pool: - pool.spawn(countdown, 10) - pool.spawn(countup, 5, 2) - print("Children spawned, awaiting completion") - except Exception as e: - print(f"Got -> {type(e).__name__}: {e}") - print(f"Task execution complete in {giambio.clock() - start:.2f} seconds") - - -if __name__ == "__main__": - giambio.run(main) diff --git a/tests/server.py b/tests/server.py index c80c127..bb90afd 100644 --- a/tests/server.py +++ b/tests/server.py @@ -21,7 +21,7 @@ async def serve(address: tuple): conn, addr = await asock.accept() logging.info(f"{addr[0]}:{addr[1]} connected") pool.spawn(handler, conn, addr) - print("oof done") + async def handler(sock: AsyncSocket, addr: tuple): addr = f"{addr[0]}:{addr[1]}" diff --git a/tests/sleep.py b/tests/sleep.py new file mode 100644 index 0000000..09e080b --- /dev/null +++ b/tests/sleep.py @@ -0,0 +1,72 @@ +import giambio + + +class Debugger(giambio.debug.BaseDebugger): + """ + A simple debugger for this test + """ + + def on_start(self): + print("## Started running") + + def on_exit(self): + print("## Finished running") + + def on_task_schedule(self, task, delay: int): + print(f">> A task named '{task.name}' was scheduled to run in {delay:.2f} seconds") + + def on_task_spawn(self, task): + print(f">> A task named '{task.name}' was spawned") + + def on_task_exit(self, task): + print(f"<< Task '{task.name}' exited") + + def before_task_step(self, task): + print(f"-> About to run a step for '{task.name}'") + + def after_task_step(self, task): + print(f"<- Ran a step for '{task.name}'") + + def before_sleep(self, task, seconds): + print(f"# About to put '{task.name}' to sleep for {seconds:.2f} seconds") + + def after_sleep(self, task, seconds): + print(f"# Task '{task.name}' slept for {seconds:.2f} seconds") + + def before_io(self, timeout): + print(f"!! About to check for I/O for up to {timeout:.2f} seconds") + + def after_io(self, timeout): + print(f"!! Done I/O check (timeout {timeout:.2f} seconds)") + + def before_cancel(self, task): + print(f"// About to cancel '{task.name}'") + + def after_cancel(self, task): + print(f"// Cancelled '{task.name}'") + + +async def child(): + print("[child] Child spawned!! Sleeping for 2 seconds") + await giambio.sleep(2) + print("[child] Had a nice nap!") + +async def child1(): + print("[child 1] Child spawned!! Sleeping for 2 seconds") + await giambio.sleep(2) + print("[child 1] Had a nice nap!") + +async def main(): + start = giambio.clock() + try: + async with giambio.create_pool() as pool: + pool.spawn(child) + pool.spawn(child1) + print("[main] Children spawned, awaiting completion") + except Exception as e: + print(f"Got -> {type(e).__name__}: {e}") + print(f"[main] Children execution complete in {giambio.clock() - start:.2f} seconds") + + +if __name__ == "__main__": + giambio.run(main, debugger=Debugger())