mirror of https://github.com/nocturn9x/giambio.git
Updated README, added debugging utility and simplified example
This commit is contained in:
parent
a5764255ac
commit
d60a372af5
263
README.md
263
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,
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
|
|
|
@ -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")
|
||||
await to_cancel.cancel()
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
"""
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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]}"
|
||||
|
|
|
@ -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())
|
Loading…
Reference in New Issue