From bc944ec565914939d3a9c8d8a7076464b375dd07 Mon Sep 17 00:00:00 2001 From: James Ward Date: Thu, 16 Nov 2023 18:55:52 -0500 Subject: [PATCH 1/7] extmod/modasyncio.c: Add Task methods to get tasks closer to CPython. Adds methods that are in CPython, such as `exception`, `result`, `get_coro`, `cancelled`, `add_done_callback`, and `remove_done_callback`. Also adds support for the unary hash so tasks may be collected in a python set. Signed-off-by: James Ward --- extmod/modasyncio.c | 160 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 159 insertions(+), 1 deletion(-) diff --git a/extmod/modasyncio.c b/extmod/modasyncio.c index a6a54eba8765a..73a814ec8b581 100644 --- a/extmod/modasyncio.c +++ b/extmod/modasyncio.c @@ -42,6 +42,12 @@ (task)->state == TASK_STATE_DONE_NOT_WAITED_ON \ || (task)->state == TASK_STATE_DONE_WAS_WAITED_ON) +#define IS_CANCELLED_ERROR(error) ( \ + mp_obj_is_subclass_fast( \ + MP_OBJ_FROM_PTR(mp_obj_get_type(error)), \ + mp_obj_dict_get(asyncio_context, MP_OBJ_NEW_QSTR(MP_QSTR_CancelledError)) \ + )) + typedef struct _mp_obj_task_t { mp_pairheap_t pairheap; mp_obj_t coro; @@ -179,6 +185,124 @@ STATIC mp_obj_t task_done(mp_obj_t self_in) { } STATIC MP_DEFINE_CONST_FUN_OBJ_1(task_done_obj, task_done); +STATIC mp_obj_t task_add_done_callback(mp_obj_t self_in, mp_obj_t callback) { + assert(mp_obj_is_callable(callback)); + mp_obj_task_t *self = MP_OBJ_TO_PTR(self_in); + + if (TASK_IS_DONE(self)) { + // In CPython the callbacks are not immediately called and are instead + // called by the event loop. However, MicroPython's event loop doesn't + // support `call_soon` to handle callback processing. + // + // Because of this, it's close enough to call the callback immediately. + + mp_call_function_2(callback, self_in, self->data); + return mp_const_none; + } + + if (self->state != mp_const_true) { + // Tasks SHOULD support more than one callback per CPython but to reduce + // the surface area of this change tasks can currently only support one. + mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT(">1 callback unsupported")); + } + + self->state = callback; + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_2(task_add_done_callback_obj, task_add_done_callback); + +STATIC mp_obj_t task_remove_done_callback(mp_obj_t self_in, mp_obj_t callback) { + mp_obj_task_t *self = MP_OBJ_TO_PTR(self_in); + + if (callback != self->state) { + // If the callback isn't a match we can count this as removing 0 callbacks + return mp_obj_new_int(0); + } + + self->state = mp_const_true; + return mp_obj_new_int(1); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_2(task_remove_done_callback_obj, task_remove_done_callback); + +STATIC mp_obj_t task_get_coro(mp_obj_t self_in) { + mp_obj_task_t *self = MP_OBJ_TO_PTR(self_in); + return MP_OBJ_FROM_PTR(self->coro); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(task_get_coro_obj, task_get_coro); + +STATIC mp_obj_t task_set_exception(mp_obj_t self_in, const mp_obj_t arg) { + mp_raise_msg(&mp_type_RuntimeError, NULL); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_2(task_set_exception_obj, task_set_exception); + +STATIC mp_obj_t task_exception(mp_obj_t self_in) { + mp_obj_task_t *self = MP_OBJ_TO_PTR(self_in); + + if (!TASK_IS_DONE(self)) { + mp_obj_t error_type = mp_obj_dict_get(asyncio_context, MP_OBJ_NEW_QSTR(MP_QSTR_InvalidStateError)); + nlr_raise(mp_make_raise_obj(error_type)); + } + + // If the exception is a cancelled error then we should raise it + if (IS_CANCELLED_ERROR(self->data)) { + nlr_raise(mp_make_raise_obj(self->data)); + } + + // If it's a StopIteration we should should return none + if (mp_obj_is_subclass_fast(MP_OBJ_FROM_PTR(mp_obj_get_type(self->data)), MP_OBJ_FROM_PTR(&mp_type_StopIteration))) { + return mp_const_none; + } + + if (!mp_obj_is_exception_instance(self->data)) { + return mp_const_none; + } + + return self->data; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(task_exception_obj, task_exception); + +STATIC mp_obj_t task_set_result(mp_obj_t self_in, const mp_obj_t arg) { + mp_raise_msg(&mp_type_RuntimeError, NULL); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_2(task_set_result_obj, task_set_result); + +STATIC mp_obj_t task_result(mp_obj_t self_in) { + mp_obj_task_t *self = MP_OBJ_TO_PTR(self_in); + + if (!TASK_IS_DONE(self)) { + mp_obj_t error_type = mp_obj_dict_get(asyncio_context, MP_OBJ_NEW_QSTR(MP_QSTR_InvalidStateError)); + nlr_raise(mp_make_raise_obj(error_type)); + } + + // If `exception()` returns anything we raise that + mp_obj_t exception_obj = task_exception(self_in); + if (exception_obj != mp_const_none) { + nlr_raise(mp_make_raise_obj(exception_obj)); + } + + // If not StopIteration, bail early + if (!mp_obj_is_subclass_fast(MP_OBJ_FROM_PTR(mp_obj_get_type(self->data)), MP_OBJ_FROM_PTR(&mp_type_StopIteration))) { + return mp_const_none; + } + + return mp_obj_exception_get_value(self->data); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(task_result_obj, task_result); + +STATIC mp_obj_t task_cancelled(mp_obj_t self_in) { + mp_obj_task_t *self = MP_OBJ_TO_PTR(self_in); + + if (!TASK_IS_DONE(self)) { + // If task isn't done it can't possibly be cancelled, and would instead + // be considered "cancelling" even if a cancel was requested until it + // has fully completed. + return mp_obj_new_bool(false); + } + + return mp_obj_new_bool(IS_CANCELLED_ERROR(self->data)); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(task_cancelled_obj, task_cancelled); + STATIC mp_obj_t task_cancel(mp_obj_t self_in) { mp_obj_task_t *self = MP_OBJ_TO_PTR(self_in); // Check if task is already finished. @@ -242,6 +366,30 @@ STATIC void task_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { dest[1] = self_in; } else if (attr == MP_QSTR_ph_key) { dest[0] = self->ph_key; + } else if (attr == MP_QSTR_add_done_callback) { + dest[0] = MP_OBJ_FROM_PTR(&task_add_done_callback_obj); + dest[1] = self_in; + } else if (attr == MP_QSTR_remove_done_callback) { + dest[0] = MP_OBJ_FROM_PTR(&task_remove_done_callback_obj); + dest[1] = self_in; + } else if (attr == MP_QSTR_get_coro) { + dest[0] = MP_OBJ_FROM_PTR(&task_get_coro_obj); + dest[1] = self_in; + } else if (attr == MP_QSTR_set_result) { + dest[0] = MP_OBJ_FROM_PTR(&task_set_result_obj); + dest[1] = self_in; + } else if (attr == MP_QSTR_result) { + dest[0] = MP_OBJ_FROM_PTR(&task_result_obj); + dest[1] = self_in; + } else if (attr == MP_QSTR_set_exception) { + dest[0] = MP_OBJ_FROM_PTR(&task_set_exception_obj); + dest[1] = self_in; + } else if (attr == MP_QSTR_exception) { + dest[0] = MP_OBJ_FROM_PTR(&task_exception_obj); + dest[1] = self_in; + } else if (attr == MP_QSTR_cancelled) { + dest[0] = MP_OBJ_FROM_PTR(&task_cancelled_obj); + dest[1] = self_in; } } else if (dest[1] != MP_OBJ_NULL) { // Store @@ -255,6 +403,15 @@ STATIC void task_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { } } +STATIC mp_obj_t task_unary_op(mp_unary_op_t op, mp_obj_t o_in) { + switch (op) { + case MP_UNARY_OP_HASH: + return MP_OBJ_NEW_SMALL_INT((mp_uint_t)o_in); + default: + return MP_OBJ_NULL; // op not supported + } +} + STATIC mp_obj_t task_getiter(mp_obj_t self_in, mp_obj_iter_buf_t *iter_buf) { (void)iter_buf; mp_obj_task_t *self = MP_OBJ_TO_PTR(self_in); @@ -298,7 +455,8 @@ STATIC MP_DEFINE_CONST_OBJ_TYPE( MP_TYPE_FLAG_ITER_IS_CUSTOM, make_new, task_make_new, attr, task_attr, - iter, &task_getiter_iternext + iter, &task_getiter_iternext, + unary_op, task_unary_op ); /******************************************************************************/ From df39e51693bb38e4a9433ab3c91c76edf60539d7 Mon Sep 17 00:00:00 2001 From: James Ward Date: Thu, 16 Nov 2023 19:08:42 -0500 Subject: [PATCH 2/7] extmod/asyncio: Add cpython task methods. Signed-off-by: James Ward --- extmod/asyncio/core.py | 4 +++ extmod/asyncio/task.py | 79 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/extmod/asyncio/core.py b/extmod/asyncio/core.py index 214cc52f45f53..676f1431d527c 100644 --- a/extmod/asyncio/core.py +++ b/extmod/asyncio/core.py @@ -23,6 +23,10 @@ class TimeoutError(Exception): pass +class InvalidStateError(Exception): + pass + + # Used when calling Loop.call_exception_handler _exc_context = {"message": "Task exception wasn't retrieved", "exception": None, "future": None} diff --git a/extmod/asyncio/task.py b/extmod/asyncio/task.py index 30be2170eb0a7..05db7c3aedc8a 100644 --- a/extmod/asyncio/task.py +++ b/extmod/asyncio/task.py @@ -175,3 +175,82 @@ def cancel(self): core._task_queue.push(self) self.data = core.CancelledError return True + + def add_done_callback(self, callback): + if not self.state: + callback(self, self.data) + + if self.state is not True: + raise RuntimeError("Tasks only support one done callback.") + + self.state = callback + + def remove_done_callback(self, callback): + if self.state is not callback: + return 0 + + self.state = True + return 1 + + def set_result(self, result): + raise RuntimeError() + + def result(self): + """ + Return the result of the Task. + If the Task is done, the result of the wrapped coroutine is returned (or if the coroutine raised an exception, that exception is re-raised.) + If the Task has been cancelled, this method raises a CancelledError exception. + If the Task’s result isn’t yet available, this method raises a InvalidStateError exception. + """ + if not self.done(): + raise core.InvalidStateError() + + exception = self.exception() + + if exception is not None: + raise exception + + if not isinstance(self.data, StopIteration): + # If this isn't the case then we're in an odd state. + return None + + return self.data.value + + def set_exception(self, exception): + raise RuntimeError() + + def exception(self): + """ + Return the exception that was set on this Task. + The exception (or None if no exception was set) is returned only if the Task is done. + If the Task has been cancelled, this method raises a CancelledError exception. + If the Task isn’t done yet, this method raises an InvalidStateError exception. + """ + if not self.done(): + raise core.InvalidStateError() + + if isinstance(self.data, core.CancelledError): + raise self.data + + if isinstance(self.data, StopIteration): + # If the data is a stop iteration we can assume this + # was a successful run rather than any possible exception + return None + + if not isinstance(self.data, BaseException): + # If the data is not any type of exception we can treat it as + # something else we don't understand but not an exception. + return None + + return self.data + + def cancelled(self) -> bool: + """ + Return True if the Task is cancelled. + The Task is cancelled when the cancellation was requested with cancel() and + the wrapped coroutine propagated the CancelledError exception thrown into it. + """ + if not self.done(): + return False + + return isinstance(self.data, core.CancelledError) From e1ef6ed0072ed4fbbc3e0137c6b403239eedd994 Mon Sep 17 00:00:00 2001 From: James Ward Date: Thu, 16 Nov 2023 19:12:33 -0500 Subject: [PATCH 3/7] tests/extmod: Add asyncio tests for new task features. Signed-off-by: James Ward --- .../extmod/asyncio_task_add_done_callback.py | 54 +++++++++++++++ .../asyncio_task_add_done_callback.py.exp | 12 ++++ tests/extmod/asyncio_task_cancelled.py | 54 +++++++++++++++ tests/extmod/asyncio_task_cancelled.py.exp | 10 +++ tests/extmod/asyncio_task_exception.py | 64 +++++++++++++++++ tests/extmod/asyncio_task_exception.py.exp | 10 +++ tests/extmod/asyncio_task_get_coro.py | 28 ++++++++ tests/extmod/asyncio_task_get_coro.py.exp | 4 ++ tests/extmod/asyncio_task_hash.py | 39 +++++++++++ tests/extmod/asyncio_task_hash.py.exp | 8 +++ .../asyncio_task_remove_done_callback.py | 59 ++++++++++++++++ .../asyncio_task_remove_done_callback.py.exp | 10 +++ tests/extmod/asyncio_task_result.py | 69 +++++++++++++++++++ tests/extmod/asyncio_task_result.py.exp | 10 +++ tests/extmod/asyncio_task_set_exception.py | 24 +++++++ .../extmod/asyncio_task_set_exception.py.exp | 2 + tests/extmod/asyncio_task_set_result.py | 24 +++++++ tests/extmod/asyncio_task_set_result.py.exp | 2 + 18 files changed, 483 insertions(+) create mode 100644 tests/extmod/asyncio_task_add_done_callback.py create mode 100644 tests/extmod/asyncio_task_add_done_callback.py.exp create mode 100644 tests/extmod/asyncio_task_cancelled.py create mode 100644 tests/extmod/asyncio_task_cancelled.py.exp create mode 100644 tests/extmod/asyncio_task_exception.py create mode 100644 tests/extmod/asyncio_task_exception.py.exp create mode 100644 tests/extmod/asyncio_task_get_coro.py create mode 100644 tests/extmod/asyncio_task_get_coro.py.exp create mode 100644 tests/extmod/asyncio_task_hash.py create mode 100644 tests/extmod/asyncio_task_hash.py.exp create mode 100644 tests/extmod/asyncio_task_remove_done_callback.py create mode 100644 tests/extmod/asyncio_task_remove_done_callback.py.exp create mode 100644 tests/extmod/asyncio_task_result.py create mode 100644 tests/extmod/asyncio_task_result.py.exp create mode 100644 tests/extmod/asyncio_task_set_exception.py create mode 100644 tests/extmod/asyncio_task_set_exception.py.exp create mode 100644 tests/extmod/asyncio_task_set_result.py create mode 100644 tests/extmod/asyncio_task_set_result.py.exp diff --git a/tests/extmod/asyncio_task_add_done_callback.py b/tests/extmod/asyncio_task_add_done_callback.py new file mode 100644 index 0000000000000..405b330ea38fa --- /dev/null +++ b/tests/extmod/asyncio_task_add_done_callback.py @@ -0,0 +1,54 @@ +# Test the Task.add_done_callback() method + +try: + import asyncio +except ImportError: + print("SKIP") + raise SystemExit + + +async def task(t, exc=None): + if t >= 0: + await asyncio.sleep(t) + if exc: + raise exc + + +def done_callback(t, er): + print("done", repr(t), repr(er)) + + +async def main(): + # Tasks that aren't done only execute done callback after finishing + print("=" * 10) + t = asyncio.create_task(task(-1)) + t.add_done_callback(done_callback) + print("Waiting for task to complete") + await asyncio.sleep(0) + print("Task has completed") + + # Task that are done run the callback immediately + print("=" * 10) + t = asyncio.create_task(task(-1)) + await asyncio.sleep(0) + print("Task has completed") + t.add_done_callback(done_callback) + print("Callback Added") + + # Task that starts, runs and finishes without an exception should return None + print("=" * 10) + t = asyncio.create_task(task(0.01)) + t.add_done_callback(done_callback) + try: + t.add_done_callback(done_callback) + except RuntimeError as e: + print("Second call to add_done_callback emits error:", repr(e)) + + # Task that raises immediately should still run done callback + print("=" * 10) + t = asyncio.create_task(task(-1, ValueError)) + t.add_done_callback(done_callback) + await asyncio.sleep(0) + + +asyncio.run(main()) diff --git a/tests/extmod/asyncio_task_add_done_callback.py.exp b/tests/extmod/asyncio_task_add_done_callback.py.exp new file mode 100644 index 0000000000000..0e7252ee4503f --- /dev/null +++ b/tests/extmod/asyncio_task_add_done_callback.py.exp @@ -0,0 +1,12 @@ +========== +Waiting for task to complete +done StopIteration() +Task has completed +========== +Task has completed +done StopIteration() +Callback Added +========== +Second call to add_done_callback emits error: RuntimeError('>1 callback unsupported',) +========== +done ValueError() diff --git a/tests/extmod/asyncio_task_cancelled.py b/tests/extmod/asyncio_task_cancelled.py new file mode 100644 index 0000000000000..0addfd442eaee --- /dev/null +++ b/tests/extmod/asyncio_task_cancelled.py @@ -0,0 +1,54 @@ +# Test the `Task.cancelled` method + +try: + import asyncio +except ImportError: + print("SKIP") + raise SystemExit + + +async def task(t): + await asyncio.sleep(t) + + +async def main(): + # Cancel task immediately doesn't mark the task as cancelled + print("=" * 10) + t = asyncio.create_task(task(2)) + t.cancel() + print("Expecting task to not be cancelled because it is not done:", t.cancelled()) + + # Cancel task immediately and wait for cancellation to complete + print("=" * 10) + t = asyncio.create_task(task(2)) + t.cancel() + await asyncio.sleep(0) + print("Expecting Task to be Cancelled:", t.cancelled()) + + # Cancel task and wait for cancellation to complete + print("=" * 10) + t = asyncio.create_task(task(2)) + await asyncio.sleep(0.01) + t.cancel() + await asyncio.sleep(0) + print("Expecting Task to be Cancelled:", t.cancelled()) + + # Cancel task multiple times after it has started + print("=" * 10) + t = asyncio.create_task(task(2)) + await asyncio.sleep(0.01) + for _ in range(4): + t.cancel() + await asyncio.sleep(0.01) + + print("Expecting Task to be Cancelled:", t.cancelled()) + + # Cancel task after it has finished + print("=" * 10) + t = asyncio.create_task(task(0.01)) + await asyncio.sleep(0.05) + t.cancel() + print("Expecting task to not be Cancelled:", t.cancelled()) + + +asyncio.run(main()) diff --git a/tests/extmod/asyncio_task_cancelled.py.exp b/tests/extmod/asyncio_task_cancelled.py.exp new file mode 100644 index 0000000000000..96783a426bb13 --- /dev/null +++ b/tests/extmod/asyncio_task_cancelled.py.exp @@ -0,0 +1,10 @@ +========== +Expecting task to not be cancelled because it is not done: False +========== +Expecting Task to be Cancelled: True +========== +Expecting Task to be Cancelled: True +========== +Expecting Task to be Cancelled: True +========== +Expecting task to not be Cancelled: False diff --git a/tests/extmod/asyncio_task_exception.py b/tests/extmod/asyncio_task_exception.py new file mode 100644 index 0000000000000..9353059e2d365 --- /dev/null +++ b/tests/extmod/asyncio_task_exception.py @@ -0,0 +1,64 @@ +# Test the Task.exception() method + +try: + import asyncio +except ImportError: + print("SKIP") + raise SystemExit + + +async def task(t, exc=None): + if t >= 0: + await asyncio.sleep(t) + if exc: + raise exc + + +async def main(): + # Task that is not done yet raises an InvalidStateError + print("=" * 10) + t = asyncio.create_task(task(1)) + await asyncio.sleep(0) + try: + t.exception() + assert False, "Should not get here" + except Exception as e: + print("Tasks that aren't done yet raise an InvalidStateError:", repr(e)) + + # Task that is cancelled raises CancelledError + print("=" * 10) + t = asyncio.create_task(task(1)) + t.cancel() + await asyncio.sleep(0) + try: + print(repr(t.exception())) + print(t.cancelled()) + assert False, "Should not get here" + except asyncio.CancelledError as e: + print("Cancelled tasks cannot retrieve exception:", repr(e)) + + # Task that starts, runs and finishes without an exception should return None + print("=" * 10) + t = asyncio.create_task(task(0.01)) + await t + print("None when no exception:", t.exception()) + + # Task that raises immediately should return that exception + print("=" * 10) + t = asyncio.create_task(task(-1, ValueError)) + try: + await t + assert False, "Should not get here" + except ValueError as e: + pass + print("Returned Exception:", repr(t.exception())) + + # Task returns `none` when somehow an exception isn't in data + print("=" * 10) + t = asyncio.create_task(task(-1)) + await t + t.data = "Example" + print(t.exception()) + + +asyncio.run(main()) diff --git a/tests/extmod/asyncio_task_exception.py.exp b/tests/extmod/asyncio_task_exception.py.exp new file mode 100644 index 0000000000000..50057be6fbf54 --- /dev/null +++ b/tests/extmod/asyncio_task_exception.py.exp @@ -0,0 +1,10 @@ +========== +Tasks that aren't done yet raise an InvalidStateError: InvalidStateError() +========== +Cancelled tasks cannot retrieve exception: CancelledError() +========== +None when no exception: None +========== +Returned Exception: ValueError() +========== +None diff --git a/tests/extmod/asyncio_task_get_coro.py b/tests/extmod/asyncio_task_get_coro.py new file mode 100644 index 0000000000000..1afc03da662c3 --- /dev/null +++ b/tests/extmod/asyncio_task_get_coro.py @@ -0,0 +1,28 @@ +# Test the `Task.get_coro()` method + +try: + import asyncio +except ImportError: + print("SKIP") + raise SystemExit + + +async def action(): + pass + + +async def main(): + # Check that the coro we include is the same coro we get back + print("=" * 10) + + coro = action() + t = asyncio.create_task(coro) + print(t.get_coro() == coro) + + # Check that the coro prop matches the get_coro() result + print("=" * 10) + t = asyncio.create_task(action()) + print(t.get_coro() == t.coro) + + +asyncio.run(main()) diff --git a/tests/extmod/asyncio_task_get_coro.py.exp b/tests/extmod/asyncio_task_get_coro.py.exp new file mode 100644 index 0000000000000..fc81328ed3d0f --- /dev/null +++ b/tests/extmod/asyncio_task_get_coro.py.exp @@ -0,0 +1,4 @@ +========== +True +========== +True diff --git a/tests/extmod/asyncio_task_hash.py b/tests/extmod/asyncio_task_hash.py new file mode 100644 index 0000000000000..d376d5b019637 --- /dev/null +++ b/tests/extmod/asyncio_task_hash.py @@ -0,0 +1,39 @@ +# Test hash unary operator for a Task + +try: + import asyncio +except ImportError: + print("SKIP") + raise SystemExit + + +async def task(): + pass + + +async def main(): + # Confirm that the hash is an int + print("=" * 10) + t1 = asyncio.create_task(task()) + t2 = asyncio.create_task(task()) + print(type(hash(t2))) + print(type(hash(t1))) + + # Check that two tasks don't have the same hash + print("=" * 10) + t1 = asyncio.create_task(task()) + t2 = asyncio.create_task(task()) + print(hash(t1) != hash(t2)) + + # Add tasks to a set + print("=" * 10) + t1 = asyncio.create_task(task()) + t2 = asyncio.create_task(task()) + + tasks = set() + tasks.add(t1) + print(t1 in tasks) + print(t2 in tasks) + + +asyncio.run(main()) diff --git a/tests/extmod/asyncio_task_hash.py.exp b/tests/extmod/asyncio_task_hash.py.exp new file mode 100644 index 0000000000000..8745c5069b7b6 --- /dev/null +++ b/tests/extmod/asyncio_task_hash.py.exp @@ -0,0 +1,8 @@ +========== + + +========== +True +========== +True +False diff --git a/tests/extmod/asyncio_task_remove_done_callback.py b/tests/extmod/asyncio_task_remove_done_callback.py new file mode 100644 index 0000000000000..88dba945b29fe --- /dev/null +++ b/tests/extmod/asyncio_task_remove_done_callback.py @@ -0,0 +1,59 @@ +# Test the Task.remove_done_callback() method + +try: + import asyncio +except ImportError: + print("SKIP") + raise SystemExit + + +async def task(t, exc=None): + if t >= 0: + await asyncio.sleep(t) + if exc: + raise exc + + +def done_callback(): + print("done") + + +def done_callback_2(): + print("done 2") + + +async def main(): + # Removing a callback returns 0 when no callbacks have been set + print("=" * 10) + t = asyncio.create_task(task(1)) + print("Returns 0 when no done callback has been set:", t.remove_done_callback(done_callback)) + + # Done callback removal only works once + print("=" * 10) + t = asyncio.create_task(task(1)) + t.add_done_callback(done_callback) + print( + "Returns 1 when a callback matches and is removed:", t.remove_done_callback(done_callback) + ) + print( + "Returns 0 on second attempt to remove the callback:", + t.remove_done_callback(done_callback), + ) + + # Only removes done callback when match + print("=" * 10) + t = asyncio.create_task(task(0.01)) + t.add_done_callback(done_callback) + print("Returns 0 when done callbacks don't match:", t.remove_done_callback(done_callback_2)) + + # A removed done callback does not execute + print("=" * 10) + t = asyncio.create_task(task(-1)) + t.add_done_callback(done_callback) + t.remove_done_callback(done_callback) + print("Waiting for task to complete") + await t + print("Task completed") + + +asyncio.run(main()) diff --git a/tests/extmod/asyncio_task_remove_done_callback.py.exp b/tests/extmod/asyncio_task_remove_done_callback.py.exp new file mode 100644 index 0000000000000..50c0b63dd0675 --- /dev/null +++ b/tests/extmod/asyncio_task_remove_done_callback.py.exp @@ -0,0 +1,10 @@ +========== +Returns 0 when no done callback has been set: 0 +========== +Returns 1 when a callback matches and is removed: 1 +Returns 0 on second attempt to remove the callback: 0 +========== +Returns 0 when done callbacks don't match: 0 +========== +Waiting for task to complete +Task completed diff --git a/tests/extmod/asyncio_task_result.py b/tests/extmod/asyncio_task_result.py new file mode 100644 index 0000000000000..a0cc3ccab89ee --- /dev/null +++ b/tests/extmod/asyncio_task_result.py @@ -0,0 +1,69 @@ +# Test the Task.result() method + +try: + import asyncio +except ImportError: + print("SKIP") + raise SystemExit + + +async def task(t, exc=None, ret=None): + if t >= 0: + await asyncio.sleep(t) + if exc: + raise exc + return ret + + +async def main(): + # Task that is not done yet raises an InvalidStateError + print("=" * 10) + t = asyncio.create_task(task(1)) + await asyncio.sleep(0) + try: + t.result() + assert False, "Should not get here" + except Exception as e: + print("InvalidStateError if still running:", repr(e)) + + # Task that is cancelled raises CancelledError + print("=" * 10) + t = asyncio.create_task(task(1)) + t.cancel() + await asyncio.sleep(0) + try: + t.result() + assert False, "Should not get here" + except asyncio.CancelledError as e: + print("CancelledError when retrieving result from cancelled task:", repr(e)) + + # Task that raises immediately should raise that exception when calling result + print("=" * 10) + t = asyncio.create_task(task(-1, ValueError)) + try: + await t + assert False, "Should not get here" + except ValueError as e: + pass + + try: + t.result() + assert False, "Should not get here" + except ValueError as e: + print("Error raised when result is attempted on task with error:", repr(e)) + + # Task that starts, runs and finishes without an exception or value should return None + print("=" * 10) + t = asyncio.create_task(task(0.01)) + await t + print("Empty Result should be None:", t.result()) + assert t.result() is None + + # Task that starts, runs and finishes without exception should return result + print("=" * 10) + t = asyncio.create_task(task(0.01, None, "hello world")) + await t + print("Happy path, result is returned:", t.result()) + + +asyncio.run(main()) diff --git a/tests/extmod/asyncio_task_result.py.exp b/tests/extmod/asyncio_task_result.py.exp new file mode 100644 index 0000000000000..e67d28c876ad3 --- /dev/null +++ b/tests/extmod/asyncio_task_result.py.exp @@ -0,0 +1,10 @@ +========== +InvalidStateError if still running: InvalidStateError() +========== +CancelledError when retrieving result from cancelled task: CancelledError() +========== +Error raised when result is attempted on task with error: ValueError() +========== +Empty Result should be None: None +========== +Happy path, result is returned: hello world diff --git a/tests/extmod/asyncio_task_set_exception.py b/tests/extmod/asyncio_task_set_exception.py new file mode 100644 index 0000000000000..f5a83416753c1 --- /dev/null +++ b/tests/extmod/asyncio_task_set_exception.py @@ -0,0 +1,24 @@ +# Test the Task.set_exception() method + +try: + import asyncio +except ImportError: + print("SKIP") + raise SystemExit + + +async def task(): + pass + + +async def main(): + # Task.set_exception must raise an error + print("=" * 10) + t = asyncio.create_task(task()) + try: + t.set_exception(RuntimeError("example")) + except Exception as e: + print("Task.set_exception is not callable:", repr(e)) + + +asyncio.run(main()) diff --git a/tests/extmod/asyncio_task_set_exception.py.exp b/tests/extmod/asyncio_task_set_exception.py.exp new file mode 100644 index 0000000000000..0ef2dedb6c830 --- /dev/null +++ b/tests/extmod/asyncio_task_set_exception.py.exp @@ -0,0 +1,2 @@ +========== +Task.set_exception is not callable: RuntimeError() diff --git a/tests/extmod/asyncio_task_set_result.py b/tests/extmod/asyncio_task_set_result.py new file mode 100644 index 0000000000000..0cae633bec016 --- /dev/null +++ b/tests/extmod/asyncio_task_set_result.py @@ -0,0 +1,24 @@ +# Test the Task.set_result() method + +try: + import asyncio +except ImportError: + print("SKIP") + raise SystemExit + + +async def task(): + pass + + +async def main(): + # Task.set_exception must raise an error + print("=" * 10) + t = asyncio.create_task(task()) + try: + t.set_result(None) + except Exception as e: + print("Task.set_result is not callable:", repr(e)) + + +asyncio.run(main()) diff --git a/tests/extmod/asyncio_task_set_result.py.exp b/tests/extmod/asyncio_task_set_result.py.exp new file mode 100644 index 0000000000000..20f2923ef26e5 --- /dev/null +++ b/tests/extmod/asyncio_task_set_result.py.exp @@ -0,0 +1,2 @@ +========== +Task.set_result is not callable: RuntimeError() From 5ecb8596d02c8ebf30f22d0030a74c9c6b4aa011 Mon Sep 17 00:00:00 2001 From: James Ward Date: Sun, 19 Nov 2023 17:19:24 -0500 Subject: [PATCH 4/7] tests/extmod: Drop set_result and set_exception tests. Signed-off-by: James Ward --- tests/extmod/asyncio_task_set_exception.py | 24 ------------------- .../extmod/asyncio_task_set_exception.py.exp | 2 -- tests/extmod/asyncio_task_set_result.py | 24 ------------------- tests/extmod/asyncio_task_set_result.py.exp | 2 -- 4 files changed, 52 deletions(-) delete mode 100644 tests/extmod/asyncio_task_set_exception.py delete mode 100644 tests/extmod/asyncio_task_set_exception.py.exp delete mode 100644 tests/extmod/asyncio_task_set_result.py delete mode 100644 tests/extmod/asyncio_task_set_result.py.exp diff --git a/tests/extmod/asyncio_task_set_exception.py b/tests/extmod/asyncio_task_set_exception.py deleted file mode 100644 index f5a83416753c1..0000000000000 --- a/tests/extmod/asyncio_task_set_exception.py +++ /dev/null @@ -1,24 +0,0 @@ -# Test the Task.set_exception() method - -try: - import asyncio -except ImportError: - print("SKIP") - raise SystemExit - - -async def task(): - pass - - -async def main(): - # Task.set_exception must raise an error - print("=" * 10) - t = asyncio.create_task(task()) - try: - t.set_exception(RuntimeError("example")) - except Exception as e: - print("Task.set_exception is not callable:", repr(e)) - - -asyncio.run(main()) diff --git a/tests/extmod/asyncio_task_set_exception.py.exp b/tests/extmod/asyncio_task_set_exception.py.exp deleted file mode 100644 index 0ef2dedb6c830..0000000000000 --- a/tests/extmod/asyncio_task_set_exception.py.exp +++ /dev/null @@ -1,2 +0,0 @@ -========== -Task.set_exception is not callable: RuntimeError() diff --git a/tests/extmod/asyncio_task_set_result.py b/tests/extmod/asyncio_task_set_result.py deleted file mode 100644 index 0cae633bec016..0000000000000 --- a/tests/extmod/asyncio_task_set_result.py +++ /dev/null @@ -1,24 +0,0 @@ -# Test the Task.set_result() method - -try: - import asyncio -except ImportError: - print("SKIP") - raise SystemExit - - -async def task(): - pass - - -async def main(): - # Task.set_exception must raise an error - print("=" * 10) - t = asyncio.create_task(task()) - try: - t.set_result(None) - except Exception as e: - print("Task.set_result is not callable:", repr(e)) - - -asyncio.run(main()) diff --git a/tests/extmod/asyncio_task_set_result.py.exp b/tests/extmod/asyncio_task_set_result.py.exp deleted file mode 100644 index 20f2923ef26e5..0000000000000 --- a/tests/extmod/asyncio_task_set_result.py.exp +++ /dev/null @@ -1,2 +0,0 @@ -========== -Task.set_result is not callable: RuntimeError() From 92092f0e0cd162cec0cd69366139dec00c114a6a Mon Sep 17 00:00:00 2001 From: James Ward Date: Sun, 19 Nov 2023 17:17:32 -0500 Subject: [PATCH 5/7] extmod/modasyncio.c: Drop set_result and set_exception. Signed-off-by: James Ward --- extmod/modasyncio.c | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/extmod/modasyncio.c b/extmod/modasyncio.c index 73a814ec8b581..62ffaace81f3a 100644 --- a/extmod/modasyncio.c +++ b/extmod/modasyncio.c @@ -230,11 +230,6 @@ STATIC mp_obj_t task_get_coro(mp_obj_t self_in) { } STATIC MP_DEFINE_CONST_FUN_OBJ_1(task_get_coro_obj, task_get_coro); -STATIC mp_obj_t task_set_exception(mp_obj_t self_in, const mp_obj_t arg) { - mp_raise_msg(&mp_type_RuntimeError, NULL); -} -STATIC MP_DEFINE_CONST_FUN_OBJ_2(task_set_exception_obj, task_set_exception); - STATIC mp_obj_t task_exception(mp_obj_t self_in) { mp_obj_task_t *self = MP_OBJ_TO_PTR(self_in); @@ -261,11 +256,6 @@ STATIC mp_obj_t task_exception(mp_obj_t self_in) { } STATIC MP_DEFINE_CONST_FUN_OBJ_1(task_exception_obj, task_exception); -STATIC mp_obj_t task_set_result(mp_obj_t self_in, const mp_obj_t arg) { - mp_raise_msg(&mp_type_RuntimeError, NULL); -} -STATIC MP_DEFINE_CONST_FUN_OBJ_2(task_set_result_obj, task_set_result); - STATIC mp_obj_t task_result(mp_obj_t self_in) { mp_obj_task_t *self = MP_OBJ_TO_PTR(self_in); @@ -375,15 +365,9 @@ STATIC void task_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { } else if (attr == MP_QSTR_get_coro) { dest[0] = MP_OBJ_FROM_PTR(&task_get_coro_obj); dest[1] = self_in; - } else if (attr == MP_QSTR_set_result) { - dest[0] = MP_OBJ_FROM_PTR(&task_set_result_obj); - dest[1] = self_in; } else if (attr == MP_QSTR_result) { dest[0] = MP_OBJ_FROM_PTR(&task_result_obj); dest[1] = self_in; - } else if (attr == MP_QSTR_set_exception) { - dest[0] = MP_OBJ_FROM_PTR(&task_set_exception_obj); - dest[1] = self_in; } else if (attr == MP_QSTR_exception) { dest[0] = MP_OBJ_FROM_PTR(&task_exception_obj); dest[1] = self_in; From d23edc34ef20566572350b1204262821c3b660c8 Mon Sep 17 00:00:00 2001 From: James Ward Date: Sun, 19 Nov 2023 17:18:56 -0500 Subject: [PATCH 6/7] extmod/asyncio: Drop set_exception and set_result. Signed-off-by: James Ward --- extmod/asyncio/task.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/extmod/asyncio/task.py b/extmod/asyncio/task.py index 05db7c3aedc8a..993b5dd8f0dcb 100644 --- a/extmod/asyncio/task.py +++ b/extmod/asyncio/task.py @@ -192,9 +192,6 @@ def remove_done_callback(self, callback): self.state = True return 1 - def set_result(self, result): - raise RuntimeError() - def result(self): """ Return the result of the Task. @@ -216,9 +213,6 @@ def result(self): return self.data.value - def set_exception(self, exception): - raise RuntimeError() - def exception(self): """ Return the exception that was set on this Task. From 8d260d8eda0ffd0ca8429bd298d3d2d8bccf41d3 Mon Sep 17 00:00:00 2001 From: James Ward Date: Wed, 22 Nov 2023 23:41:19 -0500 Subject: [PATCH 7/7] extmod/modasyncio.c: Rely on the generic hash during runtime. Signed-off-by: James Ward --- extmod/modasyncio.c | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/extmod/modasyncio.c b/extmod/modasyncio.c index 62ffaace81f3a..87350ebe2eaaa 100644 --- a/extmod/modasyncio.c +++ b/extmod/modasyncio.c @@ -387,15 +387,6 @@ STATIC void task_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { } } -STATIC mp_obj_t task_unary_op(mp_unary_op_t op, mp_obj_t o_in) { - switch (op) { - case MP_UNARY_OP_HASH: - return MP_OBJ_NEW_SMALL_INT((mp_uint_t)o_in); - default: - return MP_OBJ_NULL; // op not supported - } -} - STATIC mp_obj_t task_getiter(mp_obj_t self_in, mp_obj_iter_buf_t *iter_buf) { (void)iter_buf; mp_obj_task_t *self = MP_OBJ_TO_PTR(self_in); @@ -439,8 +430,7 @@ STATIC MP_DEFINE_CONST_OBJ_TYPE( MP_TYPE_FLAG_ITER_IS_CUSTOM, make_new, task_make_new, attr, task_attr, - iter, &task_getiter_iternext, - unary_op, task_unary_op + iter, &task_getiter_iternext ); /******************************************************************************/ pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy