[pypy-commit] pypy py3.6: Merged in alcarithemad/pypy/pep526 (pull request #595)

arigo pypy.commits at gmail.com
Thu Mar 1 05:07:26 EST 2018


Author: Armin Rigo <armin.rigo at 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.rigo at gmail.com> Approved-by: Amaury
	Forgeot d'Arc <amauryfa at 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
+        '''


More information about the pypy-commit mailing list