Commit c0b299e2 authored by Stefan Behnel's avatar Stefan Behnel

Implement PEP 526: syntax for variable annotations.

Also parses variable annotations as type declarations.
Closes #1850.
parent 5ccc9e46
...@@ -254,9 +254,11 @@ class ExprNode(Node): ...@@ -254,9 +254,11 @@ class ExprNode(Node):
# result_is_used boolean indicates that the result will be dropped and the # result_is_used boolean indicates that the result will be dropped and the
# is_numpy_attribute boolean Is a Numpy module attribute # is_numpy_attribute boolean Is a Numpy module attribute
# result_code/temp_result can safely be set to None # result_code/temp_result can safely be set to None
# annotation ExprNode or None PEP526 annotation for names or expressions
result_ctype = None result_ctype = None
type = None type = None
annotation = None
temp_code = None temp_code = None
old_temp = None # error checker for multiple frees etc. old_temp = None # error checker for multiple frees etc.
use_managed_ref = True # can be set by optimisation transforms use_managed_ref = True # can be set by optimisation transforms
...@@ -1830,6 +1832,38 @@ class NameNode(AtomicExprNode): ...@@ -1830,6 +1832,38 @@ class NameNode(AtomicExprNode):
return super(NameNode, self).coerce_to(dst_type, env) return super(NameNode, self).coerce_to(dst_type, env)
def declare_from_annotation(self, env, as_target=False):
"""Implements PEP 526 annotation typing in a fairly relaxed way.
Annotations are ignored for global variables, Python class attributes and already declared variables.
String literals are allowed and ignored.
The ambiguous Python types 'int' and 'long' are ignored and the 'cython.int' form must be used instead.
"""
if env.is_module_scope or env.is_py_class_scope:
# annotations never create global cdef names and Python classes don't support them anyway
return
name = self.name
if self.entry or env.lookup_here(name) is not None:
# already declared => ignore annotation
return
annotation = self.annotation
atype = annotation.analyse_as_type(env)
if annotation.is_name and not annotation.cython_attribute and annotation.name in ('int', 'long', 'float'):
# ignore 'int' and require 'cython.int' to avoid unsafe integer declarations
if atype in (PyrexTypes.c_long_type, PyrexTypes.c_int_type, PyrexTypes.c_float_type):
atype = PyrexTypes.c_double_type if annotation.name == 'float' else py_object_type
elif annotation.is_string_literal:
# name: "description" => not a type, but still a declared variable or attribute
atype = None
elif atype is None:
# annotations always make variables local => ignore and leave to type inference
warning(annotation.pos, "Unknown type declaration in annotation, ignoring")
if atype is None:
atype = unspecified_type if as_target and env.directives['infer_types'] != False else py_object_type
self.entry = env.declare_var(name, atype, self.pos, is_cdef=not as_target)
def analyse_as_module(self, env): def analyse_as_module(self, env):
# Try to interpret this as a reference to a cimported module. # Try to interpret this as a reference to a cimported module.
# Returns the module scope, or None. # Returns the module scope, or None.
...@@ -1869,6 +1903,9 @@ class NameNode(AtomicExprNode): ...@@ -1869,6 +1903,9 @@ class NameNode(AtomicExprNode):
def analyse_target_declaration(self, env): def analyse_target_declaration(self, env):
if not self.entry: if not self.entry:
self.entry = env.lookup_here(self.name) self.entry = env.lookup_here(self.name)
if not self.entry and self.annotation is not None:
# name : type = ...
self.declare_from_annotation(env, as_target=True)
if not self.entry: if not self.entry:
if env.directives['warn.undeclared']: if env.directives['warn.undeclared']:
warning(self.pos, "implicit declaration of '%s'" % self.name, 1) warning(self.pos, "implicit declaration of '%s'" % self.name, 1)
......
...@@ -4772,12 +4772,13 @@ class ExprStatNode(StatNode): ...@@ -4772,12 +4772,13 @@ class ExprStatNode(StatNode):
def analyse_declarations(self, env): def analyse_declarations(self, env):
from . import ExprNodes from . import ExprNodes
if isinstance(self.expr, ExprNodes.GeneralCallNode): expr = self.expr
func = self.expr.function.as_cython_attribute() if isinstance(expr, ExprNodes.GeneralCallNode):
func = expr.function.as_cython_attribute()
if func == u'declare': if func == u'declare':
args, kwds = self.expr.explicit_args_kwds() args, kwds = expr.explicit_args_kwds()
if len(args): if len(args):
error(self.expr.pos, "Variable names must be specified.") error(expr.pos, "Variable names must be specified.")
for var, type_node in kwds.key_value_pairs: for var, type_node in kwds.key_value_pairs:
type = type_node.analyse_as_type(env) type = type_node.analyse_as_type(env)
if type is None: if type is None:
...@@ -4785,6 +4786,14 @@ class ExprStatNode(StatNode): ...@@ -4785,6 +4786,14 @@ class ExprStatNode(StatNode):
else: else:
env.declare_var(var.value, type, var.pos, is_cdef=True) env.declare_var(var.value, type, var.pos, is_cdef=True)
self.__class__ = PassStatNode self.__class__ = PassStatNode
elif expr.annotation is not None:
if expr.is_name:
# non-code variable annotation, e.g. "name: type"
expr.declare_from_annotation(env)
self.__class__ = PassStatNode
elif expr.is_attribute or expr.is_subscript:
# unused expression with annotation, e.g. "a[0]: type" or "a.xyz : type"
self.__class__ = PassStatNode
def analyse_expressions(self, env): def analyse_expressions(self, env):
self.expr.result_is_used = False # hint that .result() may safely be left empty self.expr.result_is_used = False # hint that .result() may safely be left empty
......
...@@ -1485,13 +1485,17 @@ def p_nonlocal_statement(s): ...@@ -1485,13 +1485,17 @@ def p_nonlocal_statement(s):
def p_expression_or_assignment(s): def p_expression_or_assignment(s):
expr_list = [p_testlist_star_expr(s)] expr = p_testlist_star_expr(s)
if s.sy == '=' and expr_list[0].is_starred: if s.sy == ':' and (expr.is_name or expr.is_subscript or expr.is_attribute):
s.next()
expr.annotation = p_test(s)
if s.sy == '=' and expr.is_starred:
# This is a common enough error to make when learning Cython to let # This is a common enough error to make when learning Cython to let
# it fail as early as possible and give a very clear error message. # it fail as early as possible and give a very clear error message.
s.error("a starred assignment target must be in a list or tuple" s.error("a starred assignment target must be in a list or tuple"
" - maybe you meant to use an index assignment: var[0] = ...", " - maybe you meant to use an index assignment: var[0] = ...",
pos=expr_list[0].pos) pos=expr.pos)
expr_list = [expr]
while s.sy == '=': while s.sy == '=':
s.next() s.next()
if s.sy == 'yield': if s.sy == 'yield':
......
...@@ -346,6 +346,7 @@ VER_DEP_MODULES = { ...@@ -346,6 +346,7 @@ VER_DEP_MODULES = {
]), ]),
(3,5): (operator.lt, lambda x: x in ['run.py35_pep492_interop', (3,5): (operator.lt, lambda x: x in ['run.py35_pep492_interop',
'run.mod__spec__', 'run.mod__spec__',
'run.pep526_variable_annotations', # typing module
]), ]),
} }
...@@ -578,9 +579,16 @@ class TestBuilder(object): ...@@ -578,9 +579,16 @@ class TestBuilder(object):
for test in self.build_tests(test_class, path, workdir, for test in self.build_tests(test_class, path, workdir,
module, mode == 'error', tags): module, mode == 'error', tags):
suite.addTest(test) suite.addTest(test)
if mode == 'run' and ext == '.py' and not self.cython_only and not filename.startswith('test_'): if mode == 'run' and ext == '.py' and not self.cython_only and not filename.startswith('test_'):
# additionally test file in real Python # additionally test file in real Python
suite.addTest(PureDoctestTestCase(module, os.path.join(path, filename))) min_py_ver = [
(int(pyver.group(1)), int(pyver.group(2)))
for pyver in map(re.compile(r'pure([0-9]+)[.]([0-9]+)').match, tags['tag'])
if pyver
]
if not min_py_ver or any(sys.version_info >= min_ver for min_ver in min_py_ver):
suite.addTest(PureDoctestTestCase(module, os.path.join(path, filename)))
return suite return suite
......
...@@ -513,4 +513,6 @@ def annotation_syntax(a: "test new test", b : "other" = 2, *args: "ARGS", **kwar ...@@ -513,4 +513,6 @@ def annotation_syntax(a: "test new test", b : "other" = 2, *args: "ARGS", **kwar
>>> print(annotation_syntax.__annotations__['return']) >>> print(annotation_syntax.__annotations__['return'])
ret ret
""" """
return a+b result : int = a + b
return result
# mode: run
# tag: pure3.6, warnings
# cython: language_level=3
import cython
from typing import Dict, List, TypeVar, Optional, Generic, Tuple
try:
from typing import ClassVar
except ImportError:
ClassVar = Optional # fake it in Py3.5
var = 1 # type: annotation
var: int = 2
fvar: float = 1.2
some_number: cython.int # variable without initial value
some_list: List[int] = [] # variable with initial value
t: Tuple[int, ...] = (1, 2, 3)
body: Optional[List[str]]
descr_only : "descriptions are allowed but ignored"
some_number = 5
body = None
def f():
"""
>>> f()
(2, 1.5, [], (1, 2, 3))
"""
var = 1 # type: annotation
var: int = 2
fvar: float = 1.5
some_number: cython.int # variable without initial value
some_list: List[int] = [] # variable with initial value
t: Tuple[int, ...] = (1, 2, 3)
body: Optional[List[str]]
descr_only: "descriptions are allowed but ignored"
return var, fvar, some_list, t
class BasicStarship(object):
"""
>>> bs = BasicStarship(5)
>>> bs.damage
5
>>> bs.captain
'Picard'
>>> bs.stats
{}
"""
captain: str = 'Picard' # instance variable with default
damage: cython.int # instance variable without default
stats: ClassVar[Dict[str, int]] = {} # class variable
descr_only: "descriptions are allowed but ignored"
def __init__(self, damage):
self.damage = damage
@cython.cclass
class BasicStarshipExt(object):
"""
>>> bs = BasicStarshipExt(5)
>>> bs.test()
(5, 'Picard', {})
"""
captain: str = 'Picard' # instance variable with default
damage: cython.int # instance variable without default
stats: ClassVar[Dict[str, int]] = {} # class variable
descr_only: "descriptions are allowed but ignored"
def __init__(self, damage):
self.damage = damage
def test(self):
return self.damage, self.captain, self.stats
T = TypeVar('T')
class Box(Generic[T]):
def __init__(self, content):
self.content: T = content
box = Box(content=5)
class Cls(object):
pass
c = Cls()
c.x: int = 0 # Annotates c.x with int.
c.y: int # Annotates c.y with int.
d = {}
d['a']: int = 0 # Annotates d['a'] with int.
d['b']: int # Annotates d['b'] with int.
(x): int # Annotates x with int, (x) treated as expression by compiler.
(y): int = 0 # Same situation here.
_WARNINGS = """
37:19: Unknown type declaration in annotation, ignoring
38:12: Unknown type declaration in annotation, ignoring
39:18: Unknown type declaration in annotation, ignoring
73:19: Unknown type declaration in annotation, ignoring
"""
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment