xiio

really simple async runtime
git clone https://git.ce9e.org/xiio.git

commit
e8447dd8daf816df9be346b112c65e57e538fcd5
parent
782851e16a4988987e94d22388efb84d2ab988ac
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2026-04-12 17:21
refactor: split into multiple files

Diffstat

M .github/workflows/main.yml 6 +++---
M pyproject.toml 5 +----
A tests/test_core.py 146 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
R tests.py -> tests/test_multiplex.py 157 +------------------------------------------------------------
A tests/utils.py 28 ++++++++++++++++++++++++++++
A xiio/__init__.py 10 ++++++++++
R xiio.py -> xiio/core.py 90 ------------------------------------------------------------
A xiio/multiplex.py 103 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

8 files changed, 293 insertions, 252 deletions


diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml

@@ -11,9 +11,9 @@ jobs:
   11    11     - uses: actions/checkout@v6
   12    12     - uses: actions/setup-python@v6
   13    13     - run: python3 -m pip install ruff ty coverage
   14    -1     - run: ruff check xiio.py tests.py
   15    -1     - run: ty check xiio.py
   16    -1     - run: python3 -m coverage run -m unittest
   -1    14     - run: ruff check xiio tests
   -1    15     - run: ty check xiio
   -1    16     - run: python3 -m coverage run -m unittest discover tests
   17    17     - run: python3 -m coverage report
   18    18   publish:
   19    19     needs: [test]

diff --git a/pyproject.toml b/pyproject.toml

@@ -15,9 +15,6 @@ authors = [
   15    15 [project.urls]
   16    16 Homepage = "https://github.com/xi/xiio"
   17    17 
   18    -1 [tool.setuptools]
   19    -1 py-modules = ["xiio"]
   20    -1 
   21    18 [tool.coverage.run]
   22    19 branch = true
   23    -1 include = ["xiio.py"]
   -1    20 include = ["xiio/**"]

diff --git a/tests/test_core.py b/tests/test_core.py

@@ -0,0 +1,146 @@
   -1     1 import os
   -1     2 import time
   -1     3 from unittest import mock
   -1     4 
   -1     5 import xiio
   -1     6 from tests.utils import XiioTestCase
   -1     7 from xiio.core import READ
   -1     8 from xiio.core import WRITE
   -1     9 from xiio.core import Condition
   -1    10 
   -1    11 
   -1    12 def interrupt_select(self):
   -1    13     timeout = self.time - time.monotonic()
   -1    14     if timeout > 0:
   -1    15         time.sleep(timeout / 2)
   -1    16         raise KeyboardInterrupt
   -1    17     return {}
   -1    18 
   -1    19 
   -1    20 class TestConditionCombine(XiioTestCase):
   -1    21     def test_time(self):
   -1    22         result = Condition.combine([
   -1    23             Condition(),
   -1    24             Condition(time=1),
   -1    25             Condition(time=3),
   -1    26             Condition(time=-2),
   -1    27         ])
   -1    28         self.assertEqual(result.time, -2)
   -1    29 
   -1    30     def test_futures(self):
   -1    31         f1 = xiio.Future()
   -1    32         f2 = xiio.Future()
   -1    33         _f3 = xiio.Future()
   -1    34 
   -1    35         result = Condition.combine([
   -1    36             Condition(),
   -1    37             Condition(futures={f1}),
   -1    38             Condition(futures={f1, f2}),
   -1    39         ])
   -1    40 
   -1    41         self.assertEqual(result.futures, {f1, f2})
   -1    42 
   -1    43     def test_files(self):
   -1    44         result = Condition.combine([
   -1    45             Condition(),
   -1    46             Condition(files={1: READ, 2: READ}),
   -1    47             Condition(files={1: WRITE}),
   -1    48         ])
   -1    49 
   -1    50         self.assertEqual(result.files, {
   -1    51             1: READ|WRITE,
   -1    52             2: READ,
   -1    53         })
   -1    54 
   -1    55 
   -1    56 class TestConditionFulfilled(XiioTestCase):
   -1    57     def test_files(self):
   -1    58         condition = Condition(files={1: READ})
   -1    59         self.assertTrue(condition.fulfilled({1: READ, 2: READ}))
   -1    60 
   -1    61     def test_files_wrong_mode(self):
   -1    62         condition = Condition(files={1: READ})
   -1    63         self.assertFalse(condition.fulfilled({1: WRITE, 2: READ}))
   -1    64 
   -1    65     def test_future_not_done(self):
   -1    66         future = xiio.Future()
   -1    67         condition = Condition(futures={future})
   -1    68         self.assertFalse(condition.fulfilled({}))
   -1    69 
   -1    70     def test_future_result(self):
   -1    71         future = xiio.Future()
   -1    72         future.set_result(1)
   -1    73         condition = Condition(futures={future})
   -1    74         self.assertTrue(condition.fulfilled({}))
   -1    75 
   -1    76     def test_future_exception(self):
   -1    77         future = xiio.Future()
   -1    78         future.set_exception(ValueError)
   -1    79         condition = Condition(futures={future})
   -1    80         self.assertTrue(condition.fulfilled({}))
   -1    81 
   -1    82 
   -1    83 class TestFuture(XiioTestCase):
   -1    84     async def test_set_result(self):
   -1    85         future = xiio.Future()
   -1    86         future.set_result('test')
   -1    87         result = await future
   -1    88         self.assertEqual(result, 'test')
   -1    89 
   -1    90     async def test_set_exception(self):
   -1    91         future = xiio.Future()
   -1    92         future.set_exception(TypeError)
   -1    93         with self.assertRaises(TypeError):
   -1    94             await future
   -1    95 
   -1    96 
   -1    97 class TestRun(XiioTestCase):
   -1    98     def test_sleep(self):
   -1    99         async def foo():
   -1   100             await xiio.sleep(0.1)
   -1   101             return 'Hello World'
   -1   102 
   -1   103         with self.assert_duration(0.1):
   -1   104             result = xiio.run(foo())
   -1   105         self.assertEqual(result, 'Hello World')
   -1   106 
   -1   107     def test_runs_cleanup_on_error_while_paused(self):
   -1   108         stack = []
   -1   109 
   -1   110         async def foo():
   -1   111             try:
   -1   112                 await xiio.sleep(0.1)
   -1   113             finally:
   -1   114                 stack.append(1)
   -1   115 
   -1   116         with mock.patch('xiio.core.Condition.select', new=interrupt_select):
   -1   117             with self.assertRaises(KeyboardInterrupt):
   -1   118                 xiio.run(foo())
   -1   119         self.assertEqual(stack, [1])
   -1   120 
   -1   121     def test_waits_for_cleanup(self):
   -1   122         async def foo():
   -1   123             try:
   -1   124                 raise ValueError
   -1   125             finally:
   -1   126                 await xiio.sleep(0.1)
   -1   127 
   -1   128         with self.assertRaises(ValueError):
   -1   129             with self.assert_duration(0.1):
   -1   130                 xiio.run(foo())
   -1   131 
   -1   132     def test_pipe(self):
   -1   133         async def foo():
   -1   134             r, w = os.pipe()
   -1   135             try:
   -1   136                 return await xiio.gather([
   -1   137                     xiio.read(r, 32),
   -1   138                     xiio.writeall(w, b'Hello World'),
   -1   139                 ])
   -1   140             finally:
   -1   141                 os.close(w)
   -1   142                 os.close(r)
   -1   143 
   -1   144         with self.assert_duration(0):
   -1   145             result = xiio.run(foo())
   -1   146         self.assertEqual(result, [b'Hello World', None])

diff --git a/tests.py b/tests/test_multiplex.py

@@ -1,12 +1,8 @@
    1    -1 import contextlib
    2    -1 import functools
    3    -1 import inspect
    4    -1 import os
    5     1 import time
    6    -1 import unittest
    7     2 from unittest import mock
    8     3 
    9     4 import xiio
   -1     5 from tests.utils import XiioTestCase
   10     6 
   11     7 
   12     8 async def return_later(seconds, value):
@@ -27,103 +23,6 @@ def interrupt_select(self):
   27    23     return {}
   28    24 
   29    25 
   30    -1 class XiioTestCase(unittest.TestCase):
   31    -1     def _callTestMethod(self, method):  # noqa
   32    -1         if not inspect.iscoroutinefunction(method):
   33    -1             return super()._callTestMethod(method)
   34    -1 
   35    -1         @functools.wraps(method)
   36    -1         def wrapper(*args, **kwargs):
   37    -1             xiio.run(method(*args, **kwargs))
   38    -1         return super()._callTestMethod(wrapper)
   39    -1 
   40    -1     @contextlib.contextmanager
   41    -1     def assert_duration(self, expected, *, places=2):
   42    -1         start = time.monotonic()
   43    -1         try:
   44    -1             yield
   45    -1         finally:
   46    -1             actual = time.monotonic() - start
   47    -1             self.assertAlmostEqual(actual, expected, places=places)
   48    -1 
   49    -1 
   50    -1 class TestConditionCombine(XiioTestCase):
   51    -1     def test_time(self):
   52    -1         result = xiio.Condition.combine([
   53    -1             xiio.Condition(),
   54    -1             xiio.Condition(time=1),
   55    -1             xiio.Condition(time=3),
   56    -1             xiio.Condition(time=-2),
   57    -1         ])
   58    -1         self.assertEqual(result.time, -2)
   59    -1 
   60    -1     def test_futures(self):
   61    -1         f1 = xiio.Future()
   62    -1         f2 = xiio.Future()
   63    -1         _f3 = xiio.Future()
   64    -1 
   65    -1         result = xiio.Condition.combine([
   66    -1             xiio.Condition(),
   67    -1             xiio.Condition(futures={f1}),
   68    -1             xiio.Condition(futures={f1, f2}),
   69    -1         ])
   70    -1 
   71    -1         self.assertEqual(result.futures, {f1, f2})
   72    -1 
   73    -1     def test_files(self):
   74    -1         result = xiio.Condition.combine([
   75    -1             xiio.Condition(),
   76    -1             xiio.Condition(files={1: xiio.READ, 2: xiio.READ}),
   77    -1             xiio.Condition(files={1: xiio.WRITE}),
   78    -1         ])
   79    -1 
   80    -1         self.assertEqual(result.files, {
   81    -1             1: xiio.READ|xiio.WRITE,
   82    -1             2: xiio.READ,
   83    -1         })
   84    -1 
   85    -1 
   86    -1 class TestConditionFulfilled(XiioTestCase):
   87    -1     def test_files(self):
   88    -1         condition = xiio.Condition(files={1: xiio.READ})
   89    -1         self.assertTrue(condition.fulfilled({1: xiio.READ, 2: xiio.READ}))
   90    -1 
   91    -1     def test_files_wrong_mode(self):
   92    -1         condition = xiio.Condition(files={1: xiio.READ})
   93    -1         self.assertFalse(condition.fulfilled({1: xiio.WRITE, 2: xiio.READ}))
   94    -1 
   95    -1     def test_future_not_done(self):
   96    -1         future = xiio.Future()
   97    -1         condition = xiio.Condition(futures={future})
   98    -1         self.assertFalse(condition.fulfilled({}))
   99    -1 
  100    -1     def test_future_result(self):
  101    -1         future = xiio.Future()
  102    -1         future.set_result(1)
  103    -1         condition = xiio.Condition(futures={future})
  104    -1         self.assertTrue(condition.fulfilled({}))
  105    -1 
  106    -1     def test_future_exception(self):
  107    -1         future = xiio.Future()
  108    -1         future.set_exception(ValueError)
  109    -1         condition = xiio.Condition(futures={future})
  110    -1         self.assertTrue(condition.fulfilled({}))
  111    -1 
  112    -1 
  113    -1 class TestFuture(XiioTestCase):
  114    -1     async def test_set_result(self):
  115    -1         future = xiio.Future()
  116    -1         future.set_result('test')
  117    -1         result = await future
  118    -1         self.assertEqual(result, 'test')
  119    -1 
  120    -1     async def test_set_exception(self):
  121    -1         future = xiio.Future()
  122    -1         future.set_exception(TypeError)
  123    -1         with self.assertRaises(TypeError):
  124    -1             await future
  125    -1 
  126    -1 
  127    26 class TestTaskGroup(XiioTestCase):
  128    27     async def test_add_tasks_while_running(self):
  129    28         async def set_result_later(seconds, future):
@@ -235,7 +134,7 @@ class TestGather(XiioTestCase):
  235   134             finally:
  236   135                 stack.append(1)
  237   136 
  238    -1         with mock.patch('xiio.Condition.select', new=interrupt_select):
   -1   137         with mock.patch('xiio.core.Condition.select', new=interrupt_select):
  239   138             with self.assertRaises(KeyboardInterrupt):
  240   139                 await xiio.gather([foo()])
  241   140         self.assertEqual(stack, [1])
@@ -267,55 +166,3 @@ class TestTimeout(XiioTestCase):
  267   166         with self.assert_duration(0.1):
  268   167             async with xiio.timeout(None):
  269   168                 await xiio.sleep(0.1)
  270    -1 
  271    -1 
  272    -1 class TestRun(XiioTestCase):
  273    -1     def test_sleep(self):
  274    -1         async def foo():
  275    -1             await xiio.sleep(0.1)
  276    -1             return 'Hello World'
  277    -1 
  278    -1         with self.assert_duration(0.1):
  279    -1             result = xiio.run(foo())
  280    -1         self.assertEqual(result, 'Hello World')
  281    -1 
  282    -1     def test_runs_cleanup_on_error_while_paused(self):
  283    -1         stack = []
  284    -1 
  285    -1         async def foo():
  286    -1             try:
  287    -1                 await xiio.sleep(0.1)
  288    -1             finally:
  289    -1                 stack.append(1)
  290    -1 
  291    -1         with mock.patch('xiio.Condition.select', new=interrupt_select):
  292    -1             with self.assertRaises(KeyboardInterrupt):
  293    -1                 xiio.run(foo())
  294    -1         self.assertEqual(stack, [1])
  295    -1 
  296    -1     def test_waits_for_cleanup(self):
  297    -1         async def foo():
  298    -1             try:
  299    -1                 raise ValueError
  300    -1             finally:
  301    -1                 await xiio.sleep(0.1)
  302    -1 
  303    -1         with self.assertRaises(ValueError):
  304    -1             with self.assert_duration(0.1):
  305    -1                 xiio.run(foo())
  306    -1 
  307    -1     def test_pipe(self):
  308    -1         async def foo():
  309    -1             r, w = os.pipe()
  310    -1             try:
  311    -1                 return await xiio.gather([
  312    -1                     xiio.read(r, 32),
  313    -1                     xiio.writeall(w, b'Hello World'),
  314    -1                 ])
  315    -1             finally:
  316    -1                 os.close(w)
  317    -1                 os.close(r)
  318    -1 
  319    -1         with self.assert_duration(0):
  320    -1             result = xiio.run(foo())
  321    -1         self.assertEqual(result, [b'Hello World', None])

diff --git a/tests/utils.py b/tests/utils.py

@@ -0,0 +1,28 @@
   -1     1 import contextlib
   -1     2 import functools
   -1     3 import inspect
   -1     4 import time
   -1     5 import unittest
   -1     6 
   -1     7 import xiio
   -1     8 
   -1     9 
   -1    10 class XiioTestCase(unittest.TestCase):
   -1    11     def _callTestMethod(self, method):  # noqa
   -1    12         if inspect.iscoroutinefunction(method):
   -1    13             @functools.wraps(method)
   -1    14             def wrapper(*args, **kwargs):
   -1    15                 xiio.run(method(*args, **kwargs))
   -1    16         else:
   -1    17             wrapper = method
   -1    18 
   -1    19         return super()._callTestMethod(wrapper)  # ty: ignore[unresolved-attribute]
   -1    20 
   -1    21     @contextlib.contextmanager
   -1    22     def assert_duration(self, expected, *, places=2):
   -1    23         start = time.monotonic()
   -1    24         try:
   -1    25             yield
   -1    26         finally:
   -1    27             actual = time.monotonic() - start
   -1    28             self.assertAlmostEqual(actual, expected, places=places)

diff --git a/xiio/__init__.py b/xiio/__init__.py

@@ -0,0 +1,10 @@
   -1     1 from .core import run  # noqa
   -1     2 from .core import sleep  # noqa
   -1     3 from .core import read  # noqa
   -1     4 from .core import write  # noqa
   -1     5 from .core import writeall  # noqa
   -1     6 from .core import Future  # noqa
   -1     7 from .core import CancelledError  # noqa
   -1     8 from .multiplex import TaskGroup  # noqa
   -1     9 from .multiplex import gather  # noqa
   -1    10 from .multiplex import timeout  # noqa

diff --git a/xiio.py b/xiio/core.py

@@ -1,10 +1,8 @@
    1    -1 import contextlib
    2     1 import math
    3     2 import os
    4     3 import selectors
    5     4 import time
    6     5 import typing
    7    -1 from collections.abc import AsyncGenerator
    8     6 from collections.abc import Coroutine
    9     7 from collections.abc import Generator
   10     8 from selectors import EVENT_READ as READ
@@ -157,94 +155,6 @@ class Task(typing.Generic[T]):
  157   155         self._condition = None
  158   156 
  159   157 
  160    -1 class TaskGroup(typing.Generic[T]):
  161    -1     def __init__(self) -> None:
  162    -1         self.tasks: list[Task[T]] = []
  163    -1         self.exc: BaseException | None = None
  164    -1 
  165    -1     def add_task(self, coro: Coro[T]) -> Task[T]:
  166    -1         task = Task(coro.__await__())
  167    -1         self.tasks.append(task)
  168    -1         return task
  169    -1 
  170    -1     def cancel(self, exc: BaseException) -> None:
  171    -1         if not self.exc:
  172    -1             self.exc = exc
  173    -1             for task in self.tasks:
  174    -1                 task.cancel()
  175    -1 
  176    -1     def __await__(self) -> Gen[None]:
  177    -1         while self.tasks:
  178    -1             try:
  179    -1                 state = yield Condition.combine(
  180    -1                     [task.condition for task in self.tasks]
  181    -1                 )
  182    -1             except BaseException as e:
  183    -1                 state = e
  184    -1 
  185    -1             for task in self.tasks[:]:
  186    -1                 try:
  187    -1                     task.resume(state)
  188    -1                 except StopIteration as e:
  189    -1                     self.tasks.remove(task)
  190    -1                     task.result = e.value
  191    -1                 except CancelledError:
  192    -1                     self.tasks.remove(task)
  193    -1                 except BaseException as e:
  194    -1                     self.tasks.remove(task)
  195    -1                     self.cancel(e)
  196    -1 
  197    -1     async def __aenter__(self) -> 'TaskGroup[T]':
  198    -1         parent_task = typing.cast(Task[T], await GetTaskCondition())
  199    -1         gen = parent_task.gen
  200    -1 
  201    -1         async def wrapper():
  202    -1             await Condition(time=-math.inf)
  203    -1             await self
  204    -1             parent_task.gen = gen
  205    -1             parent_task._condition = None
  206    -1             await Condition(time=-math.inf)
  207    -1 
  208    -1         self.tasks.append(Task(gen))
  209    -1         parent_task.gen = typing.cast(typing.Any, wrapper().__await__())
  210    -1         next(parent_task.gen)
  211    -1         await Condition(time=-math.inf)
  212    -1 
  213    -1         return self
  214    -1 
  215    -1     async def __aexit__(self, exc_type, exc: BaseException | None, traceback) -> None:
  216    -1         await ThrowCondition(exc or StopIteration())
  217    -1         if self.exc:
  218    -1             raise self.exc
  219    -1 
  220    -1 
  221    -1 async def gather(coros: list[Coro[T]]) -> list[T]:
  222    -1     async with TaskGroup() as tg:
  223    -1         tasks = [tg.add_task(coro) for coro in coros]
  224    -1     return [typing.cast(T, task.result) for task in tasks]
  225    -1 
  226    -1 
  227    -1 @contextlib.asynccontextmanager
  228    -1 async def timeout(
  229    -1     seconds: float | None, *, throw: bool = True
  230    -1 ) -> AsyncGenerator[None, None]:
  231    -1     if seconds is None:
  232    -1         yield
  233    -1     else:
  234    -1         async def _timeout() -> typing.NoReturn:
  235    -1             await sleep(seconds)
  236    -1             raise TimeoutError
  237    -1 
  238    -1         try:
  239    -1             async with TaskGroup() as tg:
  240    -1                 task = tg.add_task(_timeout())
  241    -1                 yield
  242    -1                 task.cancel()
  243    -1         except TimeoutError:
  244    -1             if throw:
  245    -1                 raise
  246    -1 
  247    -1 
  248   158 def run(coro: Coro[T]) -> T:
  249   159     task = Task(coro.__await__())
  250   160     try:

diff --git a/xiio/multiplex.py b/xiio/multiplex.py

@@ -0,0 +1,103 @@
   -1     1 import contextlib
   -1     2 import math
   -1     3 import typing
   -1     4 from collections.abc import AsyncGenerator
   -1     5 
   -1     6 from .core import CancelledError
   -1     7 from .core import Condition
   -1     8 from .core import Coro
   -1     9 from .core import Gen
   -1    10 from .core import GetTaskCondition
   -1    11 from .core import Task
   -1    12 from .core import ThrowCondition
   -1    13 from .core import sleep
   -1    14 
   -1    15 T = typing.TypeVar('T')
   -1    16 
   -1    17 
   -1    18 class TaskGroup(typing.Generic[T]):
   -1    19     def __init__(self) -> None:
   -1    20         self.tasks: list[Task[T]] = []
   -1    21         self.exc: BaseException | None = None
   -1    22 
   -1    23     def add_task(self, coro: Coro[T]) -> Task[T]:
   -1    24         task = Task(coro.__await__())
   -1    25         self.tasks.append(task)
   -1    26         return task
   -1    27 
   -1    28     def cancel(self, exc: BaseException) -> None:
   -1    29         if not self.exc:
   -1    30             self.exc = exc
   -1    31             for task in self.tasks:
   -1    32                 task.cancel()
   -1    33 
   -1    34     def __await__(self) -> Gen[None]:
   -1    35         while self.tasks:
   -1    36             try:
   -1    37                 state = yield Condition.combine(
   -1    38                     [task.condition for task in self.tasks]
   -1    39                 )
   -1    40             except BaseException as e:
   -1    41                 state = e
   -1    42 
   -1    43             for task in self.tasks[:]:
   -1    44                 try:
   -1    45                     task.resume(state)
   -1    46                 except StopIteration as e:
   -1    47                     self.tasks.remove(task)
   -1    48                     task.result = e.value
   -1    49                 except CancelledError:
   -1    50                     self.tasks.remove(task)
   -1    51                 except BaseException as e:
   -1    52                     self.tasks.remove(task)
   -1    53                     self.cancel(e)
   -1    54 
   -1    55     async def __aenter__(self) -> 'TaskGroup[T]':
   -1    56         parent_task = typing.cast(Task[T], await GetTaskCondition())
   -1    57         gen = parent_task.gen
   -1    58 
   -1    59         async def wrapper():
   -1    60             await Condition(time=-math.inf)
   -1    61             await self
   -1    62             parent_task.gen = gen
   -1    63             parent_task._condition = None
   -1    64             await Condition(time=-math.inf)
   -1    65 
   -1    66         self.tasks.append(Task(gen))
   -1    67         parent_task.gen = typing.cast(typing.Any, wrapper().__await__())
   -1    68         next(parent_task.gen)
   -1    69         await Condition(time=-math.inf)
   -1    70 
   -1    71         return self
   -1    72 
   -1    73     async def __aexit__(self, exc_type, exc: BaseException | None, traceback) -> None:
   -1    74         await ThrowCondition(exc or StopIteration())
   -1    75         if self.exc:
   -1    76             raise self.exc
   -1    77 
   -1    78 
   -1    79 async def gather(coros: list[Coro[T]]) -> list[T]:
   -1    80     async with TaskGroup() as tg:
   -1    81         tasks = [tg.add_task(coro) for coro in coros]
   -1    82     return [typing.cast(T, task.result) for task in tasks]
   -1    83 
   -1    84 
   -1    85 @contextlib.asynccontextmanager
   -1    86 async def timeout(
   -1    87     seconds: float | None, *, throw: bool = True
   -1    88 ) -> AsyncGenerator[None, None]:
   -1    89     if seconds is None:
   -1    90         yield
   -1    91     else:
   -1    92         async def _timeout() -> typing.NoReturn:
   -1    93             await sleep(seconds)
   -1    94             raise TimeoutError
   -1    95 
   -1    96         try:
   -1    97             async with TaskGroup() as tg:
   -1    98                 task = tg.add_task(_timeout())
   -1    99                 yield
   -1   100                 task.cancel()
   -1   101         except TimeoutError:
   -1   102             if throw:
   -1   103                 raise