Updated README, added debugging utility and simplified example

This commit is contained in:
nocturn9x 2020-11-18 12:13:46 +01:00
parent a5764255ac
commit d60a372af5
12 changed files with 567 additions and 138 deletions

263
README.md
View File

@ -190,44 +190,34 @@ To demonstrate this, have a look a this example
```python ```python
import giambio import giambio
async def countdown(n: int): async def child():
print(f"Counting down from {n}!") print("[child] Child spawned! Sleeping for 2 seconds")
while n > 0: await giambio.sleep(2)
print(f"Down {n}") print("[child] Had a nice nap!")
n -= 1
await giambio.sleep(1) async def child1():
print("Countdown over") print("[child 1] Child spawned! Sleeping for 2 seconds")
return 0 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(): async def main():
start = giambio.clock() start = giambio.clock()
async with giambio.create_pool() as pool: async with giambio.create_pool() as pool:
pool.spawn(countdown, 10) pool.spawn(child)
pool.spawn(countup, 5) pool.spawn(child1)
print("Children spawned, awaiting completion") print("[main] Children spawned, awaiting completion")
print(f"Task execution complete in {giambio.clock() - start:2f} seconds") print(f"[main] Children execution complete in {giambio.clock() - start:.2f} seconds")
if __name__ == "__main__": if __name__ == "__main__":
giambio.run(main) giambio.run(main)
``` ```
There is a lot going on here, and we'll explain every bit of it step by step: 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` - First, we imported giambio and defined two async functions: `child` and `child1`
- These two functions do exactly what their name suggests, but for the purposes of - These two functions will just print something and then sleep for 2 seconds
this tutorial, `countup` will be running twice as slow as `countdown` (see the call
to `await giambio.sleep(2)`?)
- Here comes the real fun: `async with`? What's going on there? - 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 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. 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: Ok, so, let's try running this snippet and see what we get:
``` ```
Children spawned, awaiting completion [child] Child spawned!! Sleeping for 2 seconds
Counting down from 10! [child 1] Child spawned!! Sleeping for 2 seconds
Down 10 [child] Had a nice nap!
Counting up to 5! [child 1] Had a nice nap!
Up 0 [main] Children execution complete in 2.01 seconds
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
``` ```
(Your output might have some lines swapped compared to this) (Your output might have some lines swapped compared to this)
You see how `countup` and `countdown` both start and finish You see how `child` and `child1` both start and finish
together? Moreover, even though each function slept for about 10 together? Moreover, even though each function slept for about 2
seconds (therefore 20 seconds total), the program just took 10 seconds (therefore 4 seconds total), the program just took 2
seconds to complete, so our children are really running at the same time. 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: 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, 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. 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 ### A closer look
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 In the above section we explained the theory behind async functions, but now we'll inspect the magic behind
`await giambio.something()`, what you're doing is sending "commands" to the event loop asking it `giambio.run()` and its event loop to demistify _how_ giambio makes this whole async thing happen. Luckily for us,
to perform a certain thing in a given task, and to communicate your intent to the loop, the giambio has some useful tooling that lets us sneak peak inside the machinery of the library to better help us
primitives (such as `giambio.sleep`) talk a language that only giambio's event loop can understand. 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 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 giambio to get very confused and most likely just explode spectacularly badly
TODO: I/O
## Contributing ## Contributing
This is a relatively young project and it is looking for collaborators! It's not rocket science, This is a relatively young project and it is looking for collaborators! It's not rocket science,

View File

@ -22,7 +22,7 @@ from . import exceptions
from .traps import sleep, current_task from .traps import sleep, current_task
from .objects import Event from .objects import Event
from .run import run, clock, wrap_socket, create_pool, get_event_loop, new_event_loop from .run import run, clock, wrap_socket, create_pool, get_event_loop, new_event_loop
from .util import debug
__all__ = [ __all__ = [
"exceptions", "exceptions",
@ -34,5 +34,6 @@ __all__ = [
"create_pool", "create_pool",
"get_event_loop", "get_event_loop",
"current_task", "current_task",
"new_event_loop" "new_event_loop",
"debug"
] ]

View File

@ -44,6 +44,7 @@ class TaskManager:
task.parent = self.loop.current_task task.parent = self.loop.current_task
self.loop.tasks.append(task) self.loop.tasks.append(task)
self.tasks.append(task) self.tasks.append(task)
self.loop.debugger.on_task_spawn(task)
def spawn_after(self, func: types.FunctionType, n: int, *args): 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" assert n >= 0, "The time delay can't be negative"
task = Task(func(*args), func.__name__ or str(func)) task = Task(func(*args), func.__name__ or str(func))
task.parent = self.loop.current_task task.parent = self.loop.current_task
task.sleep_start = self.loop.clock()
self.loop.paused.put(task, n) self.loop.paused.put(task, n)
self.tasks.append(task) self.tasks.append(task)
self.loop.debugger.on_task_schedule(task, n)
async def __aenter__(self): async def __aenter__(self):
return self return self
@ -66,5 +69,4 @@ class TaskManager:
except BaseException: except BaseException:
self.tasks.remove(task) self.tasks.remove(task)
for to_cancel in self.tasks: for to_cancel in self.tasks:
await to_cancel.cancel() await to_cancel.cancel()
print("oof")

View File

@ -24,6 +24,7 @@ from timeit import default_timer
from .objects import Task, TimeQueue from .objects import Task, TimeQueue
from socket import SOL_SOCKET, SO_ERROR from socket import SOL_SOCKET, SO_ERROR
from .traps import want_read, want_write from .traps import want_read, want_write
from .util.debug import BaseDebugger
from collections import deque from collections import deque
from .socket import AsyncSocket, WantWrite, WantRead from .socket import AsyncSocket, WantWrite, WantRead
from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE
@ -44,11 +45,16 @@ class AsyncScheduler:
A few examples are tasks cancellation and exception propagation. A few examples are tasks cancellation and exception propagation.
""" """
def __init__(self): def __init__(self, debugger: BaseDebugger = None):
""" """
Object constructor 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 # Tasks that are ready to run
self.tasks = deque() self.tasks = deque()
# Selector object to perform I/O multiplexing # Selector object to perform I/O multiplexing
@ -71,10 +77,7 @@ class AsyncScheduler:
Returns True if there is work to do Returns True if there is work to do
""" """
if self.selector.get_map() or any([self.paused, if any([self.paused, self.tasks, self.events, self.selector.get_map()]):
self.tasks,
self.events
]):
return False return False
return True return True
@ -97,23 +100,25 @@ class AsyncScheduler:
while True: while True:
try: try:
if self.done(): 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() self.shutdown()
break break
elif not self.tasks: elif not self.tasks:
if self.paused: # If there are no actively running tasks
# If there are no actively running tasks # we try to schedule the asleep ones
# we try to schedule the asleep ones self.awake_sleeping()
self.awake_sleeping() # The next step is checking for I/O
if self.selector.get_map(): self.check_io()
# The next step is checking for I/O # Try to awake event-waiting tasks
self.check_io() self.check_events()
if self.events: # Otherwise, while there are tasks ready to run, well, run them!
# Try to awake event-waiting tasks
self.check_events()
# While there are tasks to run
while self.tasks: while self.tasks:
# Sets the currently running task # Sets the currently running task
self.current_task = self.tasks.popleft() self.current_task = self.tasks.popleft()
self.debugger.before_task_step(self.current_task)
if self.current_task.cancel_pending: if self.current_task.cancel_pending:
self.do_cancel() self.do_cancel()
if self.to_send and self.current_task.status != "init": if self.to_send and self.current_task.status != "init":
@ -124,6 +129,7 @@ class AsyncScheduler:
method, *args = self.current_task.run(data) method, *args = self.current_task.run(data)
self.current_task.status = "run" self.current_task.status = "run"
self.current_task.steps += 1 self.current_task.steps += 1
self.debugger.after_task_step(self.current_task)
# Data has been sent, reset it to None # Data has been sent, reset it to None
if self.to_send and self.current_task != "init": if self.to_send and self.current_task != "init":
self.to_send = None self.to_send = None
@ -136,12 +142,14 @@ class AsyncScheduler:
self.current_task.status = "cancelled" self.current_task.status = "cancelled"
self.current_task.cancelled = True self.current_task.cancelled = True
self.current_task.cancel_pending = False self.current_task.cancel_pending = False
self.debugger.after_cancel(self.current_task)
self.join() # TODO: Investigate if a call to join() is needed self.join() # TODO: Investigate if a call to join() is needed
except StopIteration as ret: except StopIteration as ret:
# Coroutine ends # Coroutine ends
self.current_task.status = "end" self.current_task.status = "end"
self.current_task.result = ret.value self.current_task.result = ret.value
self.current_task.finished = True self.current_task.finished = True
self.debugger.on_task_exit(self.current_task)
self.join() self.join()
except BaseException as err: except BaseException as err:
self.current_task.exc = err self.current_task.exc = err
@ -156,8 +164,10 @@ class AsyncScheduler:
""" """
# TODO: Do we need anything else? # TODO: Do we need anything else?
self.debugger.before_cancel(self.current_task)
self.current_task.throw(CancelledError) self.current_task.throw(CancelledError)
def get_running(self): def get_running(self):
""" """
Returns the current task Returns the current task
@ -187,7 +197,11 @@ class AsyncScheduler:
# Sleep until the closest deadline in order not to waste CPU cycles # Sleep until the closest deadline in order not to waste CPU cycles
while self.paused[0][0] < self.clock(): while self.paused[0][0] < self.clock():
# Reschedules tasks when their deadline has elapsed # 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: if not self.paused:
break break
@ -196,13 +210,16 @@ class AsyncScheduler:
Checks and schedules task to perform I/O 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 timeout = 0.0
elif self.paused: # If there are asleep tasks, wait until the closest elif self.paused:
# deadline # If there are asleep tasks, wait until the closest deadline
timeout = max(0.0, self.paused[0][0] - self.clock()) timeout = max(0.0, self.paused[0][0] - self.clock())
else: elif self.selector.get_map():
timeout = None # If we _only_ have I/O to do, then wait indefinitely # 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(): for key in dict(self.selector.get_map()).values():
# We make sure we don't reschedule finished tasks # We make sure we don't reschedule finished tasks
if key.data.finished: if key.data.finished:
@ -213,6 +230,7 @@ class AsyncScheduler:
# Get sockets that are ready and schedule their tasks # Get sockets that are ready and schedule their tasks
for key, _ in io_ready: for key, _ in io_ready:
self.tasks.append(key.data) # Resource ready? Schedule its task self.tasks.append(key.data) # Resource ready? Schedule its task
self.debugger.after_io(timeout)
def start(self, func: types.FunctionType, *args): def start(self, func: types.FunctionType, *args):
""" """
@ -221,8 +239,10 @@ class AsyncScheduler:
entry = Task(func(*args), func.__name__ or str(func)) entry = Task(func(*args), func.__name__ or str(func))
self.tasks.append(entry) self.tasks.append(entry)
self.debugger.on_start()
self.run() self.run()
self.has_ran = True self.has_ran = True
self.debugger.on_exit()
if entry.exc: if entry.exc:
raise entry.exc from None raise entry.exc from None
@ -254,8 +274,10 @@ class AsyncScheduler:
Puts the caller to sleep for a given amount of seconds Puts the caller to sleep for a given amount of seconds
""" """
self.debugger.before_sleep(self.current_task, seconds)
if seconds: if seconds:
self.current_task.status = "sleep" self.current_task.status = "sleep"
self.current_task.sleep_start = self.clock()
self.paused.put(self.current_task, seconds) self.paused.put(self.current_task, seconds)
else: else:
self.tasks.append(self.current_task) self.tasks.append(self.current_task)

View File

@ -43,6 +43,7 @@ class Task:
joined: bool= False joined: bool= False
cancel_pending: bool = False cancel_pending: bool = False
waiters: list = field(default_factory=list) waiters: list = field(default_factory=list)
sleep_start: int = None
def run(self, what=None): def run(self, what=None):
""" """

View File

@ -22,6 +22,7 @@ from .core import AsyncScheduler
from .exceptions import GiambioError from .exceptions import GiambioError
from .context import TaskManager from .context import TaskManager
from .socket import AsyncSocket from .socket import AsyncSocket
from .util.debug import BaseDebugger
from types import FunctionType, CoroutineType, GeneratorType from types import FunctionType, CoroutineType, GeneratorType
@ -40,7 +41,7 @@ def get_event_loop():
raise GiambioError("no event loop set") from None 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 Associates a new event loop to the current thread
and deactivates the old one. This should not be and deactivates the old one. This should not be
@ -50,23 +51,25 @@ def new_event_loop():
try: try:
loop = thread_local.loop loop = thread_local.loop
except AttributeError: except AttributeError:
thread_local.loop = AsyncScheduler() thread_local.loop = AsyncScheduler(debugger)
else: else:
if not loop.done(): if not loop.done():
raise GiambioError("cannot set event loop while running") raise GiambioError("cannot set event loop while running")
else: 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 Starts the event loop from a synchronous entry point
""" """
if isinstance(func, (CoroutineType, GeneratorType)): 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, ...)") "\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) thread_local.loop.start(func, *args)
@ -76,15 +79,20 @@ def clock():
loop 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: def wrap_socket(sock: socket.socket) -> AsyncSocket:
""" """
Wraps a synchronous socket into a giambio.socket.AsyncSocket Wraps a synchronous socket into a giambio.socket.AsyncSocket
""" """
try:
return thread_local.loop.wrap_socket(sock) 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(): def create_pool():
@ -95,5 +103,5 @@ def create_pool():
try: try:
return TaskManager(thread_local.loop) return TaskManager(thread_local.loop)
except AttributeError: 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 " outside of an async context?") from None

15
giambio/util/__init__.py Normal file
View File

@ -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.
"""

197
giambio/util/debug.py Normal file
View File

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

8
pyvenv.cfg Normal file
View File

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

View File

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

View File

@ -21,7 +21,7 @@ async def serve(address: tuple):
conn, addr = await asock.accept() conn, addr = await asock.accept()
logging.info(f"{addr[0]}:{addr[1]} connected") logging.info(f"{addr[0]}:{addr[1]} connected")
pool.spawn(handler, conn, addr) pool.spawn(handler, conn, addr)
print("oof done")
async def handler(sock: AsyncSocket, addr: tuple): async def handler(sock: AsyncSocket, addr: tuple):
addr = f"{addr[0]}:{addr[1]}" addr = f"{addr[0]}:{addr[1]}"

72
tests/sleep.py Normal file
View File

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