Skip to content

Commit

Permalink
py/objgenerator: Allow to pend an exception for next execution.
Browse files Browse the repository at this point in the history
This implements .pend_throw(exc) method, which sets up an exception to be
triggered on the next call to generator's .__next__() or .send() method.
This is unlike .throw(), which immediately starts to execute the generator
to process the exception. This effectively adds Future-like capabilities
to generator protocol (exception will be raised in the future).

The need for such a method arised to implement uasyncio wait_for() function
efficiently (its behavior is clearly "Future" like, and normally would
require to introduce an expensive Future wrapper around all native
couroutines, like upstream asyncio does).

py/objgenerator: pend_throw: Return previous pended value.

This effectively allows to store an additional value (not necessary an
exception) in a coroutine while it's not being executed. uasyncio has
exactly this usecase: to mark a coro waiting in I/O queue (and thus
not executed in the normal scheduling queue), for the purpose of
implementing wait_for() function (cancellation of such waiting coro
by a timeout).
  • Loading branch information
pfalcon committed Nov 10, 2017
1 parent 579b864 commit 0e64f56
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 3 deletions.
7 changes: 7 additions & 0 deletions py/mpconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,13 @@ typedef double mp_float_t;
#define MICROPY_PY_ASYNC_AWAIT (1)
#endif

// Non-standard .pend_throw() method for generators, allowing for
// Future-like behavior with respect to exception handling (useful
// for implement async schedulers).
#ifndef MICROPY_PY_GENERATOR_PEND_THROW
#define MICROPY_PY_GENERATOR_PEND_THROW (1)
#endif

// Issue a warning when comparing str and bytes objects
#ifndef MICROPY_PY_STR_BYTES_CMP_WARN
#define MICROPY_PY_STR_BYTES_CMP_WARN (0)
Expand Down
30 changes: 28 additions & 2 deletions py/objgenerator.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* The MIT License (MIT)
*
* Copyright (c) 2013, 2014 Damien P. George
* Copyright (c) 2014 Paul Sokolovsky
* Copyright (c) 2014-2017 Paul Sokolovsky
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
Expand Down Expand Up @@ -104,7 +104,16 @@ mp_vm_return_kind_t mp_obj_gen_resume(mp_obj_t self_in, mp_obj_t send_value, mp_
mp_raise_TypeError("can't send non-None value to a just-started generator");
}
} else {
*self->code_state.sp = send_value;
#if MICROPY_PY_GENERATOR_PEND_THROW
// If exception is pending (set using .pend_throw()), process it now.
if (*self->code_state.sp != mp_const_none) {
throw_value = *self->code_state.sp;
*self->code_state.sp = MP_OBJ_NULL;
} else
#endif
{
*self->code_state.sp = send_value;
}
}
mp_obj_dict_t *old_globals = mp_globals_get();
mp_globals_set(self->globals);
Expand All @@ -125,6 +134,9 @@ mp_vm_return_kind_t mp_obj_gen_resume(mp_obj_t self_in, mp_obj_t send_value, mp_

case MP_VM_RETURN_YIELD:
*ret_val = *self->code_state.sp;
#if MICROPY_PY_GENERATOR_PEND_THROW
*self->code_state.sp = mp_const_none;
#endif
if (*ret_val == MP_OBJ_STOP_ITERATION) {
self->code_state.ip = 0;
}
Expand Down Expand Up @@ -222,10 +234,24 @@ STATIC mp_obj_t gen_instance_close(mp_obj_t self_in) {

STATIC MP_DEFINE_CONST_FUN_OBJ_1(gen_instance_close_obj, gen_instance_close);

STATIC mp_obj_t gen_instance_pend_throw(mp_obj_t self_in, mp_obj_t exc_in) {
mp_obj_gen_instance_t *self = MP_OBJ_TO_PTR(self_in);
if (self->code_state.sp == self->code_state.state - 1) {
mp_raise_TypeError("can't pend throw to just-started generator");
}
mp_obj_t prev = *self->code_state.sp;
*self->code_state.sp = exc_in;
return prev;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(gen_instance_pend_throw_obj, gen_instance_pend_throw);

STATIC const mp_rom_map_elem_t gen_instance_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR_close), MP_ROM_PTR(&gen_instance_close_obj) },
{ MP_ROM_QSTR(MP_QSTR_send), MP_ROM_PTR(&gen_instance_send_obj) },
{ MP_ROM_QSTR(MP_QSTR_throw), MP_ROM_PTR(&gen_instance_throw_obj) },
#if MICROPY_PY_GENERATOR_PEND_THROW
{ MP_ROM_QSTR(MP_QSTR_pend_throw), MP_ROM_PTR(&gen_instance_pend_throw_obj) },
#endif
};

STATIC MP_DEFINE_CONST_DICT(gen_instance_locals_dict, gen_instance_locals_dict_table);
Expand Down
26 changes: 26 additions & 0 deletions tests/basics/generator_pend_throw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
def gen():
i = 0
while 1:
yield i
i += 1

g = gen()

try:
g.pend_throw
except AttributeError:
print("SKIP")
raise SystemExit


print(next(g))
print(next(g))
g.pend_throw(ValueError())

v = None
try:
v = next(g)
except Exception as e:
print("raised", repr(e))

print("ret was:", v)
4 changes: 4 additions & 0 deletions tests/basics/generator_pend_throw.py.exp
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
0
1
raised ValueError()
ret was: None
2 changes: 1 addition & 1 deletion tests/run-tests
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ def run_tests(pyb, tests, args, base_path="."):
# Some tests are known to fail with native emitter
# Remove them from the below when they work
if args.emit == 'native':
skip_tests.update({'basics/%s.py' % t for t in 'gen_yield_from gen_yield_from_close gen_yield_from_ducktype gen_yield_from_exc gen_yield_from_iter gen_yield_from_send gen_yield_from_stopped gen_yield_from_throw gen_yield_from_throw2 gen_yield_from_throw3 generator1 generator2 generator_args generator_close generator_closure generator_exc generator_return generator_send'.split()}) # require yield
skip_tests.update({'basics/%s.py' % t for t in 'gen_yield_from gen_yield_from_close gen_yield_from_ducktype gen_yield_from_exc gen_yield_from_iter gen_yield_from_send gen_yield_from_stopped gen_yield_from_throw gen_yield_from_throw2 gen_yield_from_throw3 generator1 generator2 generator_args generator_close generator_closure generator_exc generator_pend_throw generator_return generator_send'.split()}) # require yield
skip_tests.update({'basics/%s.py' % t for t in 'bytes_gen class_store_class globals_del string_join'.split()}) # require yield
skip_tests.update({'basics/async_%s.py' % t for t in 'def await await2 for for2 with with2'.split()}) # require yield
skip_tests.update({'basics/%s.py' % t for t in 'try_reraise try_reraise2'.split()}) # require raise_varargs
Expand Down

0 comments on commit 0e64f56

Please sign in to comment.