Skip to content

Commit 6364401

Browse files
committed
py/objgenerator: Allow to pend an exception for next execution.
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).
1 parent f4ed2df commit 6364401

5 files changed

Lines changed: 68 additions & 3 deletions

File tree

py/mpconfig.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,15 @@ typedef double mp_float_t;
706706
#define MICROPY_PY_ASYNC_AWAIT (1)
707707
#endif
708708

709+
// Non-standard .pend_throw() method for generators, allowing for
710+
// Future-like behavior with respect to exception handling: an
711+
// exception set with .pend_throw() will activate on the next call
712+
// to generator's .send() or .__next__(). (This is useful to implement
713+
// async schedulers.)
714+
#ifndef MICROPY_PY_GENERATOR_PEND_THROW
715+
#define MICROPY_PY_GENERATOR_PEND_THROW (1)
716+
#endif
717+
709718
// Issue a warning when comparing str and bytes objects
710719
#ifndef MICROPY_PY_STR_BYTES_CMP_WARN
711720
#define MICROPY_PY_STR_BYTES_CMP_WARN (0)

py/objgenerator.c

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* The MIT License (MIT)
55
*
66
* Copyright (c) 2013, 2014 Damien P. George
7-
* Copyright (c) 2014 Paul Sokolovsky
7+
* Copyright (c) 2014-2017 Paul Sokolovsky
88
*
99
* Permission is hereby granted, free of charge, to any person obtaining a copy
1010
* of this software and associated documentation files (the "Software"), to deal
@@ -104,7 +104,16 @@ mp_vm_return_kind_t mp_obj_gen_resume(mp_obj_t self_in, mp_obj_t send_value, mp_
104104
mp_raise_TypeError("can't send non-None value to a just-started generator");
105105
}
106106
} else {
107-
*self->code_state.sp = send_value;
107+
#if MICROPY_PY_GENERATOR_PEND_THROW
108+
// If exception is pending (set using .pend_throw()), process it now.
109+
if (*self->code_state.sp != mp_const_none) {
110+
throw_value = *self->code_state.sp;
111+
*self->code_state.sp = MP_OBJ_NULL;
112+
} else
113+
#endif
114+
{
115+
*self->code_state.sp = send_value;
116+
}
108117
}
109118
mp_obj_dict_t *old_globals = mp_globals_get();
110119
mp_globals_set(self->globals);
@@ -125,6 +134,9 @@ mp_vm_return_kind_t mp_obj_gen_resume(mp_obj_t self_in, mp_obj_t send_value, mp_
125134

126135
case MP_VM_RETURN_YIELD:
127136
*ret_val = *self->code_state.sp;
137+
#if MICROPY_PY_GENERATOR_PEND_THROW
138+
*self->code_state.sp = mp_const_none;
139+
#endif
128140
break;
129141

130142
case MP_VM_RETURN_EXCEPTION: {
@@ -219,10 +231,24 @@ STATIC mp_obj_t gen_instance_close(mp_obj_t self_in) {
219231

220232
STATIC MP_DEFINE_CONST_FUN_OBJ_1(gen_instance_close_obj, gen_instance_close);
221233

234+
STATIC mp_obj_t gen_instance_pend_throw(mp_obj_t self_in, mp_obj_t exc_in) {
235+
mp_obj_gen_instance_t *self = MP_OBJ_TO_PTR(self_in);
236+
if (self->code_state.sp == self->code_state.state - 1) {
237+
mp_raise_TypeError("can't pend throw to just-started generator");
238+
}
239+
mp_obj_t prev = *self->code_state.sp;
240+
*self->code_state.sp = exc_in;
241+
return prev;
242+
}
243+
STATIC MP_DEFINE_CONST_FUN_OBJ_2(gen_instance_pend_throw_obj, gen_instance_pend_throw);
244+
222245
STATIC const mp_rom_map_elem_t gen_instance_locals_dict_table[] = {
223246
{ MP_ROM_QSTR(MP_QSTR_close), MP_ROM_PTR(&gen_instance_close_obj) },
224247
{ MP_ROM_QSTR(MP_QSTR_send), MP_ROM_PTR(&gen_instance_send_obj) },
225248
{ MP_ROM_QSTR(MP_QSTR_throw), MP_ROM_PTR(&gen_instance_throw_obj) },
249+
#if MICROPY_PY_GENERATOR_PEND_THROW
250+
{ MP_ROM_QSTR(MP_QSTR_pend_throw), MP_ROM_PTR(&gen_instance_pend_throw_obj) },
251+
#endif
226252
};
227253

228254
STATIC MP_DEFINE_CONST_DICT(gen_instance_locals_dict, gen_instance_locals_dict_table);
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
def gen():
2+
i = 0
3+
while 1:
4+
yield i
5+
i += 1
6+
7+
g = gen()
8+
9+
try:
10+
g.pend_throw
11+
except AttributeError:
12+
print("SKIP")
13+
raise SystemExit
14+
15+
16+
print(next(g))
17+
print(next(g))
18+
g.pend_throw(ValueError())
19+
20+
v = None
21+
try:
22+
v = next(g)
23+
except Exception as e:
24+
print("raised", repr(e))
25+
26+
print("ret was:", v)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
0
2+
1
3+
raised ValueError()
4+
ret was: None

tests/run-tests

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ def run_tests(pyb, tests, args, base_path="."):
337337
# Some tests are known to fail with native emitter
338338
# Remove them from the below when they work
339339
if args.emit == 'native':
340-
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
340+
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
341341
skip_tests.update({'basics/%s.py' % t for t in 'bytes_gen class_store_class globals_del string_join'.split()}) # require yield
342342
skip_tests.update({'basics/async_%s.py' % t for t in 'def await await2 for for2 with with2'.split()}) # require yield
343343
skip_tests.update({'basics/%s.py' % t for t in 'try_reraise try_reraise2'.split()}) # require raise_varargs

0 commit comments

Comments
 (0)