Author: Armin Rigo <armin.r...@gmail.com> Branch: py3.6 Changeset: r93928:552eccd54e83 Date: 2018-03-01 10:06 +0000 http://bitbucket.org/pypy/pypy/changeset/552eccd54e83/
Log: Merged in alcarithemad/pypy/pep526 (pull request #595) PEP 526: Type annotations for variables Approved-by: Armin Rigo <armin.r...@gmail.com> Approved-by: Amaury Forgeot d'Arc <amaur...@gmail.com> diff --git a/lib-python/3/opcode.py b/lib-python/3/opcode.py --- a/lib-python/3/opcode.py +++ b/lib-python/3/opcode.py @@ -121,7 +121,7 @@ def_op('RETURN_VALUE', 83) def_op('IMPORT_STAR', 84) - +def_op('SETUP_ANNOTATIONS', 85) def_op('YIELD_VALUE', 86) def_op('POP_BLOCK', 87) def_op('END_FINALLY', 88) @@ -171,6 +171,7 @@ haslocal.append(125) def_op('DELETE_FAST', 126) # Local variable number haslocal.append(126) +name_op('STORE_ANNOTATION', 127) # Index in name list def_op('RAISE_VARARGS', 130) # Number of raise arguments (1, 2, or 3) def_op('CALL_FUNCTION', 131) # #args + (#kwargs << 8) diff --git a/pypy/interpreter/app_main.py b/pypy/interpreter/app_main.py --- a/pypy/interpreter/app_main.py +++ b/pypy/interpreter/app_main.py @@ -577,6 +577,7 @@ mainmodule = type(sys)('__main__') mainmodule.__loader__ = sys.__loader__ mainmodule.__builtins__ = os.__builtins__ + mainmodule.__annotations__ = {} sys.modules['__main__'] = mainmodule if not no_site: diff --git a/pypy/interpreter/astcompiler/assemble.py b/pypy/interpreter/astcompiler/assemble.py --- a/pypy/interpreter/astcompiler/assemble.py +++ b/pypy/interpreter/astcompiler/assemble.py @@ -690,6 +690,9 @@ ops.POP_JUMP_IF_FALSE: -1, ops.JUMP_IF_NOT_DEBUG: 0, + ops.SETUP_ANNOTATIONS: 0, + ops.STORE_ANNOTATION: -1, + # TODO ops.BUILD_LIST_FROM_ARG: 1, diff --git a/pypy/interpreter/astcompiler/ast.py b/pypy/interpreter/astcompiler/ast.py --- a/pypy/interpreter/astcompiler/ast.py +++ b/pypy/interpreter/astcompiler/ast.py @@ -339,6 +339,8 @@ return Assign.from_object(space, w_node) if space.isinstance_w(w_node, get(space).w_AugAssign): return AugAssign.from_object(space, w_node) + if space.isinstance_w(w_node, get(space).w_AnnAssign): + return AnnAssign.from_object(space, w_node) if space.isinstance_w(w_node, get(space).w_For): return For.from_object(space, w_node) if space.isinstance_w(w_node, get(space).w_AsyncFor): @@ -816,6 +818,64 @@ State.ast_type('AugAssign', 'stmt', ['target', 'op', 'value']) +class AnnAssign(stmt): + + def __init__(self, target, annotation, value, simple, lineno, col_offset): + self.target = target + self.annotation = annotation + self.value = value + self.simple = simple + stmt.__init__(self, lineno, col_offset) + + def walkabout(self, visitor): + visitor.visit_AnnAssign(self) + + def mutate_over(self, visitor): + self.target = self.target.mutate_over(visitor) + self.annotation = self.annotation.mutate_over(visitor) + if self.value: + self.value = self.value.mutate_over(visitor) + return visitor.visit_AnnAssign(self) + + def to_object(self, space): + w_node = space.call_function(get(space).w_AnnAssign) + w_target = self.target.to_object(space) # expr + space.setattr(w_node, space.newtext('target'), w_target) + w_annotation = self.annotation.to_object(space) # expr + space.setattr(w_node, space.newtext('annotation'), w_annotation) + w_value = self.value.to_object(space) if self.value is not None else space.w_None # expr + space.setattr(w_node, space.newtext('value'), w_value) + w_simple = space.newint(self.simple) # int + space.setattr(w_node, space.newtext('simple'), w_simple) + w_lineno = space.newint(self.lineno) # int + space.setattr(w_node, space.newtext('lineno'), w_lineno) + w_col_offset = space.newint(self.col_offset) # int + space.setattr(w_node, space.newtext('col_offset'), w_col_offset) + return w_node + + @staticmethod + def from_object(space, w_node): + w_target = get_field(space, w_node, 'target', False) + w_annotation = get_field(space, w_node, 'annotation', False) + w_value = get_field(space, w_node, 'value', True) + w_simple = get_field(space, w_node, 'simple', False) + w_lineno = get_field(space, w_node, 'lineno', False) + w_col_offset = get_field(space, w_node, 'col_offset', False) + _target = expr.from_object(space, w_target) + if _target is None: + raise_required_value(space, w_node, 'target') + _annotation = expr.from_object(space, w_annotation) + if _annotation is None: + raise_required_value(space, w_node, 'annotation') + _value = expr.from_object(space, w_value) + _simple = obj_to_int(space, w_simple) + _lineno = obj_to_int(space, w_lineno) + _col_offset = obj_to_int(space, w_col_offset) + return AnnAssign(_target, _annotation, _value, _simple, _lineno, _col_offset) + +State.ast_type('AnnAssign', 'stmt', ['target', 'annotation', 'value', 'simple']) + + class For(stmt): def __init__(self, target, iter, body, orelse, lineno, col_offset): @@ -3673,10 +3733,11 @@ class comprehension(AST): - def __init__(self, target, iter, ifs): + def __init__(self, target, iter, ifs, is_async): self.target = target self.iter = iter self.ifs = ifs + self.is_async = is_async def mutate_over(self, visitor): self.target = self.target.mutate_over(visitor) @@ -3702,6 +3763,8 @@ ifs_w = [node.to_object(space) for node in self.ifs] # expr w_ifs = space.newlist(ifs_w) space.setattr(w_node, space.newtext('ifs'), w_ifs) + w_is_async = space.newint(self.is_async) # int + space.setattr(w_node, space.newtext('is_async'), w_is_async) return w_node @staticmethod @@ -3709,6 +3772,7 @@ w_target = get_field(space, w_node, 'target', False) w_iter = get_field(space, w_node, 'iter', False) w_ifs = get_field(space, w_node, 'ifs', False) + w_is_async = get_field(space, w_node, 'is_async', False) _target = expr.from_object(space, w_target) if _target is None: raise_required_value(space, w_node, 'target') @@ -3717,9 +3781,10 @@ raise_required_value(space, w_node, 'iter') ifs_w = space.unpackiterable(w_ifs) _ifs = [expr.from_object(space, w_item) for w_item in ifs_w] - return comprehension(_target, _iter, _ifs) - -State.ast_type('comprehension', 'AST', ['target', 'iter', 'ifs']) + _is_async = obj_to_int(space, w_is_async) + return comprehension(_target, _iter, _ifs, _is_async) + +State.ast_type('comprehension', 'AST', ['target', 'iter', 'ifs', 'is_async']) class excepthandler(AST): @@ -4066,6 +4131,8 @@ return self.default_visitor(node) def visit_AugAssign(self, node): return self.default_visitor(node) + def visit_AnnAssign(self, node): + return self.default_visitor(node) def visit_For(self, node): return self.default_visitor(node) def visit_AsyncFor(self, node): @@ -4230,6 +4297,12 @@ node.target.walkabout(self) node.value.walkabout(self) + def visit_AnnAssign(self, node): + node.target.walkabout(self) + node.annotation.walkabout(self) + if node.value: + node.value.walkabout(self) + def visit_For(self, node): node.target.walkabout(self) node.iter.walkabout(self) diff --git a/pypy/interpreter/astcompiler/astbuilder.py b/pypy/interpreter/astcompiler/astbuilder.py --- a/pypy/interpreter/astcompiler/astbuilder.py +++ b/pypy/interpreter/astcompiler/astbuilder.py @@ -737,6 +737,7 @@ raise AssertionError("unknown statment type") def handle_expr_stmt(self, stmt): + from pypy.interpreter.pyparser.parser import AbstractNonterminal if stmt.num_children() == 1: expression = self.handle_testlist(stmt.get_child(0)) return ast.Expr(expression, stmt.get_lineno(), stmt.get_column()) @@ -754,6 +755,44 @@ operator = augassign_operator_map[op_str] return ast.AugAssign(target_expr, operator, value_expr, stmt.get_lineno(), stmt.get_column()) + elif stmt.get_child(1).type == syms.annassign: + # Variable annotation (PEP 526), which may or may not include assignment. + target = stmt.get_child(0) + target_expr = self.handle_testlist(target) + simple = 0 + # target is a name, nothing funky + if isinstance(target_expr, ast.Name): + # The PEP demands that `(x): T` be treated differently than `x: T` + # however, the parser does not easily expose the wrapping parens, which are a no-op + # they are elided by handle_testlist if they existed. + # so here we walk down the parse tree until we hit a terminal, and check whether it's + # a left paren + simple_test = target.get_child(0) + while isinstance(simple_test, AbstractNonterminal): + simple_test = simple_test.get_child(0) + if simple_test.type != tokens.LPAR: + simple = 1 + # subscripts are allowed with nothing special + elif isinstance(target_expr, ast.Subscript): + pass + # attributes are also fine here + elif isinstance(target_expr, ast.Attribute): + pass + # tuples and lists get special error messages + elif isinstance(target_expr, ast.Tuple): + self.error("only single target (not tuple) can be annotated", target) + elif isinstance(target_expr, ast.List): + self.error("only single target (not list) can be annotated", target) + # and everything else gets a generic error + else: + self.error("illegal target for annoation", target) + self.set_context(target_expr, ast.Store) + second = stmt.get_child(1) + annotation = self.handle_expr(second.get_child(1)) + value_expr = None + if second.num_children() == 4: + value_expr = self.handle_testlist(second.get_child(-1)) + return ast.AnnAssign(target_expr, annotation, value_expr, simple, stmt.get_lineno(), stmt.get_column()) else: # Normal assignment. targets = [] @@ -1315,7 +1354,8 @@ expr = self.handle_expr(comp_node.get_child(3)) assert isinstance(expr, ast.expr) if for_node.num_children() == 1: - comp = ast.comprehension(for_targets[0], expr, None) + # FIXME: determine whether this is actually async + comp = ast.comprehension(for_targets[0], expr, None, 0) else: # Modified in python2.7, see http://bugs.python.org/issue6704 # Fixing unamed tuple location @@ -1324,7 +1364,8 @@ col = expr_node.col_offset line = expr_node.lineno target = ast.Tuple(for_targets, ast.Store, line, col) - comp = ast.comprehension(target, expr, None) + # FIXME: determine whether this is actually async + comp = ast.comprehension(target, expr, None, 0) if comp_node.num_children() == 5: comp_node = comp_iter = comp_node.get_child(4) assert comp_iter.type == syms.comp_iter diff --git a/pypy/interpreter/astcompiler/codegen.py b/pypy/interpreter/astcompiler/codegen.py --- a/pypy/interpreter/astcompiler/codegen.py +++ b/pypy/interpreter/astcompiler/codegen.py @@ -299,6 +299,12 @@ else: return False + def _maybe_setup_annotations(self): + # if the scope contained an annotated variable assignemt, + # this will emit the requisite SETUP_ANNOTATIONS + if self.scope.contains_annotated and not isinstance(self, AbstractFunctionCodeGenerator): + self.emit_op(ops.SETUP_ANNOTATIONS) + def visit_Module(self, mod): if not self._handle_body(mod.body): self.first_lineno = self.lineno = 1 @@ -925,6 +931,66 @@ self.visit_sequence(targets) return True + def _annotation_evaluate(self, item): + # PEP 526 requires that some things be evaluated, to avoid bugs + # where a non-assigning variable annotation references invalid items + # this is effectively a NOP, but will fail if e.g. item is an + # Attribute and one of the chained names does not exist + item.walkabout(self) + self.emit_op(ops.POP_TOP) + + def _annotation_eval_slice(self, target): + if isinstance(target, ast.Index): + self._annotation_evaluate(target.value) + elif isinstance(target, ast.Slice): + for val in [target.lower, target.upper, target.step]: + if val: + self._annotation_evaluate(val) + elif isinstance(target, ast.ExtSlice): + for val in target.dims: + if isinstance(val, ast.Index) or isinstance(val, ast.Slice): + self._annotation_eval_slice(val) + else: + self.error("Invalid nested slice", val) + else: + self.error("Invalid slice?", target) + + def visit_AnnAssign(self, assign): + self.update_position(assign.lineno, True) + target = assign.target + # if there's an assignment to be done, do it + if assign.value: + assign.value.walkabout(self) + target.walkabout(self) + # the PEP requires that certain parts of the target be evaluated at runtime + # to avoid silent annotation-related errors + if isinstance(target, ast.Name): + # if it's just a simple name and we're not in a function, store + # the annotation in __annotations__ + if assign.simple and not isinstance(self.scope, symtable.FunctionScope): + assign.annotation.walkabout(self) + name = target.id + self.emit_op_arg(ops.STORE_ANNOTATION, self.add_name(self.names, name)) + elif isinstance(target, ast.Attribute): + # the spec requires that `a.b: int` evaluates `a` + # and in a non-function scope, also evaluates `int` + # (N.B.: if the target is of the form `a.b.c`, `a.b` will be evaluated) + if not assign.value: + attr = target.value + self._annotation_evaluate(attr) + elif isinstance(target, ast.Subscript): + # similar to the above, `a[0:5]: int` evaluates the name and the slice argument + # and if not in a function, also evaluates the annotation + sl = target.slice + self._annotation_evaluate(target.value) + self._annotation_eval_slice(sl) + else: + self.error("can't handle annotation with %s" % (target,), target) + # if this is not in a function, evaluate the annotation + if not (assign.simple or isinstance(self.scope, symtable.FunctionScope)): + self._annotation_evaluate(assign.annotation) + + def visit_With(self, wih): self.update_position(wih.lineno, True) self.handle_withitem(wih, 0, is_async=False) @@ -1527,6 +1593,7 @@ symbols, compile_info, qualname=None) def _compile(self, tree): + self._maybe_setup_annotations() tree.walkabout(self) def _get_code_flags(self): @@ -1656,6 +1723,7 @@ w_qualname = self.space.newtext(self.qualname) self.load_const(w_qualname) self.name_op("__qualname__", ast.Store) + self._maybe_setup_annotations() # compile the body proper self._handle_body(cls.body) # return the (empty) __class__ cell diff --git a/pypy/interpreter/astcompiler/symtable.py b/pypy/interpreter/astcompiler/symtable.py --- a/pypy/interpreter/astcompiler/symtable.py +++ b/pypy/interpreter/astcompiler/symtable.py @@ -12,6 +12,7 @@ SYM_PARAM = 2 << 1 SYM_NONLOCAL = 2 << 2 SYM_USED = 2 << 3 +SYM_ANNOTATED = 2 << 4 SYM_BOUND = (SYM_PARAM | SYM_ASSIGNED) # codegen.py actually deals with these: @@ -44,6 +45,7 @@ self.child_has_free = False self.nested = False self.doc_removable = False + self.contains_annotated = False self._in_try_body_depth = 0 def lookup(self, name): @@ -139,7 +141,7 @@ self.free_vars.append(name) free[name] = None self.has_free = True - elif flags & SYM_BOUND: + elif flags & (SYM_BOUND | SYM_ANNOTATED): self.symbols[name] = SCOPE_LOCAL local[name] = None try: @@ -420,6 +422,20 @@ self.scope.note_return(ret) ast.GenericASTVisitor.visit_Return(self, ret) + def visit_AnnAssign(self, assign): + # __annotations__ is not setup or used in functions. + if not isinstance(self.scope, FunctionScope): + self.scope.contains_annotated = True + target = assign.target + if isinstance(target, ast.Name): + scope = SYM_ANNOTATED + name = target.id + if assign.value: + scope |= SYM_USED + self.note_symbol(name, scope) + else: + target.walkabout(self) + def visit_ClassDef(self, clsdef): self.note_symbol(clsdef.name, SYM_ASSIGNED) self.visit_sequence(clsdef.bases) @@ -485,10 +501,13 @@ msg = "name '%s' is nonlocal and global" % (name,) raise SyntaxError(msg, glob.lineno, glob.col_offset) - if old_role & (SYM_USED | SYM_ASSIGNED): + if old_role & (SYM_USED | SYM_ASSIGNED | SYM_ANNOTATED): if old_role & SYM_ASSIGNED: msg = "name '%s' is assigned to before global declaration"\ % (name,) + elif old_role & SYM_ANNOTATED: + msg = "annotated name '%s' can't be global" \ + % (name,) else: msg = "name '%s' is used prior to global declaration" % \ (name,) @@ -498,6 +517,7 @@ def visit_Nonlocal(self, nonl): for name in nonl.names: old_role = self.scope.lookup_role(name) + print(name, old_role) msg = "" if old_role & SYM_GLOBAL: msg = "name '%s' is nonlocal and global" % (name,) @@ -505,6 +525,9 @@ msg = "name '%s' is parameter and nonlocal" % (name,) if isinstance(self.scope, ModuleScope): msg = "nonlocal declaration not allowed at module level" + if old_role & SYM_ANNOTATED: + msg = "annotated name '%s' can't be nonlocal" \ + % (name,) if msg is not "": raise SyntaxError(msg, nonl.lineno, nonl.col_offset) diff --git a/pypy/interpreter/astcompiler/test/test_astbuilder.py b/pypy/interpreter/astcompiler/test/test_astbuilder.py --- a/pypy/interpreter/astcompiler/test/test_astbuilder.py +++ b/pypy/interpreter/astcompiler/test/test_astbuilder.py @@ -614,6 +614,44 @@ assert len(dec.args) == 2 assert dec.keywords is None + def test_annassign(self): + simple = self.get_first_stmt('a: int') + assert isinstance(simple, ast.AnnAssign) + assert isinstance(simple.target, ast.Name) + assert simple.target.ctx == ast.Store + assert isinstance(simple.annotation, ast.Name) + assert simple.value == None + assert simple.simple == 1 + + with_value = self.get_first_stmt('x: str = "test"') + assert isinstance(with_value, ast.AnnAssign) + assert isinstance(with_value.value, ast.Str) + assert self.space.eq_w(with_value.value.s, self.space.wrap("test")) + + not_simple = self.get_first_stmt('(a): int') + assert isinstance(not_simple, ast.AnnAssign) + assert isinstance(not_simple.target, ast.Name) + assert not_simple.target.ctx == ast.Store + assert not_simple.simple == 0 + + attrs = self.get_first_stmt('a.b.c: int') + assert isinstance(attrs, ast.AnnAssign) + assert isinstance(attrs.target, ast.Attribute) + + subscript = self.get_first_stmt('a[0:2]: int') + assert isinstance(subscript, ast.AnnAssign) + assert isinstance(subscript.target, ast.Subscript) + + exc_tuple = py.test.raises(SyntaxError, self.get_ast, 'a, b: int').value + assert exc_tuple.msg == "only single target (not tuple) can be annotated" + + exc_list = py.test.raises(SyntaxError, self.get_ast, '[]: int').value + assert exc_list.msg == "only single target (not list) can be annotated" + + exc_bad_target = py.test.raises(SyntaxError, self.get_ast, '{}: int').value + assert exc_bad_target.msg == "illegal target for annoation" + + def test_augassign(self): aug_assigns = ( ("+=", ast.Add), diff --git a/pypy/interpreter/astcompiler/test/test_symtable.py b/pypy/interpreter/astcompiler/test/test_symtable.py --- a/pypy/interpreter/astcompiler/test/test_symtable.py +++ b/pypy/interpreter/astcompiler/test/test_symtable.py @@ -486,6 +486,37 @@ scp = self.mod_scope("with x: pass") assert scp.lookup("_[1]") == symtable.SCOPE_LOCAL + def test_annotation_global(self): + src_global = ("def f():\n" + " x: int\n" + " global x\n") + exc_global = py.test.raises(SyntaxError, self.func_scope, src_global).value + assert exc_global.msg == "annotated name 'x' can't be global" + assert exc_global.lineno == 3 + + def test_annotation_nonlocal(self): + src_nonlocal = ("def f():\n" + " x: int\n" + " nonlocal x\n") + exc_nonlocal = py.test.raises(SyntaxError, self.func_scope, src_nonlocal).value + assert exc_nonlocal.msg == "annotated name 'x' can't be nonlocal" + assert exc_nonlocal.lineno == 3 + + def test_annotation_assignment(self): + scp = self.mod_scope("x: int = 1") + assert scp.contains_annotated == True + + scp2 = self.mod_scope("x = 1") + assert scp2.contains_annotated == False + + fscp = self.func_scope("def f(): x: int") + assert fscp.contains_annotated == False + assert fscp.lookup("x") == symtable.SCOPE_LOCAL + + def test_nonsimple_annotation(self): + fscp = self.func_scope("def f(): implicit_global[0]: int") + assert fscp.lookup("implicit_global") == symtable.SCOPE_GLOBAL_IMPLICIT + def test_issue13343(self): scp = self.mod_scope("lambda *, k1=x, k2: None") assert scp.lookup("x") == symtable.SCOPE_GLOBAL_IMPLICIT diff --git a/pypy/interpreter/astcompiler/tools/Python.asdl b/pypy/interpreter/astcompiler/tools/Python.asdl --- a/pypy/interpreter/astcompiler/tools/Python.asdl +++ b/pypy/interpreter/astcompiler/tools/Python.asdl @@ -17,6 +17,7 @@ stmt* body, expr* decorator_list, expr? returns) | AsyncFunctionDef(identifier name, arguments args, stmt* body, expr* decorator_list, expr? returns) + | ClassDef(identifier name, expr* bases, keyword* keywords, @@ -27,6 +28,8 @@ | Delete(expr* targets) | Assign(expr* targets, expr value) | AugAssign(expr target, operator op, expr value) + -- 'simple' indicates that we annotate simple name without parens + | AnnAssign(expr target, expr annotation, expr? value, int simple) -- use 'orelse' because else is a keyword in target languages | For(expr target, expr iter, stmt* body, stmt* orelse) @@ -107,7 +110,7 @@ cmpop = Eq | NotEq | Lt | LtE | Gt | GtE | Is | IsNot | In | NotIn - comprehension = (expr target, expr iter, expr* ifs) + comprehension = (expr target, expr iter, expr* ifs, int is_async) excepthandler = ExceptHandler(expr? type, identifier? name, stmt* body) attributes (int lineno, int col_offset) diff --git a/pypy/interpreter/astcompiler/validate.py b/pypy/interpreter/astcompiler/validate.py --- a/pypy/interpreter/astcompiler/validate.py +++ b/pypy/interpreter/astcompiler/validate.py @@ -212,6 +212,12 @@ self._validate_exprs(node.targets, ast.Store) self._validate_expr(node.value) + def visit_AnnAssign(self, node): + self._validate_expr(node.target, ast.Store) + self._validate_expr(node.annotation) + if node.value: + self._validate_expr(node.value) + def visit_AugAssign(self, node): self._validate_expr(node.target, ast.Store) self._validate_expr(node.value) diff --git a/pypy/interpreter/main.py b/pypy/interpreter/main.py --- a/pypy/interpreter/main.py +++ b/pypy/interpreter/main.py @@ -13,6 +13,8 @@ raise mainmodule = module.Module(space, w_main) space.setitem(w_modules, w_main, mainmodule) + w_annotations = space.newdict() + space.setitem_str(mainmodule.w_dict, '__annotations__', w_annotations) return mainmodule diff --git a/pypy/interpreter/pyopcode.py b/pypy/interpreter/pyopcode.py --- a/pypy/interpreter/pyopcode.py +++ b/pypy/interpreter/pyopcode.py @@ -292,6 +292,10 @@ self.DELETE_DEREF(oparg, next_instr) elif opcode == opcodedesc.DELETE_FAST.index: self.DELETE_FAST(oparg, next_instr) + elif opcode == opcodedesc.SETUP_ANNOTATIONS.index: + self.SETUP_ANNOTATIONS(oparg, next_instr) + elif opcode == opcodedesc.STORE_ANNOTATION.index: + self.STORE_ANNOTATION(oparg, next_instr) elif opcode == opcodedesc.DELETE_GLOBAL.index: self.DELETE_GLOBAL(oparg, next_instr) elif opcode == opcodedesc.DELETE_NAME.index: @@ -947,6 +951,18 @@ varname) self.locals_cells_stack_w[varindex] = None + def SETUP_ANNOTATIONS(self, oparg, next_instr): + w_locals = self.getorcreatedebug().w_locals + if not self.space.finditem_str(w_locals, '__annotations__'): + w_annotations = self.space.newdict() + self.space.setitem_str(w_locals, '__annotations__', w_annotations) + + def STORE_ANNOTATION(self, varindex, next_instr): + varname = self.getname_u(varindex) + w_newvalue = self.popvalue() + self.space.setitem_str(self.getorcreatedebug().w_locals.getitem_str('__annotations__'), varname, + w_newvalue) + def BUILD_TUPLE(self, itemcount, next_instr): items = self.popvalues(itemcount) w_tuple = self.space.newtuple(items) diff --git a/pypy/interpreter/pyparser/data/Grammar3.6 b/pypy/interpreter/pyparser/data/Grammar3.6 new file mode 100644 --- /dev/null +++ b/pypy/interpreter/pyparser/data/Grammar3.6 @@ -0,0 +1,149 @@ +# Grammar for Python + +# NOTE WELL: You should also follow all the steps listed at +# https://devguide.python.org/grammar/ + +# Start symbols for the grammar: +# single_input is a single interactive statement; +# file_input is a module or sequence of commands read from an input file; +# eval_input is the input for the eval() functions. +# NB: compound_stmt in single_input is followed by extra NEWLINE! +single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE +file_input: (NEWLINE | stmt)* ENDMARKER +eval_input: testlist NEWLINE* ENDMARKER + +decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE +decorators: decorator+ +decorated: decorators (classdef | funcdef | async_funcdef) + +async_funcdef: ASYNC funcdef +funcdef: 'def' NAME parameters ['->' test] ':' suite + +parameters: '(' [typedargslist] ')' +typedargslist: (tfpdef ['=' test] (',' tfpdef ['=' test])* [',' [ + '*' [tfpdef] (',' tfpdef ['=' test])* [',' ['**' tfpdef [',']]] + | '**' tfpdef [',']]] + | '*' [tfpdef] (',' tfpdef ['=' test])* [',' ['**' tfpdef [',']]] + | '**' tfpdef [',']) +tfpdef: NAME [':' test] +varargslist: (vfpdef ['=' test] (',' vfpdef ['=' test])* [',' [ + '*' [vfpdef] (',' vfpdef ['=' test])* [',' ['**' vfpdef [',']]] + | '**' vfpdef [',']]] + | '*' [vfpdef] (',' vfpdef ['=' test])* [',' ['**' vfpdef [',']]] + | '**' vfpdef [','] +) +vfpdef: NAME + +stmt: simple_stmt | compound_stmt +simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE +small_stmt: (expr_stmt | del_stmt | pass_stmt | flow_stmt | + import_stmt | global_stmt | nonlocal_stmt | assert_stmt) +expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) | + ('=' (yield_expr|testlist_star_expr))*) +annassign: ':' test ['=' test] +testlist_star_expr: (test|star_expr) (',' (test|star_expr))* [','] +augassign: ('+=' | '-=' | '*=' | '@=' | '/=' | '%=' | '&=' | '|=' | '^=' | + '<<=' | '>>=' | '**=' | '//=') +# For normal and annotated assignments, additional restrictions enforced by the interpreter +del_stmt: 'del' exprlist +pass_stmt: 'pass' +flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt | yield_stmt +break_stmt: 'break' +continue_stmt: 'continue' +return_stmt: 'return' [testlist] +yield_stmt: yield_expr +raise_stmt: 'raise' [test ['from' test]] +import_stmt: import_name | import_from +import_name: 'import' dotted_as_names +# note below: the ('.' | '...') is necessary because '...' is tokenized as ELLIPSIS +import_from: ('from' (('.' | '...')* dotted_name | ('.' | '...')+) + 'import' ('*' | '(' import_as_names ')' | import_as_names)) +import_as_name: NAME ['as' NAME] +dotted_as_name: dotted_name ['as' NAME] +import_as_names: import_as_name (',' import_as_name)* [','] +dotted_as_names: dotted_as_name (',' dotted_as_name)* +dotted_name: NAME ('.' NAME)* +global_stmt: 'global' NAME (',' NAME)* +nonlocal_stmt: 'nonlocal' NAME (',' NAME)* +assert_stmt: 'assert' test [',' test] + +compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated | async_stmt +async_stmt: ASYNC (funcdef | with_stmt | for_stmt) +if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite] +while_stmt: 'while' test ':' suite ['else' ':' suite] +for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite] +try_stmt: ('try' ':' suite + ((except_clause ':' suite)+ + ['else' ':' suite] + ['finally' ':' suite] | + 'finally' ':' suite)) +with_stmt: 'with' with_item (',' with_item)* ':' suite +with_item: test ['as' expr] +# NB compile.c makes sure that the default except clause is last +except_clause: 'except' [test ['as' NAME]] +suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT + +test: or_test ['if' or_test 'else' test] | lambdef +test_nocond: or_test | lambdef_nocond +lambdef: 'lambda' [varargslist] ':' test +lambdef_nocond: 'lambda' [varargslist] ':' test_nocond +or_test: and_test ('or' and_test)* +and_test: not_test ('and' not_test)* +not_test: 'not' not_test | comparison +comparison: expr (comp_op expr)* +# <> isn't actually a valid comparison operator in Python. It's here for the +# sake of a __future__ import described in PEP 401 (which really works :-) +comp_op: '<'|'>'|'=='|'>='|'<='|'<>'|'!='|'in'|'not' 'in'|'is'|'is' 'not' +star_expr: '*' expr +expr: xor_expr ('|' xor_expr)* +xor_expr: and_expr ('^' and_expr)* +and_expr: shift_expr ('&' shift_expr)* +shift_expr: arith_expr (('<<'|'>>') arith_expr)* +arith_expr: term (('+'|'-') term)* +term: factor (('*'|'@'|'/'|'%'|'//') factor)* +factor: ('+'|'-'|'~') factor | power +power: atom_expr ['**' factor] +atom_expr: [AWAIT] atom trailer* +atom: ('(' [yield_expr|testlist_comp] ')' | + '[' [testlist_comp] ']' | + '{' [dictorsetmaker] '}' | + NAME | NUMBER | STRING+ | '...' | 'None' | 'True' | 'False') +testlist_comp: (test|star_expr) ( comp_for | (',' (test|star_expr))* [','] ) +trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME +subscriptlist: subscript (',' subscript)* [','] +subscript: test | [test] ':' [test] [sliceop] +sliceop: ':' [test] +exprlist: (expr|star_expr) (',' (expr|star_expr))* [','] +testlist: test (',' test)* [','] +dictorsetmaker: ( ((test ':' test | '**' expr) + (comp_for | (',' (test ':' test | '**' expr))* [','])) | + ((test | star_expr) + (comp_for | (',' (test | star_expr))* [','])) ) + +classdef: 'class' NAME ['(' [arglist] ')'] ':' suite + +arglist: argument (',' argument)* [','] + +# The reason that keywords are test nodes instead of NAME is that using NAME +# results in an ambiguity. ast.c makes sure it's a NAME. +# "test '=' test" is really "keyword '=' test", but we have no such token. +# These need to be in a single rule to avoid grammar that is ambiguous +# to our LL(1) parser. Even though 'test' includes '*expr' in star_expr, +# we explicitly match '*' here, too, to give it proper precedence. +# Illegal combinations and orderings are blocked in ast.c: +# multiple (test comp_for) arguments are blocked; keyword unpackings +# that precede iterable unpackings are blocked; etc. +argument: ( test [comp_for] | + test '=' test | + '**' test | + '*' test ) + +comp_iter: comp_for | comp_if +comp_for: [ASYNC] 'for' exprlist 'in' or_test [comp_iter] +comp_if: 'if' test_nocond [comp_iter] + +# not used in grammar, but may appear in "node" passed from Parser to Compiler +encoding_decl: NAME + +yield_expr: 'yield' [yield_arg] +yield_arg: 'from' test | testlist diff --git a/pypy/interpreter/pyparser/pygram.py b/pypy/interpreter/pyparser/pygram.py --- a/pypy/interpreter/pyparser/pygram.py +++ b/pypy/interpreter/pyparser/pygram.py @@ -9,7 +9,7 @@ def _get_python_grammar(): here = os.path.dirname(__file__) - fp = open(os.path.join(here, "data", "Grammar3.5")) + fp = open(os.path.join(here, "data", "Grammar3.6")) try: gram_source = fp.read() finally: diff --git a/pypy/interpreter/test/test_annotations.py b/pypy/interpreter/test/test_annotations.py new file mode 100644 --- /dev/null +++ b/pypy/interpreter/test/test_annotations.py @@ -0,0 +1,113 @@ +class AppTestAnnotations: + + def test_toplevel_annotation(self): + # exec because this needs to be in "top level" scope + # whereas the docstring-based tests are inside a function + # (or don't care) + exec("a: int; assert __annotations__['a'] == int") + + def test_toplevel_invalid(self): + exec('try: a: invalid\nexcept NameError: pass\n') + + def test_non_simple_annotation(self): + ''' + class C: + (a): int + assert "a" not in __annotations__ + ''' + + def test_simple_with_target(self): + ''' + class C: + a: int = 1 + assert __annotations__["a"] == int + assert a == 1 + ''' + + def test_attribute_target(self): + ''' + class C: + a = 1 + a.x: int + assert __annotations__ == {} + ''' + + def test_subscript_target(self): + ''' + # ensure that these type annotations don't raise exceptions + # during compilation + class C: + a = 1 + a[0]: int + a[1:2]: int + a[1:2:2]: int + a[1:2:2,...]: int + assert __annotations__ == {} + ''' + + def test_class_annotation(self): + ''' + class C: + a: int + b: str + assert "__annotations__" in locals() + assert C.__annotations__ == {"a": int, "b": str} + ''' + + def test_unevaluated_name(self): + ''' + class C: + def __init__(self): + self.x: invalid_name = 1 + assert self.x == 1 + C() + ''' + + def test_nonexistent_target(self): + ''' + try: + # this is invalid because `y` is undefined + # it should raise a NameError + y[0]: invalid + except NameError: + ... + ''' + + def test_repeated_setup(self): + # each exec will run another SETUP_ANNOTATIONS + # we want to confirm that this doesn't blow away + # the previous __annotations__ + d = {} + exec('a: int', d) + exec('b: int', d) + exec('assert __annotations__ == {"a": int, "b": int}', d) + + def test_function_no___annotations__(self): + ''' + a: int + assert "__annotations__" not in locals() + ''' + + def test_unboundlocal(self): + # a simple variable annotation implies its target is a local + ''' + a: int + try: + print(a) + except UnboundLocalError: + return + assert False + ''' + + def test_reassigned___annotations__(self): + ''' + class C: + __annotations__ = None + try: + a: int + raise + except TypeError: + pass + except: + assert False + ''' _______________________________________________ pypy-commit mailing list pypy-commit@python.org https://mail.python.org/mailman/listinfo/pypy-commit