py/objgenerator: Implement PEP479, StopIteration convs to RuntimeError.

This commit implements PEP479 which disallows raising StopIteration inside
a generator to signal that it should be finished.  Instead, the generator
should simply return when it is complete.

See https://www.python.org/dev/peps/pep-0479/ for details.
This commit is contained in:
Damien George 2018-09-14 00:44:06 +10:00
parent 17f7c683d2
commit 3f6ffe059f
12 changed files with 63 additions and 98 deletions

View File

@ -145,6 +145,10 @@ mp_vm_return_kind_t mp_obj_gen_resume(mp_obj_t self_in, mp_obj_t send_value, mp_
size_t n_state = mp_decode_uint_value(self->code_state.fun_bc->bytecode); size_t n_state = mp_decode_uint_value(self->code_state.fun_bc->bytecode);
self->code_state.ip = 0; self->code_state.ip = 0;
*ret_val = self->code_state.state[n_state - 1]; *ret_val = self->code_state.state[n_state - 1];
// PEP479: if StopIteration is raised inside a generator it is replaced with RuntimeError
if (mp_obj_is_subclass_fast(MP_OBJ_FROM_PTR(mp_obj_get_type(*ret_val)), MP_OBJ_FROM_PTR(&mp_type_StopIteration))) {
*ret_val = mp_obj_new_exception_msg(&mp_type_RuntimeError, "generator raised StopIteration");
}
break; break;
} }
} }
@ -168,15 +172,6 @@ STATIC mp_obj_t gen_resume_and_raise(mp_obj_t self_in, mp_obj_t send_value, mp_o
return ret; return ret;
case MP_VM_RETURN_EXCEPTION: case MP_VM_RETURN_EXCEPTION:
// TODO: Optimization of returning MP_OBJ_STOP_ITERATION is really part
// of mp_iternext() protocol, but this function is called by other methods
// too, which may not handled MP_OBJ_STOP_ITERATION.
if (mp_obj_is_subclass_fast(MP_OBJ_FROM_PTR(mp_obj_get_type(ret)), MP_OBJ_FROM_PTR(&mp_type_StopIteration))) {
mp_obj_t val = mp_obj_exception_get_value(ret);
if (val == mp_const_none) {
return MP_OBJ_STOP_ITERATION;
}
}
nlr_raise(ret); nlr_raise(ret);
} }
} }
@ -216,11 +211,10 @@ STATIC mp_obj_t gen_instance_close(mp_obj_t self_in) {
case MP_VM_RETURN_YIELD: case MP_VM_RETURN_YIELD:
mp_raise_msg(&mp_type_RuntimeError, "generator ignored GeneratorExit"); mp_raise_msg(&mp_type_RuntimeError, "generator ignored GeneratorExit");
// Swallow StopIteration & GeneratorExit (== successful close), and re-raise any other // Swallow GeneratorExit (== successful close), and re-raise any other
case MP_VM_RETURN_EXCEPTION: case MP_VM_RETURN_EXCEPTION:
// ret should always be an instance of an exception class // ret should always be an instance of an exception class
if (mp_obj_is_subclass_fast(MP_OBJ_FROM_PTR(mp_obj_get_type(ret)), MP_OBJ_FROM_PTR(&mp_type_GeneratorExit)) || if (mp_obj_is_subclass_fast(MP_OBJ_FROM_PTR(mp_obj_get_type(ret)), MP_OBJ_FROM_PTR(&mp_type_GeneratorExit))) {
mp_obj_is_subclass_fast(MP_OBJ_FROM_PTR(mp_obj_get_type(ret)), MP_OBJ_FROM_PTR(&mp_type_StopIteration))) {
return mp_const_none; return mp_const_none;
} }
nlr_raise(ret); nlr_raise(ret);

View File

@ -13,34 +13,6 @@ g = gen2()
print(list(g)) print(list(g))
# Like above, but terminate subgen using StopIteration
def gen3():
yield 1
yield 2
raise StopIteration
def gen4():
print("here1")
print((yield from gen3()))
print("here2")
g = gen4()
print(list(g))
# Like above, but terminate subgen using StopIteration with value
def gen5():
yield 1
yield 2
raise StopIteration(123)
def gen6():
print("here1")
print((yield from gen5()))
print("here2")
g = gen6()
print(list(g))
# StopIteration from within a Python function, within a native iterator (map), within a yield from # StopIteration from within a Python function, within a native iterator (map), within a yield from
def gen7(x): def gen7(x):
if x < 3: if x < 3:

View File

@ -1,14 +0,0 @@
here1
3
here2
[1, 2]
here1
None
here2
[1, 2]
here1
123
here2
[1, 2]
444
[0, 1, 2]

View File

@ -55,14 +55,14 @@ except StopIteration:
# Yet another variation - leaf generator gets GeneratorExit, # Yet another variation - leaf generator gets GeneratorExit,
# but raises StopIteration instead. This still should close chain properly. # and reraises a new GeneratorExit. This still should close chain properly.
def gen5(): def gen5():
yield 1 yield 1
try: try:
yield 2 yield 2
except GeneratorExit: except GeneratorExit:
print("leaf caught GeneratorExit and raised StopIteration instead") print("leaf caught GeneratorExit and reraised GeneratorExit")
raise StopIteration(123) raise GeneratorExit(123)
yield 3 yield 3
yield 4 yield 4

View File

@ -1,20 +0,0 @@
-1
1
StopIteration
-1
1
2
leaf caught GeneratorExit and swallowed it
delegating caught GeneratorExit
StopIteration
-1
1
2
leaf caught GeneratorExit and raised StopIteration instead
delegating caught GeneratorExit
StopIteration
123
RuntimeError
0
1
close

View File

@ -25,6 +25,20 @@ def gen3():
g3 = gen3() g3 = gen3()
print(next(g3)) print(next(g3))
try: try:
g3.throw(StopIteration) g3.throw(KeyError)
except KeyError:
print('got KeyError from downstream!')
# case where a thrown exception is caught and stops the generator
def gen4():
try:
yield 1
yield 2
except:
pass
g4 = gen4()
print(next(g4))
try:
g4.throw(ValueError)
except StopIteration: except StopIteration:
print('got StopIteration from downstream!') print('got StopIteration')

View File

@ -1,6 +0,0 @@
1
got ValueError from upstream!
str1
got TypeError from downstream!
123
got StopIteration from downstream!

View File

@ -31,13 +31,14 @@ except StopIteration:
print("StopIteration") print("StopIteration")
# Throwing StopIteration in response to close() is ok # Throwing GeneratorExit in response to close() is ok
def gen2(): def gen2():
try: try:
yield 1 yield 1
yield 2 yield 2
except: except:
raise StopIteration print('raising GeneratorExit')
raise GeneratorExit
g = gen2() g = gen2()
next(g) next(g)

View File

@ -1,10 +0,0 @@
None
StopIteration
1
None
StopIteration
[1, 2]
None
StopIteration
None
ValueError

View File

@ -0,0 +1,29 @@
# tests for correct PEP479 behaviour (introduced in Python 3.5)
# basic case: StopIteration is converted into a RuntimeError
def gen():
yield 1
raise StopIteration
g = gen()
print(next(g))
try:
next(g)
except RuntimeError:
print('RuntimeError')
# trying to continue a failed generator now raises StopIteration
try:
next(g)
except StopIteration:
print('StopIteration')
# throwing a StopIteration which is uncaught will be converted into a RuntimeError
def gen():
yield 1
yield 2
g = gen()
print(next(g))
try:
g.throw(StopIteration)
except RuntimeError:
print('RuntimeError')

View File

@ -0,0 +1,5 @@
1
RuntimeError
StopIteration
1
RuntimeError

View File

@ -352,7 +352,7 @@ def run_tests(pyb, tests, args, base_path="."):
# Some tests are known to fail with native emitter # Some tests are known to fail with native emitter
# Remove them from the below when they work # Remove them from the below when they work
if args.emit == 'native': 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_executing 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_name generator_pend_throw 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_executing 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_name generator_pend_throw generator_return generator_send generator_pep479'.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/%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 with_break with_return'.split()}) # require yield skip_tests.update({'basics/async_%s.py' % t for t in 'def await await2 for for2 with with2 with_break with_return'.split()}) # require yield
skip_tests.update({'basics/%s.py' % t for t in 'try_reraise try_reraise2'.split()}) # require raise_varargs skip_tests.update({'basics/%s.py' % t for t in 'try_reraise try_reraise2'.split()}) # require raise_varargs