[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