From 578b96a45acc7c3da451bc167cf6244107411e70 Mon Sep 17 00:00:00 2001 From: Stefan Behnel <stefan_ml@behnel.de> Date: Sun, 18 Mar 2012 11:09:36 +0100 Subject: [PATCH] implementation of PEP 380 (yield from) --- Cython/Compiler/ExprNodes.py | 63 +- Cython/Compiler/Nodes.py | 9 + Cython/Compiler/ParseTreeTransforms.py | 6 +- Cython/Compiler/Parsing.py | 11 +- Cython/Utility/Generator.c | 350 ++++++-- runtests.py | 1 + tests/errors/e_generators.pyx | 4 +- tests/run/yield_from_pep380.pyx | 1008 ++++++++++++++++++++++++ 8 files changed, 1393 insertions(+), 59 deletions(-) create mode 100644 tests/run/yield_from_pep380.pyx diff --git a/Cython/Compiler/ExprNodes.py b/Cython/Compiler/ExprNodes.py index cd4f09e6f..8803b03a6 100755 --- a/Cython/Compiler/ExprNodes.py +++ b/Cython/Compiler/ExprNodes.py @@ -6501,10 +6501,12 @@ class YieldExprNode(ExprNode): # arg ExprNode the value to return from the generator # label_name string name of the C label used for this yield # label_num integer yield label number + # is_yield_from boolean is a YieldFromExprNode to delegate to another generator subexprs = ['arg'] type = py_object_type label_num = 0 + is_yield_from = False def analyse_types(self, env): if not self.label_num: @@ -6513,11 +6515,12 @@ class YieldExprNode(ExprNode): if self.arg is not None: self.arg.analyse_types(env) if not self.arg.type.is_pyobject: - self.arg = self.arg.coerce_to_pyobject(env) + self.coerce_yield_argument(env) + + def coerce_yield_argument(self, env): + self.arg = self.arg.coerce_to_pyobject(env) def generate_evaluation_code(self, code): - self.label_name = code.new_label('resume_from_yield') - code.use_label(self.label_name) if self.arg: self.arg.generate_evaluation_code(code) self.arg.make_owned_reference(code) @@ -6526,10 +6529,19 @@ class YieldExprNode(ExprNode): Naming.retval_cname, self.arg.result_as(py_object_type))) self.arg.generate_post_assignment_code(code) - #self.arg.generate_disposal_code(code) self.arg.free_temps(code) else: code.put_init_to_py_none(Naming.retval_cname, py_object_type) + self.generate_yield_code(code) + + def generate_yield_code(self, code): + """ + Generate the code to return the argument in 'Naming.retval_cname' + and to continue at the yield label. + """ + self.label_name = code.new_label('resume_from_yield') + code.use_label(self.label_name) + saved = [] code.funcstate.closure_temps.reset() for cname, type, manage_ref in code.funcstate.temps_in_use(): @@ -6545,6 +6557,7 @@ class YieldExprNode(ExprNode): code.putln("%s->resume_label = %d;" % ( Naming.generator_cname, self.label_num)) code.putln("return %s;" % Naming.retval_cname); + code.put_label(self.label_name) for cname, save_cname, type in saved: code.putln('%s = %s->%s;' % (cname, Naming.cur_scope_cname, save_cname)) @@ -6562,6 +6575,48 @@ class YieldExprNode(ExprNode): code.putln(code.error_goto_if_null(Naming.sent_value_cname, self.pos)) +class YieldFromExprNode(YieldExprNode): + # "yield from GEN" expression + is_yield_from = True + + def coerce_yield_argument(self, env): + if not self.arg.type.is_string: + # FIXME: support C arrays and C++ iterators? + error(self.pos, "yielding from non-Python object not supported") + self.arg = self.arg.coerce_to_pyobject(env) + + def generate_evaluation_code(self, code): + code.globalstate.use_utility_code(UtilityCode.load_cached("YieldFrom", "Generator.c")) + + self.arg.generate_evaluation_code(code) + self.arg.make_owned_reference(code) + code.put_xgiveref(self.arg.result()) + code.putln("%s = __Pyx_Generator_Yield_From(%s, %s);" % ( + Naming.retval_cname, + Naming.generator_cname, + self.arg.result_as(py_object_type))) + self.arg.generate_post_assignment_code(code) # reference was stolen + self.arg.free_temps(code) + code.put_xgotref(Naming.retval_cname) + + code.putln("if (likely(%s)) {" % Naming.retval_cname) + self.generate_yield_code(code) + code.putln("} else {") + # either error or sub-generator has normally terminated: return value => node result + if self.result_is_used: + # YieldExprNode has allocated the result temp for us + code.putln("if (__Pyx_PyGen_FetchStopIterationValue(&%s) < 0) %s" % ( + self.result(), + code.error_goto(self.pos))) + else: + code.putln("PyObject* exc_type = PyErr_Occurred();") + code.putln("if (exc_type) {") + code.putln("if (!PyErr_GivenExceptionMatches(exc_type, PyExc_StopIteration)) %s" % + code.error_goto(self.pos)) + code.putln("PyErr_Clear();") + code.putln("}") + code.putln("}") + class GlobalsExprNode(AtomicExprNode): type = dict_type is_temp = 1 diff --git a/Cython/Compiler/Nodes.py b/Cython/Compiler/Nodes.py index bc5a73c0e..c639b88d8 100644 --- a/Cython/Compiler/Nodes.py +++ b/Cython/Compiler/Nodes.py @@ -5297,9 +5297,11 @@ class ReturnStatNode(StatNode): # # value ExprNode or None # return_type PyrexType + # in_generator return inside of generator => raise StopIteration child_attrs = ["value"] is_terminator = True + in_generator = False # Whether we are in a parallel section in_parallel = False @@ -5349,6 +5351,13 @@ class ReturnStatNode(StatNode): rhs=self.value, code=code, have_gil=self.in_nogil_context) + elif self.in_generator: + # return value == raise StopIteration(value), but uncatchable + code.putln( + "%s = NULL; PyErr_SetObject(PyExc_StopIteration, %s);" % ( + Naming.retval_cname, + self.value.result_as(self.return_type))) + self.value.generate_disposal_code(code) else: self.value.make_owned_reference(code) code.putln( diff --git a/Cython/Compiler/ParseTreeTransforms.py b/Cython/Compiler/ParseTreeTransforms.py index 6e48b145d..2c22f8b98 100644 --- a/Cython/Compiler/ParseTreeTransforms.py +++ b/Cython/Compiler/ParseTreeTransforms.py @@ -2025,16 +2025,12 @@ class YieldNodeCollector(TreeVisitor): return self.visitchildren(node) def visit_YieldExprNode(self, node): - if self.has_return_value: - error(node.pos, "'yield' outside function") self.yields.append(node) self.visitchildren(node) def visit_ReturnStatNode(self, node): if node.value: self.has_return_value = True - if self.yields: - error(node.pos, "'return' with argument inside generator") self.returns.append(node) def visit_ClassDefNode(self, node): @@ -2071,6 +2067,8 @@ class MarkClosureVisitor(CythonTransform): return node for i, yield_expr in enumerate(collector.yields): yield_expr.label_num = i + 1 + for retnode in collector.returns: + retnode.in_generator = True gbody = Nodes.GeneratorBodyDefNode( pos=node.pos, name=node.name, body=node.body) diff --git a/Cython/Compiler/Parsing.py b/Cython/Compiler/Parsing.py index 07d8f2a73..a7bca9c5b 100644 --- a/Cython/Compiler/Parsing.py +++ b/Cython/Compiler/Parsing.py @@ -340,11 +340,20 @@ def p_yield_expression(s): # s.sy == "yield" pos = s.position() s.next() + is_yield_from = False + if s.sy == 'from': + is_yield_from = True + s.next() if s.sy != ')' and s.sy not in statement_terminators: arg = p_testlist(s) else: + if is_yield_from: + s.error("'yield from' requires a source argument", pos=pos) arg = None - return ExprNodes.YieldExprNode(pos, arg=arg) + if is_yield_from: + return ExprNodes.YieldFromExprNode(pos, arg=arg) + else: + return ExprNodes.YieldExprNode(pos, arg=arg) def p_yield_statement(s): # s.sy == "yield" diff --git a/Cython/Utility/Generator.c b/Cython/Utility/Generator.c index 5327f4ede..a0203864c 100644 --- a/Cython/Utility/Generator.c +++ b/Cython/Utility/Generator.c @@ -1,3 +1,25 @@ +//////////////////// YieldFrom.proto //////////////////// + +static CYTHON_INLINE PyObject* __Pyx_Generator_Yield_From(__pyx_GeneratorObject *gen, PyObject *source); + +//////////////////// YieldFrom //////////////////// +//@requires: Generator + +static CYTHON_INLINE PyObject* __Pyx_Generator_Yield_From(__pyx_GeneratorObject *gen, PyObject *source) { + PyObject *source_gen, *retval; + source_gen = PyObject_GetIter(source); + if (unlikely(!source_gen)) + return NULL; + /* source_gen is now the iterator, make the first next() call */ + retval = Py_TYPE(source_gen)->tp_iternext(source_gen); + if (likely(retval)) { + gen->yieldfrom = source_gen; + return retval; + } + Py_DECREF(source_gen); + return NULL; +} + //////////////////// Generator.proto //////////////////// #define __Pyx_Generator_USED #include <structmember.h> @@ -15,21 +37,109 @@ typedef struct { PyObject *exc_traceback; PyObject *gi_weakreflist; PyObject *classobj; + PyObject *yieldfrom; } __pyx_GeneratorObject; static __pyx_GeneratorObject *__Pyx_Generator_New(__pyx_generator_body_t body, PyObject *closure); static int __pyx_Generator_init(void); +#if 1 || PY_VERSION_HEX < 0x030300B0 +static int __Pyx_PyGen_FetchStopIterationValue(PyObject **pvalue); +#else +#define __Pyx_PyGen_FetchStopIterationValue(pvalue) PyGen_FetchStopIterationValue(pvalue) +#endif + //////////////////// Generator //////////////////// +//@requires: Exceptions.c::PyErrFetchRestore +//@requires: Exceptions.c::SwapException +//@requires: Exceptions.c::RaiseException + static PyObject *__Pyx_Generator_Next(PyObject *self); static PyObject *__Pyx_Generator_Send(PyObject *self, PyObject *value); static PyObject *__Pyx_Generator_Close(PyObject *self); static PyObject *__Pyx_Generator_Throw(PyObject *gen, PyObject *args); +static PyTypeObject __pyx_GeneratorType; +#define __Pyx_Generator_CheckExact(obj) (Py_TYPE(obj) == &__pyx_GeneratorType) +#define __Pyx_Generator_Undelegate(gen) Py_CLEAR((gen)->yieldfrom) + +// If StopIteration exception is set, fetches its 'value' +// attribute if any, otherwise sets pvalue to None. +// +// Returns 0 if no exception or StopIteration is set. +// If any other exception is set, returns -1 and leaves +// pvalue unchanged. +#if 1 || PY_VERSION_HEX < 0x030300B0 +static int __Pyx_PyGen_FetchStopIterationValue(PyObject **pvalue) { + PyObject *et, *ev, *tb; + PyObject *value = NULL; + + __Pyx_ErrFetch(&et, &ev, &tb); + + if (!et) { + Py_XDECREF(tb); + Py_XDECREF(ev); + Py_INCREF(Py_None); + *pvalue = Py_None; + return 0; + } + + if (unlikely(et != PyExc_StopIteration) && + unlikely(!PyErr_GivenExceptionMatches(et, PyExc_StopIteration))) { + __Pyx_ErrRestore(et, ev, tb); + return -1; + } + + // most common case: plain StopIteration without or with separate argument + if (likely(et == PyExc_StopIteration)) { + if (likely(!ev) || !PyObject_IsInstance(ev, PyExc_StopIteration)) { + // PyErr_SetObject() and friends put the value directly into ev + if (!ev) { + Py_INCREF(Py_None); + ev = Py_None; + } + Py_XDECREF(tb); + Py_DECREF(et); + *pvalue = ev; + return 0; + } + } + // otherwise: normalise and check what that gives us + PyErr_NormalizeException(&et, &ev, &tb); + if (unlikely(!PyObject_IsInstance(ev, PyExc_StopIteration))) { + // looks like normalisation failed - raise the new exception + __Pyx_ErrRestore(et, ev, tb); + return -1; + } + Py_XDECREF(tb); + Py_DECREF(et); +#if PY_VERSION_HEX >= 0x030300A0 + value = ((PyStopIterationObject *)ev)->value; + Py_INCREF(value); + Py_DECREF(ev); +#else + { + PyObject* args = PyObject_GetAttrString(ev, "args"); + Py_DECREF(ev); + if (likely(args)) { + value = PyObject_GetItem(args, 0); + Py_DECREF(args); + } + if (unlikely(!value)) { + __Pyx_ErrRestore(NULL, NULL, NULL); + Py_INCREF(Py_None); + value = Py_None; + } + } +#endif + *pvalue = value; + return 0; +} +#endif + static CYTHON_INLINE -void __Pyx_Generator_ExceptionClear(__pyx_GeneratorObject *self) -{ +void __Pyx_Generator_ExceptionClear(__pyx_GeneratorObject *self) { PyObject *exc_type = self->exc_type; PyObject *exc_value = self->exc_value; PyObject *exc_traceback = self->exc_traceback; @@ -44,15 +154,20 @@ void __Pyx_Generator_ExceptionClear(__pyx_GeneratorObject *self) } static CYTHON_INLINE -PyObject *__Pyx_Generator_SendEx(__pyx_GeneratorObject *self, PyObject *value) -{ - PyObject *retval; - - if (unlikely(self->is_running)) { +int __Pyx_Generator_CheckRunning(__pyx_GeneratorObject *gen) { + if (unlikely(gen->is_running)) { PyErr_SetString(PyExc_ValueError, "generator already executing"); - return NULL; + return 1; } + return 0; +} + +static CYTHON_INLINE +PyObject *__Pyx_Generator_SendEx(__pyx_GeneratorObject *self, PyObject *value) { + PyObject *retval; + + assert(!self->is_running); if (unlikely(self->resume_label == 0)) { if (unlikely(value && value != Py_None)) { @@ -86,75 +201,213 @@ PyObject *__Pyx_Generator_SendEx(__pyx_GeneratorObject *self, PyObject *value) return retval; } -static PyObject *__Pyx_Generator_Next(PyObject *self) -{ - return __Pyx_Generator_SendEx((__pyx_GeneratorObject *) self, Py_None); +static CYTHON_INLINE +PyObject *__Pyx_Generator_FinishDelegation(__pyx_GeneratorObject *gen) { + PyObject *ret; + PyObject *val = NULL; + __Pyx_Generator_Undelegate(gen); + __Pyx_PyGen_FetchStopIterationValue(&val); + // val == NULL on failure => pass on exception + ret = __Pyx_Generator_SendEx(gen, val); + Py_XDECREF(val); + return ret; } -static PyObject *__Pyx_Generator_Send(PyObject *self, PyObject *value) -{ - return __Pyx_Generator_SendEx((__pyx_GeneratorObject *) self, value); +static PyObject *__Pyx_Generator_Next(PyObject *self) { + __pyx_GeneratorObject *gen = (__pyx_GeneratorObject*) self; + PyObject *yf = gen->yieldfrom; + if (unlikely(__Pyx_Generator_CheckRunning(gen))) + return NULL; + if (yf) { + PyObject *ret; + // FIXME: does this really need an INCREF() ? + //Py_INCREF(yf); + /* YieldFrom code ensures that yf is an iterator */ + gen->is_running = 1; + ret = Py_TYPE(yf)->tp_iternext(yf); + gen->is_running = 0; + //Py_DECREF(yf); + if (likely(ret)) { + return ret; + } + return __Pyx_Generator_FinishDelegation(gen); + } + return __Pyx_Generator_SendEx(gen, Py_None); } -static PyObject *__Pyx_Generator_Close(PyObject *self) -{ - __pyx_GeneratorObject *generator = (__pyx_GeneratorObject *) self; - PyObject *retval; +static PyObject *__Pyx_Generator_Send(PyObject *self, PyObject *value) { + __pyx_GeneratorObject *gen = (__pyx_GeneratorObject*) self; + PyObject *yf = gen->yieldfrom; + if (unlikely(__Pyx_Generator_CheckRunning(gen))) + return NULL; + if (yf) { + PyObject *ret; + // FIXME: does this really need an INCREF() ? + //Py_INCREF(yf); + gen->is_running = 1; + if (__Pyx_Generator_CheckExact(yf)) { + ret = __Pyx_Generator_Send(yf, value); + } else { + if (value == Py_None) + ret = PyIter_Next(yf); + else + ret = PyObject_CallMethod(yf, "send", "O", value); + } + gen->is_running = 0; + //Py_DECREF(yf); + if (likely(ret)) { + return ret; + } + return __Pyx_Generator_FinishDelegation(gen); + } + return __Pyx_Generator_SendEx(gen, value); +} + +// This helper function is used by gen_close and gen_throw to +// close a subiterator being delegated to by yield-from. +static int __Pyx_Generator_CloseIter(__pyx_GeneratorObject *gen, PyObject *yf) { + PyObject *retval = NULL; + int err = 0; + + if (__Pyx_Generator_CheckExact(yf)) { + retval = __Pyx_Generator_Close(yf); + if (!retval) + return -1; + } else { + PyObject *meth; + gen->is_running = 1; + meth = PyObject_GetAttrString(yf, "close"); + if (unlikely(!meth)) { + if (!PyErr_ExceptionMatches(PyExc_AttributeError)) { + PyErr_WriteUnraisable(yf); + } + PyErr_Clear(); + } else { + retval = PyObject_CallFunction(meth, ""); + Py_DECREF(meth); + if (!retval) + err = -1; + } + gen->is_running = 0; + } + Py_XDECREF(retval); + return err; +} + +static PyObject *__Pyx_Generator_Close(PyObject *self) { + __pyx_GeneratorObject *gen = (__pyx_GeneratorObject *) self; + PyObject *retval, *raised_exception; + PyObject *yf = gen->yieldfrom; + int err = 0; + + if (unlikely(__Pyx_Generator_CheckRunning(gen))) + return NULL; + + if (yf) { + Py_INCREF(yf); + err = __Pyx_Generator_CloseIter(gen, yf); + __Pyx_Generator_Undelegate(gen); + Py_DECREF(yf); + } + if (err == 0) #if PY_VERSION_HEX < 0x02050000 - PyErr_SetNone(PyExc_StopIteration); + PyErr_SetNone(PyExc_StopIteration); #else - PyErr_SetNone(PyExc_GeneratorExit); + PyErr_SetNone(PyExc_GeneratorExit); #endif - retval = __Pyx_Generator_SendEx(generator, NULL); + retval = __Pyx_Generator_SendEx(gen, NULL); if (retval) { Py_DECREF(retval); PyErr_SetString(PyExc_RuntimeError, "generator ignored GeneratorExit"); return NULL; } -#if PY_VERSION_HEX < 0x02050000 - if (PyErr_ExceptionMatches(PyExc_StopIteration)) -#else - if (PyErr_ExceptionMatches(PyExc_StopIteration) - || PyErr_ExceptionMatches(PyExc_GeneratorExit)) + raised_exception = PyErr_Occurred(); + if (!raised_exception + || raised_exception == PyExc_StopIteration +#if PY_VERSION_HEX >= 0x02050000 + || raised_exception == PyExc_GeneratorExit + || PyErr_GivenExceptionMatches(raised_exception, PyExc_GeneratorExit) #endif + || PyErr_GivenExceptionMatches(raised_exception, PyExc_StopIteration)) { - PyErr_Clear(); /* ignore these errors */ + if (raised_exception) PyErr_Clear(); /* ignore these errors */ Py_INCREF(Py_None); return Py_None; } return NULL; } -static PyObject *__Pyx_Generator_Throw(PyObject *self, PyObject *args) -{ - __pyx_GeneratorObject *generator = (__pyx_GeneratorObject *) self; +static PyObject *__Pyx_Generator_Throw(PyObject *self, PyObject *args) { + __pyx_GeneratorObject *gen = (__pyx_GeneratorObject *) self; PyObject *typ; PyObject *tb = NULL; PyObject *val = NULL; + PyObject *yf = gen->yieldfrom; if (!PyArg_UnpackTuple(args, (char *)"throw", 1, 3, &typ, &val, &tb)) return NULL; + + if (unlikely(__Pyx_Generator_CheckRunning(gen))) + return NULL; + + if (yf) { + PyObject *ret; + Py_INCREF(yf); +#if PY_VERSION_HEX >= 0x02050000 + if (PyErr_GivenExceptionMatches(typ, PyExc_GeneratorExit)) { + int err = __Pyx_Generator_CloseIter(gen, yf); + Py_DECREF(yf); + __Pyx_Generator_Undelegate(gen); + if (err < 0) + return __Pyx_Generator_SendEx(gen, NULL); + goto throw_here; + } +#endif + gen->is_running = 1; + if (__Pyx_Generator_CheckExact(yf)) { + ret = __Pyx_Generator_Throw(yf, args); + } else { + PyObject *meth = PyObject_GetAttrString(yf, "throw"); + if (unlikely(!meth)) { + Py_DECREF(yf); + if (!PyErr_ExceptionMatches(PyExc_AttributeError)) { + gen->is_running = 0; + return NULL; + } + PyErr_Clear(); + __Pyx_Generator_Undelegate(gen); + gen->is_running = 0; + goto throw_here; + } + ret = PyObject_CallObject(meth, args); + Py_DECREF(meth); + } + gen->is_running = 0; + Py_DECREF(yf); + if (!ret) { + ret = __Pyx_Generator_FinishDelegation(gen); + } + return ret; + } +throw_here: __Pyx_Raise(typ, val, tb, NULL); - return __Pyx_Generator_SendEx(generator, NULL); + return __Pyx_Generator_SendEx(gen, NULL); } -static int -__Pyx_Generator_traverse(PyObject *self, visitproc visit, void *arg) -{ +static int __Pyx_Generator_traverse(PyObject *self, visitproc visit, void *arg) { __pyx_GeneratorObject *gen = (__pyx_GeneratorObject *) self; Py_VISIT(gen->closure); Py_VISIT(gen->classobj); + Py_VISIT(gen->yieldfrom); Py_VISIT(gen->exc_type); Py_VISIT(gen->exc_value); Py_VISIT(gen->exc_traceback); return 0; } -static void -__Pyx_Generator_dealloc(PyObject *self) -{ +static void __Pyx_Generator_dealloc(PyObject *self) { __pyx_GeneratorObject *gen = (__pyx_GeneratorObject *) self; PyObject_GC_UnTrack(gen); @@ -172,15 +425,14 @@ __Pyx_Generator_dealloc(PyObject *self) PyObject_GC_UnTrack(self); Py_CLEAR(gen->closure); Py_CLEAR(gen->classobj); + Py_CLEAR(gen->yieldfrom); Py_CLEAR(gen->exc_type); Py_CLEAR(gen->exc_value); Py_CLEAR(gen->exc_traceback); PyObject_GC_Del(gen); } -static void -__Pyx_Generator_del(PyObject *self) -{ +static void __Pyx_Generator_del(PyObject *self) { PyObject *res; PyObject *error_type, *error_value, *error_traceback; __pyx_GeneratorObject *gen = (__pyx_GeneratorObject *) self; @@ -233,14 +485,18 @@ __Pyx_Generator_del(PyObject *self) * undone. */ #ifdef COUNT_ALLOCS - --self->ob_type->tp_frees; - --self->ob_type->tp_allocs; + --Py_TYPE(self)->tp_frees; + --Py_TYPE(self)->tp_allocs; #endif } static PyMemberDef __pyx_Generator_memberlist[] = { {(char *) "gi_running", +#if PY_VERSION_HEX >= 0x02060000 + T_BOOL, +#else T_INT, +#endif offsetof(__pyx_GeneratorObject, is_running), READONLY, NULL}, @@ -310,10 +566,8 @@ static PyTypeObject __pyx_GeneratorType = { #endif }; -static -__pyx_GeneratorObject *__Pyx_Generator_New(__pyx_generator_body_t body, - PyObject *closure) -{ +static __pyx_GeneratorObject *__Pyx_Generator_New(__pyx_generator_body_t body, + PyObject *closure) { __pyx_GeneratorObject *gen = PyObject_GC_New(__pyx_GeneratorObject, &__pyx_GeneratorType); @@ -326,6 +580,7 @@ __pyx_GeneratorObject *__Pyx_Generator_New(__pyx_generator_body_t body, gen->is_running = 0; gen->resume_label = 0; gen->classobj = NULL; + gen->yieldfrom = NULL; gen->exc_type = NULL; gen->exc_value = NULL; gen->exc_traceback = NULL; @@ -335,7 +590,6 @@ __pyx_GeneratorObject *__Pyx_Generator_New(__pyx_generator_body_t body, return gen; } -static int __pyx_Generator_init(void) -{ +static int __pyx_Generator_init(void) { return PyType_Ready(&__pyx_GeneratorType); } diff --git a/runtests.py b/runtests.py index c0c18ca62..55a918cf5 100644 --- a/runtests.py +++ b/runtests.py @@ -166,6 +166,7 @@ VER_DEP_MODULES = { ]), (2,5) : (operator.lt, lambda x: x in ['run.any', 'run.all', + 'run.yield_from_pep380', # GeneratorExit 'run.relativeimport_T542', 'run.relativeimport_star_T542', ]), diff --git a/tests/errors/e_generators.pyx b/tests/errors/e_generators.pyx index d13da55d3..fdb9d3b7a 100644 --- a/tests/errors/e_generators.pyx +++ b/tests/errors/e_generators.pyx @@ -14,8 +14,8 @@ class Foo: yield _ERRORS = u""" -5:4: 'return' with argument inside generator -9:4: 'yield' outside function +#5:4: 'return' with argument inside generator +#9:4: 'yield' outside function 11:0: 'yield' not supported here 14:4: 'yield' not supported here """ diff --git a/tests/run/yield_from_pep380.pyx b/tests/run/yield_from_pep380.pyx new file mode 100644 index 000000000..fc3ec08c5 --- /dev/null +++ b/tests/run/yield_from_pep380.pyx @@ -0,0 +1,1008 @@ +# -*- coding: utf-8 -*- + +""" +Test suite for PEP 380 implementation + +adapted from original tests written by Greg Ewing +see <http://www.cosc.canterbury.ac.nz/greg.ewing/python/yield-from/YieldFrom-Python3.1.2-rev5.zip> +""" + +import sys +IS_PY3 = sys.version_info[0] >= 3 + +def _lines(trace): + for line in trace: + print(line) + +def test_delegation_of_initial_next_to_subgenerator(): + """ + >>> _lines(test_delegation_of_initial_next_to_subgenerator()) + Starting g1 + Starting g2 + Yielded 42 + Finishing g2 + Finishing g1 + """ + trace = [] + def g1(): + trace.append("Starting g1") + yield from g2() + trace.append("Finishing g1") + def g2(): + trace.append("Starting g2") + yield 42 + trace.append("Finishing g2") + for x in g1(): + trace.append("Yielded %s" % (x,)) + return trace + +def test_raising_exception_in_initial_next_call(): + """ + >>> _lines(test_raising_exception_in_initial_next_call()) + Starting g1 + Starting g2 + Finishing g2 + Finishing g1 + """ + trace = [] + def g1(): + try: + trace.append("Starting g1") + yield from g2() + finally: + trace.append("Finishing g1") + def g2(): + try: + trace.append("Starting g2") + raise ValueError("spanish inquisition occurred") + finally: + trace.append("Finishing g2") + try: + for x in g1(): + trace.append("Yielded %s" % (x,)) + except ValueError as e: + pass + else: + trace.append("subgenerator failed to raise ValueError") + return trace + +def test_delegation_of_next_call_to_subgenerator(): + """ + >>> _lines(test_delegation_of_next_call_to_subgenerator()) + Starting g1 + Yielded g1 ham + Starting g2 + Yielded g2 spam + Yielded g2 more spam + Finishing g2 + Yielded g1 eggs + Finishing g1 + """ + trace = [] + def g1(): + trace.append("Starting g1") + yield "g1 ham" + yield from g2() + yield "g1 eggs" + trace.append("Finishing g1") + def g2(): + trace.append("Starting g2") + yield "g2 spam" + yield "g2 more spam" + trace.append("Finishing g2") + for x in g1(): + trace.append("Yielded %s" % (x,)) + return trace + +def test_raising_exception_in_delegated_next_call(): + """ + >>> _lines(test_raising_exception_in_delegated_next_call()) + Starting g1 + Yielded g1 ham + Starting g2 + Yielded g2 spam + Finishing g2 + Finishing g1 + """ + trace = [] + def g1(): + try: + trace.append("Starting g1") + yield "g1 ham" + yield from g2() + yield "g1 eggs" + finally: + trace.append("Finishing g1") + def g2(): + try: + trace.append("Starting g2") + yield "g2 spam" + raise ValueError("hovercraft is full of eels") + yield "g2 more spam" + finally: + trace.append("Finishing g2") + try: + for x in g1(): + trace.append("Yielded %s" % (x,)) + except ValueError: + pass + else: + trace.append("subgenerator failed to raise ValueError") + return trace + +def test_delegation_of_send(): + """ + >>> _lines(test_delegation_of_send()) + Starting g1 + g1 received 1 + Starting g2 + Yielded g2 spam + g2 received 2 + Yielded g2 more spam + g2 received 3 + Finishing g2 + Yielded g1 eggs + g1 received 4 + Finishing g1 + """ + trace = [] + def g1(): + trace.append("Starting g1") + x = yield "g1 ham" + trace.append("g1 received %s" % (x,)) + yield from g2() + x = yield "g1 eggs" + trace.append("g1 received %s" % (x,)) + trace.append("Finishing g1") + def g2(): + trace.append("Starting g2") + x = yield "g2 spam" + trace.append("g2 received %s" % (x,)) + x = yield "g2 more spam" + trace.append("g2 received %s" % (x,)) + trace.append("Finishing g2") + g = g1() + y = next(g) + x = 1 + try: + while 1: + y = g.send(x) + trace.append("Yielded %s" % (y,)) + x += 1 + except StopIteration: + pass + return trace + +def test_handling_exception_while_delegating_send(): + """ + >>> _lines(test_handling_exception_while_delegating_send()) + Starting g1 + g1 received 1 + Starting g2 + Yielded g2 spam + g2 received 2 + """ + trace = [] + def g1(): + trace.append("Starting g1") + x = yield "g1 ham" + trace.append("g1 received %s" % (x,)) + yield from g2() + x = yield "g1 eggs" + trace.append("g1 received %s" % (x,)) + trace.append("Finishing g1") + def g2(): + trace.append("Starting g2") + x = yield "g2 spam" + trace.append("g2 received %s" % (x,)) + raise ValueError("hovercraft is full of eels") + x = yield "g2 more spam" + trace.append("g2 received %s" % (x,)) + trace.append("Finishing g2") + def run(): + g = g1() + y = next(g) + x = 1 + try: + while 1: + y = g.send(x) + trace.append("Yielded %s" % (y,)) + x += 1 + except StopIteration: + trace.append("StopIteration") + try: + run() + except ValueError: + pass # ok + else: + trace.append("no ValueError") + return trace + +def test_delegating_close(): + """ + >>> _lines(test_delegating_close()) + Starting g1 + Yielded g1 ham + Starting g2 + Yielded g2 spam + Finishing g2 + Finishing g1 + """ + trace = [] + def g1(): + try: + trace.append("Starting g1") + yield "g1 ham" + yield from g2() + yield "g1 eggs" + finally: + trace.append("Finishing g1") + def g2(): + try: + trace.append("Starting g2") + yield "g2 spam" + yield "g2 more spam" + finally: + trace.append("Finishing g2") + g = g1() + for i in range(2): + x = next(g) + trace.append("Yielded %s" % (x,)) + g.close() + return trace + +def test_handing_exception_while_delegating_close(): + """ + >>> _lines(test_handing_exception_while_delegating_close()) + Starting g1 + Yielded g1 ham + Starting g2 + Yielded g2 spam + Finishing g2 + Finishing g1 + """ + trace = [] + def g1(): + try: + trace.append("Starting g1") + yield "g1 ham" + yield from g2() + yield "g1 eggs" + finally: + trace.append("Finishing g1") + def g2(): + try: + trace.append("Starting g2") + yield "g2 spam" + yield "g2 more spam" + finally: + trace.append("Finishing g2") + raise ValueError("nybbles have exploded with delight") + try: + g = g1() + for i in range(2): + x = next(g) + trace.append("Yielded %s" % (x,)) + g.close() + except ValueError: + pass + else: + trace.append("subgenerator failed to raise ValueError") + return trace + +def test_delegating_throw(): + """ + >>> _lines(test_delegating_throw()) + Starting g1 + Yielded g1 ham + Starting g2 + Yielded g2 spam + Finishing g2 + Finishing g1 + """ + trace = [] + def g1(): + try: + trace.append("Starting g1") + yield "g1 ham" + yield from g2() + yield "g1 eggs" + finally: + trace.append("Finishing g1") + def g2(): + try: + trace.append("Starting g2") + yield "g2 spam" + yield "g2 more spam" + finally: + trace.append("Finishing g2") + try: + g = g1() + for i in range(2): + x = next(g) + trace.append("Yielded %s" % (x,)) + e = ValueError("tomato ejected") + g.throw(e) + except ValueError: + pass + else: + trace.append("subgenerator failed to raise ValueError") + return trace + +def __test_value_attribute_of_StopIteration_exception(): + """ + StopIteration: + value = None + StopIteration: spam + value = spam + StopIteration: spam + value = eggs + """ + trace = [] + def pex(e): + trace.append("%s: %s" % (e.__class__.__name__, e)) + trace.append("value = %s" % (e.value,)) + e = StopIteration() + pex(e) + e = StopIteration("spam") + pex(e) + e.value = "eggs" + pex(e) + return trace + +def test_exception_value_crash(): + """ + >>> test_exception_value_crash() + ['g2'] + """ + # There used to be a refcount error in CPython when the return value + # stored in the StopIteration has a refcount of 1. + def g1(): + yield from g2() + def g2(): + yield "g2" + return [42] + return list(g1()) + +def test_generator_return_value(): + """ + >>> _lines(test_generator_return_value()) + Starting g1 + Yielded g1 ham + Starting g2 + Yielded g2 spam + Yielded g2 more spam + Finishing g2 + g2 returned None + Starting g2 + Yielded g2 spam + Yielded g2 more spam + Finishing g2 + g2 returned 42 + Yielded g1 eggs + Finishing g1 + """ + trace = [] + def g1(): + trace.append("Starting g1") + yield "g1 ham" + ret = yield from g2() + trace.append("g2 returned %s" % (ret,)) + ret = yield from g2(42) + trace.append("g2 returned %s" % (ret,)) + yield "g1 eggs" + trace.append("Finishing g1") + def g2(v = None): + trace.append("Starting g2") + yield "g2 spam" + yield "g2 more spam" + trace.append("Finishing g2") + if v: + return v + for x in g1(): + trace.append("Yielded %s" % (x,)) + return trace + +def test_delegation_of_next_to_non_generator(): + """ + >>> _lines(test_delegation_of_next_to_non_generator()) + Yielded 0 + Yielded 1 + Yielded 2 + """ + trace = [] + def g(): + yield from range(3) + for x in g(): + trace.append("Yielded %s" % (x,)) + return trace + +def test_conversion_of_sendNone_to_next(): + """ + >>> _lines(test_conversion_of_sendNone_to_next()) + Yielded: 0 + Yielded: 1 + Yielded: 2 + """ + trace = [] + def g(): + yield from range(3) + gi = g() + for x in range(3): + y = gi.send(None) + trace.append("Yielded: %s" % (y,)) + return trace + +def test_delegation_of_close_to_non_generator(): + """ + >>> _lines(test_delegation_of_close_to_non_generator()) + starting g + finishing g + """ + trace = [] + def g(): + try: + trace.append("starting g") + yield from range(3) + trace.append("g should not be here") + finally: + trace.append("finishing g") + gi = g() + next(gi) + gi.close() + return trace + +def test_delegating_throw_to_non_generator(): + """ + >>> _lines(test_delegating_throw_to_non_generator()) + Starting g + Yielded 0 + Yielded 1 + Yielded 2 + Yielded 3 + Yielded 4 + Finishing g + """ + trace = [] + def g(): + try: + trace.append("Starting g") + yield from range(10) + finally: + trace.append("Finishing g") + try: + gi = g() + for i in range(5): + x = next(gi) + trace.append("Yielded %s" % (x,)) + e = ValueError("tomato ejected") + gi.throw(e) + except ValueError: + pass + else: + trace.append("subgenerator failed to raise ValueError") + return trace + +def test_attempting_to_send_to_non_generator(): + """ + >>> _lines(test_attempting_to_send_to_non_generator()) + starting g + finishing g + """ + trace = [] + def g(): + try: + trace.append("starting g") + yield from range(3) + trace.append("g should not be here") + finally: + trace.append("finishing g") + try: + gi = g() + next(gi) + for x in range(3): + y = gi.send(42) + trace.append("Should not have yielded:", y) + except AttributeError: + pass + else: + trace.append("was able to send into non-generator") + return trace + +def test_broken_getattr_handling(): + """ + >>> test_broken_getattr_handling() + [] + """ + class Broken: + def __iter__(self): + return self + def __next__(self): + return 1 + next = __next__ + def __getattr__(self, attr): + 1/0 + + if IS_PY3: + expected_exception = ZeroDivisionError + else: + expected_exception = AttributeError + + def g(): + yield from Broken() + + not_raised = [] + try: + gi = g() + assert next(gi) == 1 + gi.send(1) + except expected_exception: + pass + else: + not_raised.append(1) + + try: + gi = g() + assert next(gi) == 1 + gi.throw(AttributeError) + except ZeroDivisionError: + pass + else: + not_raised.append(2) + + """ + # this currently only calls PyErr_WriteUnraisable() and doesn't raise ... + try: + gi = g() + assert next(gi) == 1 + gi.close() + except ZeroDivisionError: + pass + else: + not_raised.append(3) + """ + gi = g() + assert next(gi) == 1 + gi.close() + + return not_raised + +def test_exception_in_initial_next_call(): + """ + >>> _lines(test_exception_in_initial_next_call()) + g1 about to yield from g2 + """ + trace = [] + def g1(): + trace.append("g1 about to yield from g2") + yield from g2() + trace.append("g1 should not be here") + def g2(): + yield 1/0 + def run(): + gi = g1() + next(gi) + try: + run() + except ZeroDivisionError: + pass + else: + trace.append("ZeroDivisionError not raised") + return trace + +def test_attempted_yield_from_loop(): + """ + >>> _lines(test_attempted_yield_from_loop()) + g1: starting + Yielded: y1 + g1: about to yield from g2 + g2: starting + Yielded: y2 + g2: about to yield from g1 + """ + trace = [] + def g1(): + trace.append("g1: starting") + yield "y1" + trace.append("g1: about to yield from g2") + yield from g2() + trace.append("g1 should not be here") + + def g2(): + trace.append("g2: starting") + yield "y2" + trace.append("g2: about to yield from g1") + yield from gi + trace.append("g2 should not be here") + try: + gi = g1() + for y in gi: + trace.append("Yielded: %s" % (y,)) + except ValueError: + pass # "generator already executing" + else: + trace.append("subgenerator didn't raise ValueError") + return trace + +def test_attempted_reentry(): + """ + >>> _lines(test_attempted_reentry()) + g1: starting + Yielded: y1 + g1: about to yield from g2 + g2: starting + Yielded: y2 + g2: about to yield from g1 + g2: caught ValueError + Yielded: y3 + g1: after delegating to g2 + Yielded: y4 + """ + trace = [] + def g1(): + trace.append("g1: starting") + yield "y1" + trace.append("g1: about to yield from g2") + yield from g2() + trace.append("g1: after delegating to g2") + yield "y4" + + def g2(): + trace.append("g2: starting") + yield "y2" + trace.append("g2: about to yield from g1") + try: + yield from gi + except ValueError: + trace.append("g2: caught ValueError") + else: + trace.append("g1 did not raise ValueError on reentry") + yield "y3" + gi = g1() + for y in gi: + trace.append("Yielded: %s" % (y,)) + return trace + +def test_returning_value_from_delegated_throw(): + """ + >>> _lines(test_returning_value_from_delegated_throw()) + Starting g1 + Yielded g1 ham + Starting g2 + Yielded g2 spam + Caught LunchError in g2 + Yielded g2 yet more spam + Yielded g1 eggs + Finishing g1 + """ + trace = [] + def g1(): + try: + trace.append("Starting g1") + yield "g1 ham" + yield from g2() + yield "g1 eggs" + finally: + trace.append("Finishing g1") + def g2(): + try: + trace.append("Starting g2") + yield "g2 spam" + yield "g2 more spam" + except LunchError: + trace.append("Caught LunchError in g2") + yield "g2 lunch saved" + yield "g2 yet more spam" + class LunchError(Exception): + pass + g = g1() + for i in range(2): + x = next(g) + trace.append("Yielded %s" % (x,)) + e = LunchError("tomato ejected") + g.throw(e) + for x in g: + trace.append("Yielded %s" % (x,)) + return trace + +def test_next_and_return_with_value(): + """ + >>> _lines(test_next_and_return_with_value()) + g starting + f resuming g + g returning None + f caught StopIteration + g starting + f resuming g + g returning 42 + f caught StopIteration + """ + trace = [] + def f(r): + gi = g(r) + next(gi) + try: + trace.append("f resuming g") + next(gi) + trace.append("f SHOULD NOT BE HERE") + except StopIteration: + trace.append("f caught StopIteration") + def g(r): + trace.append("g starting") + yield + trace.append("g returning %s" % (r,)) + return r + f(None) + f(42) + return trace + +def test_send_and_return_with_value(): + """ + >>> _lines(test_send_and_return_with_value()) + g starting + f sending spam to g + g received spam + g returning None + f caught StopIteration + g starting + f sending spam to g + g received spam + g returning 42 + f caught StopIteration + """ + trace = [] + def f(r): + gi = g(r) + next(gi) + try: + trace.append("f sending spam to g") + gi.send("spam") + trace.append("f SHOULD NOT BE HERE") + except StopIteration: + trace.append("f caught StopIteration") + def g(r): + trace.append("g starting") + x = yield + trace.append("g received %s" % (x,)) + trace.append("g returning %s" % (r,)) + return r + f(None) + f(42) + return trace + +def test_catching_exception_from_subgen_and_returning(): + """ + Test catching an exception thrown into a + subgenerator and returning a value + + >>> _lines(test_catching_exception_from_subgen_and_returning()) + 1 + inner caught ValueError + inner returned 2 to outer + 2 + """ + trace = [] + def inner(): + try: + yield 1 + except ValueError: + trace.append("inner caught ValueError") + return 2 + + def outer(): + v = yield from inner() + trace.append("inner returned %r to outer" % v) + yield v + g = outer() + trace.append(next(g)) + trace.append(g.throw(ValueError)) + return trace + +def test_throwing_GeneratorExit_into_subgen_that_returns(): + """ + Test throwing GeneratorExit into a subgenerator that + catches it and returns normally. + + >>> _lines(test_throwing_GeneratorExit_into_subgen_that_returns()) + Enter g + Enter f + """ + trace = [] + def f(): + try: + trace.append("Enter f") + yield + trace.append("Exit f") + except GeneratorExit: + return + def g(): + trace.append("Enter g") + yield from f() + trace.append("Exit g") + try: + gi = g() + next(gi) + gi.throw(GeneratorExit) + except GeneratorExit: + pass + else: + trace.append("subgenerator failed to raise GeneratorExit") + return trace + +def test_throwing_GeneratorExit_into_subgenerator_that_yields(): + """ + Test throwing GeneratorExit into a subgenerator that + catches it and yields. + + >>> _lines(test_throwing_GeneratorExit_into_subgenerator_that_yields()) + Enter g + Enter f + """ + trace = [] + def f(): + try: + trace.append("Enter f") + yield + trace.append("Exit f") + except GeneratorExit: + yield + def g(): + trace.append("Enter g") + yield from f() + trace.append("Exit g") + try: + gi = g() + next(gi) + gi.throw(GeneratorExit) + except RuntimeError: + pass # "generator ignored GeneratorExit" + else: + trace.append("subgenerator failed to raise GeneratorExit") + return trace + +def test_throwing_GeneratorExit_into_subgen_that_raises(): + """ + Test throwing GeneratorExit into a subgenerator that + catches it and raises a different exception. + + >>> _lines(test_throwing_GeneratorExit_into_subgen_that_raises()) + Enter g + Enter f + """ + trace = [] + def f(): + try: + trace.append("Enter f") + yield + trace.append("Exit f") + except GeneratorExit: + raise ValueError("Vorpal bunny encountered") + def g(): + trace.append("Enter g") + yield from f() + trace.append("Exit g") + try: + gi = g() + next(gi) + gi.throw(GeneratorExit) + except ValueError: + pass # "Vorpal bunny encountered" + else: + trace.append("subgenerator failed to raise ValueError") + return trace + +def test_yield_from_empty(): + """ + >>> test_yield_from_empty() + """ + def g(): + yield from () + try: + next(g()) + except StopIteration: + pass + else: + return "FAILED" + +# test re-entry guards + +def _reentering_gen(): + def one(): + yield 0 + yield from two() + yield 3 + def two(): + yield 1 + try: + yield from g1 + except ValueError: + pass + yield 2 + g1 = one() + return g1 + +def test_delegating_generators_claim_to_be_running_next(): + """ + >>> test_delegating_generators_claim_to_be_running_next() + [0, 1, 2, 3] + """ + return list(_reentering_gen()) + +def test_delegating_generators_claim_to_be_running_send(): + """ + >>> test_delegating_generators_claim_to_be_running_send() + [0, 1, 2, 3] + """ + g1 = _reentering_gen() + res = [next(g1)] + try: + while True: + res.append(g1.send(42)) + except StopIteration: + pass + return res + +def test_delegating_generators_claim_to_be_running_throw(): + """ + >>> test_delegating_generators_claim_to_be_running_throw() + [0, 1, 2, 3] + """ + class MyErr(Exception): + pass + def one(): + try: + yield 0 + except MyErr: + pass + yield from two() + try: + yield 3 + except MyErr: + pass + def two(): + try: + yield 1 + except MyErr: + pass + try: + yield from g1 + except ValueError: + pass + try: + yield 2 + except MyErr: + pass + g1 = one() + res = [next(g1)] + try: + while True: + res.append(g1.throw(MyErr)) + except StopIteration: + pass + return res + +def test_delegating_generators_claim_to_be_running_close(): + """ + >>> test_delegating_generators_claim_to_be_running_close() + 42 + """ + class MyIt(object): + def __iter__(self): + return self + def __next__(self): + return 42 + next = __next__ + def close(self_): + assert g1.gi_running + try: + next(g1) + except ValueError: + pass # guard worked + else: + assert False, "re-entry guard failed to bark" + def one(): + yield from MyIt() + g1 = one() + ret = next(g1) + g1.close() + return ret -- 2.30.9