Beautified and simplified some code + README additions

This commit is contained in:
nocturn9x 2021-07-22 11:13:08 +02:00
parent 2003f096f6
commit 44a07521b2
19 changed files with 102 additions and 130 deletions

View File

@ -59,6 +59,13 @@ Giambio means to take the best of all of its predecessors, while being:
- Dependency-free: No fancy C modules, no external libraries, just pure idiomatic Python code - Dependency-free: No fancy C modules, no external libraries, just pure idiomatic Python code
- Community-based: I frankly wouldn't have bothered making this if curio was open to community additions - Community-based: I frankly wouldn't have bothered making this if curio was open to community additions
Another problem I would like to address and that I've heard some developers rant about is the lack of control
that the `run()` paradigm causes: you can read a way better and more detailed explanation [here](https://gist.github.com/Justasic/b57bfd05dd8e7a108bc433c8c9a66e59).
Giambio fixes this problem by exposing all of its internal machinery to the public and also allowing
to not start listening for events automatically by doing `AsyncScheduler(...).start(..., loop=False)`, in which case
the responsibility of handling everything (including loop ticks) is transferred to the end user allowing for a much
more granular control of the loop according to one's needs.
## Current limitations ## Current limitations
giambio is **highly** experimental and there's a lot to work to do before it's usable. Namely: giambio is **highly** experimental and there's a lot to work to do before it's usable. Namely:

View File

@ -20,7 +20,7 @@ __author__ = "Nocturn9x"
__version__ = (0, 0, 1) __version__ = (0, 0, 1)
from . import exceptions, socket, context, core from . import exceptions, socket, context, core, task, io
from .traps import sleep, current_task from .traps import sleep, current_task
from .sync import Event from .sync import Event
from .run import run, clock, create_pool, get_event_loop, new_event_loop, with_timeout from .run import run, clock, create_pool, get_event_loop, new_event_loop, with_timeout

View File

@ -34,15 +34,13 @@ class TaskManager:
Object constructor Object constructor
""" """
# The event loop associated with this pool
self.loop: giambio.core.AsyncScheduler = giambio.get_event_loop()
# All the tasks that belong to this pool # All the tasks that belong to this pool
self.tasks: List[giambio.objects.Task] = [] self.tasks: List[giambio.task.Task] = []
# Whether we have been cancelled or not # Whether we have been cancelled or not
self.cancelled: bool = False self.cancelled: bool = False
# The clock time of when we started running, used for # The clock time of when we started running, used for
# timeouts expiration # timeouts expiration
self.started: float = self.loop.clock() self.started: float = giambio.clock()
# The pool's timeout (in seconds) # The pool's timeout (in seconds)
if timeout: if timeout:
self.timeout: float = self.started + timeout self.timeout: float = self.started + timeout
@ -57,7 +55,7 @@ class TaskManager:
Spawns a child task Spawns a child task
""" """
assert self._proper_init assert self._proper_init, "Cannot use improperly initialized pool"
return await giambio.traps.create_task(func, *args) return await giambio.traps.create_task(func, *args)
async def __aenter__(self): async def __aenter__(self):

View File

@ -18,15 +18,11 @@ limitations under the License.
# Import libraries and internal resources # Import libraries and internal resources
import types import types
import socket
from itertools import chain
from giambio.task import Task from giambio.task import Task
from giambio.sync import Event
from timeit import default_timer from timeit import default_timer
from giambio.context import TaskManager from giambio.context import TaskManager
from typing import List, Optional, Set, Any from typing import List, Optional, Any
from giambio.util.debug import BaseDebugger from giambio.util.debug import BaseDebugger
from giambio.traps import want_read, want_write
from giambio.internal import TimeQueue, DeadlinesQueue from giambio.internal import TimeQueue, DeadlinesQueue
from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE
from giambio.exceptions import ( from giambio.exceptions import (
@ -105,7 +101,7 @@ class AsyncScheduler:
# Tasks that are ready to run # Tasks that are ready to run
self.run_ready: List[Task] = [] self.run_ready: List[Task] = []
# Selector object to perform I/O multiplexing # Selector object to perform I/O multiplexing
self.selector: DefaultSelector = DefaultSelector() self.selector = selector or DefaultSelector()
# This will always point to the currently running coroutine (Task object) # This will always point to the currently running coroutine (Task object)
self.current_task: Optional[Task] = None self.current_task: Optional[Task] = None
# Monotonic clock to keep track of elapsed time reliably # Monotonic clock to keep track of elapsed time reliably
@ -134,11 +130,25 @@ class AsyncScheduler:
Returns repr(self) Returns repr(self)
""" """
fields = {"debugger", "tasks", "run_ready", "selector", "current_task", fields = {
"clock", "paused", "has_ran", "current_pool", "io_skip", "debugger",
"deadlines", "_data", "io_skip_limit", "io_max_timeout" "tasks",
} "run_ready",
data = ", ".join(name + "=" + str(value) for name, value in zip(fields, (getattr(self, field) for field in fields))) "selector",
"current_task",
"clock",
"paused",
"has_ran",
"current_pool",
"io_skip",
"deadlines",
"_data",
"io_skip_limit",
"io_max_timeout",
}
data = ", ".join(
name + "=" + str(value) for name, value in zip(fields, (getattr(self, field) for field in fields))
)
return f"{type(self).__name__}({data})" return f"{type(self).__name__}({data})"
def done(self) -> bool: def done(self) -> bool:
@ -146,18 +156,16 @@ class AsyncScheduler:
Returns True if there is no work to do Returns True if there is no work to do
""" """
if any([self.paused, self.run_ready, self.selector.get_map()]): return not any([self.paused, self.run_ready, self.selector.get_map()])
return False
return True
def shutdown(self): def shutdown(self):
""" """
Shuts down the event loop Shuts down the event loop
""" """
for task in self.tasks:
self.io_release_task(task)
self.selector.close() self.selector.close()
self.tasks = []
self.current_task = self.current_pool = None
# TODO: Anything else? # TODO: Anything else?
def run(self): def run(self):
@ -198,11 +206,7 @@ class AsyncScheduler:
if self.paused: if self.paused:
# Next we try to (re)schedule the asleep tasks # Next we try to (re)schedule the asleep tasks
self.awake_sleeping() self.awake_sleeping()
if ( if self.current_pool and self.current_pool.timeout and not self.current_pool.timed_out:
self.current_pool
and self.current_pool.timeout
and not self.current_pool.timed_out
):
# Stores deadlines for tasks (deadlines are pool-specific). # Stores deadlines for tasks (deadlines are pool-specific).
# The deadlines queue will internally make sure not to store # The deadlines queue will internally make sure not to store
# a deadline for the same pool twice. This makes the timeouts # a deadline for the same pool twice. This makes the timeouts
@ -232,14 +236,21 @@ class AsyncScheduler:
self.current_task.exc = err self.current_task.exc = err
self.join(self.current_task) self.join(self.current_task)
def create_task(self, coro, *args) -> Task: def create_task(self, corofunc: types.FunctionType, *args, **kwargs) -> Task:
""" """
Creates a task Creates a task from a coroutine function and schedules it
to run. Any extra keyword or positional argument are then
passed to the function
:param corofunc: The coroutine function (not a coroutine!) to
spawn
:type corofunc: function
""" """
task = Task(coro.__name__ or str(coro), coro(*args), self.current_pool) task = Task(corofunc.__name__ or str(corofunc), corofunc(*args, **kwargs), self.current_pool)
task.next_deadline = self.current_pool.timeout or 0.0 task.next_deadline = self.current_pool.timeout or 0.0
task.joiners = {self.current_task} task.joiners = {self.current_task}
self._data = task
self.tasks.append(task) self.tasks.append(task)
self.run_ready.append(task) self.run_ready.append(task)
self.debugger.on_task_spawn(task) self.debugger.on_task_spawn(task)
@ -251,7 +262,7 @@ class AsyncScheduler:
""" """
Runs a single step for the current task. Runs a single step for the current task.
A step ends when the task awaits any of A step ends when the task awaits any of
giambio's primitives or async methods. our primitives or async methods.
Note that this method does NOT catch any Note that this method does NOT catch any
exception arising from tasks, nor does it exception arising from tasks, nor does it
@ -278,7 +289,7 @@ class AsyncScheduler:
# somewhere) # somewhere)
method, *args = self.current_task.run(data) method, *args = self.current_task.run(data)
if data is self._data: if data is self._data:
self._data = None self._data = None
# Some debugging and internal chatter here # Some debugging and internal chatter here
self.current_task.status = "run" self.current_task.status = "run"
self.current_task.steps += 1 self.current_task.steps += 1
@ -290,8 +301,7 @@ class AsyncScheduler:
# compared to us. If you get this exception and you're 100% sure you're # compared to us. If you get this exception and you're 100% sure you're
# not mixing async primitives from other libraries, then it's a bug! # not mixing async primitives from other libraries, then it's a bug!
raise InternalError( raise InternalError(
"Uh oh! Something very bad just happened, did" "Uh oh! Something very bad just happened, did you try to mix primitives from other async libraries?"
" you try to mix primitives from other async libraries?"
) from None ) from None
# Sneaky method call, thanks to David Beazley for this ;) # Sneaky method call, thanks to David Beazley for this ;)
getattr(self, method)(*args) getattr(self, method)(*args)
@ -320,12 +330,19 @@ class AsyncScheduler:
if self.selector.get_map() and sock in self.selector.get_map(): if self.selector.get_map() and sock in self.selector.get_map():
self.selector.unregister(sock) self.selector.unregister(sock)
def suspend(self): def suspend(self, task: Task):
""" """
Suspends execution of the current task Suspends execution of the given task. This is basically
a do-nothing method, since it will not reschedule the task
before returning. The task will stay suspended as long as
something else outside the loop calls a trap to reschedule it.
This method will unregister any I/O as well to ensure the task
isn't rescheduled in further calls to select()
""" """
... # TODO: Unschedule I/O? if task.last_io:
self.io_release_task(task)
def reschedule_running(self): def reschedule_running(self):
""" """
@ -334,6 +351,8 @@ class AsyncScheduler:
if self.current_task: if self.current_task:
self.run_ready.append(self.current_task) self.run_ready.append(self.current_task)
else:
raise GiambioError("giambio is not running")
def do_cancel(self, task: Task): def do_cancel(self, task: Task):
""" """
@ -357,7 +376,6 @@ class AsyncScheduler:
self._data = self.current_task self._data = self.current_task
self.reschedule_running() self.reschedule_running()
def get_current_pool(self): def get_current_pool(self):
""" """
'Returns' the current pool to an async caller 'Returns' the current pool to an async caller
@ -366,7 +384,6 @@ class AsyncScheduler:
self._data = self.current_pool self._data = self.current_pool
self.reschedule_running() self.reschedule_running()
def get_current_loop(self): def get_current_loop(self):
""" """
'Returns' self to an async caller 'Returns' self to an async caller
@ -394,6 +411,7 @@ class AsyncScheduler:
""" """
self.run_ready.extend(tasks) self.run_ready.extend(tasks)
self.reschedule_running()
def awake_sleeping(self): def awake_sleeping(self):
""" """
@ -470,7 +488,9 @@ class AsyncScheduler:
def start(self, func: types.FunctionType, *args, loop: bool = True): def start(self, func: types.FunctionType, *args, loop: bool = True):
""" """
Starts the event loop from a sync context Starts the event loop from a sync context. If the loop parameter
is false, the event loop will not start listening for events
automatically and the dispatching is on the users' shoulders
""" """
entry = Task(func.__name__ or str(func), func(*args), None) entry = Task(func.__name__ or str(func), func(*args), None)
@ -502,7 +522,7 @@ class AsyncScheduler:
else: # If we're at the main task, we're sure everything else exited else: # If we're at the main task, we're sure everything else exited
return True return True
def get_all_tasks(self) -> chain: def get_all_tasks(self) -> List[Task]:
""" """
Returns a list of all the tasks the loop is currently Returns a list of all the tasks the loop is currently
keeping track of: this includes both running and paused tasks. keeping track of: this includes both running and paused tasks.
@ -536,9 +556,7 @@ class AsyncScheduler:
if ensure_done: if ensure_done:
self.cancel_all() self.cancel_all()
elif not self.done(): elif not self.done():
raise GiambioError( raise GiambioError("event loop not terminated, call this method with ensure_done=False to forcefully exit")
"event loop not terminated, call this method with ensure_done=False to forcefully exit"
)
self.shutdown() self.shutdown()
def reschedule_joiners(self, task: Task): def reschedule_joiners(self, task: Task):
@ -639,9 +657,7 @@ class AsyncScheduler:
# or dangling resource open after being cancelled, so maybe we need # or dangling resource open after being cancelled, so maybe we need
# a different approach altogether # a different approach altogether
if task.status == "io": if task.status == "io":
for k in filter( for k in filter(lambda o: o.data == task, dict(self.selector.get_map()).values()):
lambda o: o.data == task, dict(self.selector.get_map()).values()
):
self.selector.unregister(k.fileobj) self.selector.unregister(k.fileobj)
elif task.status == "sleep": elif task.status == "sleep":
self.paused.discard(task) self.paused.discard(task)
@ -711,26 +727,4 @@ class AsyncScheduler:
self.selector.register(sock, evt, self.current_task) self.selector.register(sock, evt, self.current_task)
except KeyError: except KeyError:
# The socket is already registered doing something else # The socket is already registered doing something else
raise ResourceBusy( raise ResourceBusy("The given socket is being read/written by another task") from None
"The given socket is being read/written by another task"
) from None
# noinspection PyMethodMayBeStatic
async def connect_sock(self, sock: socket.socket, address_tuple: tuple):
"""
Connects a socket asynchronously to a given endpoint
:param sock: The socket that must to be connected
:type sock: socket.socket
:param address_tuple: A tuple in the same form as the one
passed to socket.socket.connect with an address as a string
and a port as an integer
:type address_tuple: tuple
"""
await want_write(sock)
try:
return sock.connect(address_tuple)
except BlockingIOError:
await want_write(sock)
return sock.connect(address_tuple)

View File

@ -95,7 +95,9 @@ class ErrorStack(GiambioError):
tracebacks = "" tracebacks = ""
for i, err in enumerate(self.errors): for i, err in enumerate(self.errors):
if i not in (1, len(self.errors)): if i not in (1, len(self.errors)):
tracebacks += f"\n{''.join(traceback.format_exception(type(err), err, err.__traceback__))}\n{'-' * 32}\n" tracebacks += (
f"\n{''.join(traceback.format_exception(type(err), err, err.__traceback__))}\n{'-' * 32}\n"
)
else: else:
tracebacks += f"\n{''.join(traceback.format_exception(type(err), err, err.__traceback__))}" tracebacks += f"\n{''.join(traceback.format_exception(type(err), err, err.__traceback__))}"
return f"Multiple errors occurred:\n{tracebacks}" return f"Multiple errors occurred:\n{tracebacks}"

View File

@ -15,8 +15,11 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
""" """
import giambio
from typing import List, Tuple
from giambio.task import Task from giambio.task import Task
from heapq import heappush, heappop from heapq import heappush, heappop, heapify
class TimeQueue: class TimeQueue:

View File

@ -169,7 +169,7 @@ class AsyncSocket:
Wrapper socket method Wrapper socket method
""" """
raise RuntimeError('Use with_timeout() to set a timeout') raise RuntimeError("Use with_timeout() to set a timeout")
def gettimeout(self): def gettimeout(self):
""" """
@ -205,15 +205,15 @@ class AsyncSocket:
try: try:
result = self.sock.connect(address) result = self.sock.connect(address)
if getattr(self, 'do_handshake_on_connect', False): if getattr(self, "do_handshake_on_connect", False):
await self.do_handshake() await self.do_handshake()
return result return result
except WantWrite: except WantWrite:
await want_write(self.sock) await want_write(self.sock)
err = self.sock.getsockopt(SOL_SOCKET, SO_ERROR) err = self.sock.getsockopt(SOL_SOCKET, SO_ERROR)
if err != 0: if err != 0:
raise OSError(err, f'Connect call failed {address}') raise OSError(err, f"Connect call failed {address}")
if getattr(self, 'do_handshake_on_connect', False): if getattr(self, "do_handshake_on_connect", False):
await self.do_handshake() await self.do_handshake()
async def recvfrom(self, buffersize, flags=0): async def recvfrom(self, buffersize, flags=0):

View File

@ -16,6 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
""" """
from giambio.traps import event_wait, event_set from giambio.traps import event_wait, event_set
from giambio.exceptions import GiambioError
class Event: class Event:
@ -38,7 +39,7 @@ class Event:
""" """
if self.set: if self.set:
raise giambio.exceptions.GiambioError("The event has already been set") raise GiambioError("The event has already been set")
await event_set(self) await event_set(self)
async def wait(self): async def wait(self):

View File

@ -18,7 +18,7 @@ limitations under the License.
import giambio import giambio
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Union, Coroutine, List, Tuple, Set from typing import Union, Coroutine, Set
@dataclass @dataclass
@ -104,8 +104,9 @@ class Task:
are propagated as well are propagated as well
""" """
self.joiners.add(await giambio.traps.current_task()) task = await giambio.traps.current_task()
print(self.joiners) if task:
self.joiners.add(task)
res = await giambio.traps.join(self) res = await giambio.traps.join(self)
if self.exc: if self.exc:
raise self.exc raise self.exc
@ -142,5 +143,4 @@ class Task:
self.coroutine.close() self.coroutine.close()
except RuntimeError: except RuntimeError:
pass # TODO: This is kinda bad pass # TODO: This is kinda bad
assert not self.last_io assert not self.last_io, f"task {self.name} was destroyed, but has pending I/O"

View File

@ -25,7 +25,7 @@ import types
import inspect import inspect
from giambio.task import Task from giambio.task import Task
from types import FunctionType from types import FunctionType
from typing import List, Union, Iterable from typing import Union, Iterable
from giambio.exceptions import GiambioError from giambio.exceptions import GiambioError
@ -58,7 +58,7 @@ async def create_task(coro: FunctionType, *args):
elif inspect.iscoroutinefunction(coro): elif inspect.iscoroutinefunction(coro):
return await create_trap("create_task", coro, *args) return await create_trap("create_task", coro, *args)
else: else:
raise TypeError("coro must be a coroutine or coroutine function") raise TypeError("coro must be a coroutine function")
async def sleep(seconds: Union[int, float]): async def sleep(seconds: Union[int, float]):
@ -122,7 +122,7 @@ async def join(task):
Awaits a given task for completion Awaits a given task for completion
:param task: The task to join :param task: The task to join
:type task: class: Task :type task: :class: Task
""" """
return await create_trap("join", task) return await create_trap("join", task)
@ -187,7 +187,6 @@ async def event_set(event):
""" """
event.set = True event.set = True
await reschedule_running()
await schedule_tasks(event.waiters) await schedule_tasks(event.waiters)
@ -197,11 +196,3 @@ async def schedule_tasks(tasks: Iterable[Task]):
""" """
await create_trap("schedule_tasks", tasks) await create_trap("schedule_tasks", tasks)
async def reschedule_running():
"""
Reschedules the current task for execution
"""
await create_trap("reschedule_running")

View File

@ -11,15 +11,11 @@ async def child(name: int):
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:
await pool.spawn( await pool.spawn(child, 1) # If you comment this line, the pool will exit immediately!
child, 1
) # If you comment this line, the pool will exit immediately!
task = await pool.spawn(child, 2) task = await pool.spawn(child, 2)
await task.cancel() await task.cancel()
print("[main] Children spawned, awaiting completion") print("[main] Children spawned, awaiting completion")
print( print(f"[main] Children execution complete in {giambio.clock() - start:.2f} seconds")
f"[main] Children execution complete in {giambio.clock() - start:.2f} seconds"
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -13,9 +13,7 @@ class Debugger(giambio.debug.BaseDebugger):
print("## Finished running") print("## Finished running")
def on_task_schedule(self, task, delay: int): def on_task_schedule(self, task, delay: int):
print( print(f">> A task named '{task.name}' was scheduled to run in {delay:.2f} seconds")
f">> A task named '{task.name}' was scheduled to run in {delay:.2f} seconds"
)
def on_task_spawn(self, task): def on_task_spawn(self, task):
print(f">> A task named '{task.name}' was spawned") print(f">> A task named '{task.name}' was spawned")

View File

@ -15,9 +15,7 @@ async def child(ev: giambio.Event, pause: int):
await giambio.sleep(pause) await giambio.sleep(pause)
end_sleep = giambio.clock() - start_sleep end_sleep = giambio.clock() - start_sleep
end_total = giambio.clock() - start_total end_total = giambio.clock() - start_total
print( print(f"[child] Done! Slept for {end_total} seconds total ({end_pause} paused, {end_sleep} sleeping), nice nap!")
f"[child] Done! Slept for {end_total} seconds total ({end_pause} paused, {end_sleep} sleeping), nice nap!"
)
async def parent(pause: int = 1): async def parent(pause: int = 1):

View File

@ -26,9 +26,7 @@ async def main():
except Exception as error: except Exception as error:
# Because exceptions just *work*! # Because exceptions just *work*!
print(f"[main] Exception from child caught! {repr(error)}") print(f"[main] Exception from child caught! {repr(error)}")
print( print(f"[main] Children execution complete in {giambio.clock() - start:.2f} seconds")
f"[main] Children execution complete in {giambio.clock() - start:.2f} seconds"
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -43,9 +43,7 @@ async def main():
except Exception as error: except Exception as error:
# Because exceptions just *work*! # Because exceptions just *work*!
print(f"[main] Exception from child caught! {repr(error)}") print(f"[main] Exception from child caught! {repr(error)}")
print( print(f"[main] Children execution complete in {giambio.clock() - start:.2f} seconds")
f"[main] Children execution complete in {giambio.clock() - start:.2f} seconds"
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -19,9 +19,7 @@ async def main():
print("[main] Children spawned, awaiting completion") print("[main] Children spawned, awaiting completion")
# This will *only* execute when everything inside the async with block # This will *only* execute when everything inside the async with block
# has ran, including any other pool # has ran, including any other pool
print( print(f"[main] Children execution complete in {giambio.clock() - start:.2f} seconds")
f"[main] Children execution complete in {giambio.clock() - start:.2f} seconds"
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -29,9 +29,7 @@ async def handler(sock: AsyncSocket, client_address: tuple):
""" """
Handles a single client connection Handles a single client connection
:param sock: The giambio.socket.AsyncSocket object connected :param sock: The AsyncSocket object connected to the client
to the client
:type sock: :class: giambio.socket.AsyncSocket
:param client_address: The client's address represented as a tuple :param client_address: The client's address represented as a tuple
(address, port) where address is a string and port is an integer (address, port) where address is a string and port is an integer
:type client_address: tuple :type client_address: tuple
@ -39,9 +37,7 @@ async def handler(sock: AsyncSocket, client_address: tuple):
address = f"{client_address[0]}:{client_address[1]}" address = f"{client_address[0]}:{client_address[1]}"
async with sock: # Closes the socket automatically async with sock: # Closes the socket automatically
await sock.send_all( await sock.send_all(b"Welcome to the server pal, feel free to send me something!\n")
b"Welcome to the server pal, feel free to send me something!\n"
)
while True: while True:
await sock.send_all(b"-> ") await sock.send_all(b"-> ")
data = await sock.receive(1024) data = await sock.receive(1024)
@ -49,9 +45,7 @@ async def handler(sock: AsyncSocket, client_address: tuple):
break break
elif data == b"exit\n": elif data == b"exit\n":
await sock.send_all(b"I'm dead dude\n") await sock.send_all(b"I'm dead dude\n")
raise TypeError( raise TypeError("Oh, no, I'm gonna die!") # This kills the entire application!
"Oh, no, I'm gonna die!"
) # This kills the entire application!
logging.info(f"Got: {data!r} from {address}") logging.info(f"Got: {data!r} from {address}")
await sock.send_all(b"Got: " + data) await sock.send_all(b"Got: " + data)
logging.info(f"Echoed back {data!r} to {address}") logging.info(f"Echoed back {data!r} to {address}")

View File

@ -19,9 +19,7 @@ async def main():
await pool.spawn(child) await pool.spawn(child)
await pool.spawn(child1) await pool.spawn(child1)
print("[main] Children spawned, awaiting completion") print("[main] Children spawned, awaiting completion")
print( print(f"[main] Children execution complete in {giambio.clock() - start:.2f} seconds")
f"[main] Children execution complete in {giambio.clock() - start:.2f} seconds"
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -16,9 +16,7 @@ async def main():
await child(20) # TODO: Broken await child(20) # TODO: Broken
except giambio.exceptions.TooSlowError: except giambio.exceptions.TooSlowError:
print("[main] One or more children have timed out!") print("[main] One or more children have timed out!")
print( print(f"[main] Children execution complete in {giambio.clock() - start:.2f} seconds")
f"[main] Children execution complete in {giambio.clock() - start:.2f} seconds"
)
if __name__ == "__main__": if __name__ == "__main__":