Commit c036607a authored by Stefan Behnel's avatar Stefan Behnel

implement support for extracting type declarations from signature annotations

parent cbe735b8
...@@ -9,6 +9,9 @@ Latest ...@@ -9,6 +9,9 @@ Latest
Features added Features added
-------------- --------------
* Simple support for declaring Python object types in Python signature
annotations.
Bugs fixed Bugs fixed
---------- ----------
......
...@@ -289,15 +289,16 @@ class ExprNode(Node): ...@@ -289,15 +289,16 @@ class ExprNode(Node):
# #
# #
is_sequence_constructor = 0 is_sequence_constructor = False
is_string_literal = 0 is_dict_literal = False
is_attribute = 0 is_string_literal = False
is_subscript = 0 is_attribute = False
is_subscript = False
saved_subexpr_nodes = None saved_subexpr_nodes = None
is_temp = 0 is_temp = False
is_target = 0 is_target = False
is_starred = 0 is_starred = False
constant_result = constant_value_not_set constant_result = constant_value_not_set
...@@ -1174,6 +1175,20 @@ class FloatNode(ConstNode): ...@@ -1174,6 +1175,20 @@ class FloatNode(ConstNode):
self.result_code = c_value self.result_code = c_value
def _analyse_name_as_type(name, pos, env):
type = PyrexTypes.parse_basic_type(name)
if type is not None:
return type
from TreeFragment import TreeFragment
pos = (pos[0], pos[1], pos[2]-7)
declaration = TreeFragment(u"sizeof(%s)" % name, name=pos[0].filename, initial_pos=pos)
sizeof_node = declaration.root.stats[0].expr
sizeof_node = sizeof_node.analyse_types(env)
if isinstance(sizeof_node, SizeofTypeNode):
return sizeof_node.arg_type
return None
class BytesNode(ConstNode): class BytesNode(ConstNode):
# A char* or bytes literal # A char* or bytes literal
# #
...@@ -1196,16 +1211,7 @@ class BytesNode(ConstNode): ...@@ -1196,16 +1211,7 @@ class BytesNode(ConstNode):
return self.value return self.value
def analyse_as_type(self, env): def analyse_as_type(self, env):
type = PyrexTypes.parse_basic_type(self.value) return _analyse_name_as_type(self.value.decode('ISO8859-1'), self.pos, env)
if type is not None:
return type
from TreeFragment import TreeFragment
pos = (self.pos[0], self.pos[1], self.pos[2]-7)
declaration = TreeFragment(u"sizeof(%s)" % self.value, name=pos[0].filename, initial_pos=pos)
sizeof_node = declaration.root.stats[0].expr
sizeof_node = sizeof_node.analyse_types(env)
if isinstance(sizeof_node, SizeofTypeNode):
return sizeof_node.arg_type
def can_coerce_to_char_literal(self): def can_coerce_to_char_literal(self):
return len(self.value) == 1 return len(self.value) == 1
...@@ -1279,6 +1285,9 @@ class UnicodeNode(ConstNode): ...@@ -1279,6 +1285,9 @@ class UnicodeNode(ConstNode):
def calculate_constant_result(self): def calculate_constant_result(self):
self.constant_result = self.value self.constant_result = self.value
def analyse_as_type(self, env):
return _analyse_name_as_type(self.value, self.pos, env)
def as_sliced_node(self, start, stop, step=None): def as_sliced_node(self, start, stop, step=None):
if StringEncoding.string_contains_surrogates(self.value[:stop]): if StringEncoding.string_contains_surrogates(self.value[:stop]):
# this is unsafe as it may give different results # this is unsafe as it may give different results
...@@ -1388,6 +1397,9 @@ class StringNode(PyConstNode): ...@@ -1388,6 +1397,9 @@ class StringNode(PyConstNode):
# only the Unicode value is portable across Py2/3 # only the Unicode value is portable across Py2/3
self.constant_result = self.unicode_value self.constant_result = self.unicode_value
def analyse_as_type(self, env):
return _analyse_name_as_type(self.unicode_value or self.value.decode('ISO8859-1'), self.pos, env)
def as_sliced_node(self, start, stop, step=None): def as_sliced_node(self, start, stop, step=None):
value = type(self.value)(self.value[start:stop:step]) value = type(self.value)(self.value[start:stop:step])
value.encoding = self.value.encoding value.encoding = self.value.encoding
...@@ -6706,6 +6718,7 @@ class DictNode(ExprNode): ...@@ -6706,6 +6718,7 @@ class DictNode(ExprNode):
is_temp = 1 is_temp = 1
exclude_null_values = False exclude_null_values = False
type = dict_type type = dict_type
is_dict_literal = True
obj_conversion_errors = [] obj_conversion_errors = []
......
...@@ -612,8 +612,8 @@ class CFuncDeclaratorNode(CDeclaratorNode): ...@@ -612,8 +612,8 @@ class CFuncDeclaratorNode(CDeclaratorNode):
nonempty -= 1 nonempty -= 1
func_type_args = [] func_type_args = []
for i, arg_node in enumerate(self.args): for i, arg_node in enumerate(self.args):
name_declarator, type = arg_node.analyse(env, nonempty = nonempty, name_declarator, type = arg_node.analyse(
is_self_arg = (i == 0 and env.is_c_class_scope)) env, nonempty=nonempty, is_self_arg=(i == 0 and env.is_c_class_scope))
name = name_declarator.name name = name_declarator.name
if name in directive_locals: if name in directive_locals:
type_node = directive_locals[name] type_node = directive_locals[name]
...@@ -800,7 +800,7 @@ class CArgDeclNode(Node): ...@@ -800,7 +800,7 @@ class CArgDeclNode(Node):
if nonempty: if nonempty:
if self.base_type.is_basic_c_type: if self.base_type.is_basic_c_type:
# char, short, long called "int" # char, short, long called "int"
type = self.base_type.analyse(env, could_be_name = True) type = self.base_type.analyse(env, could_be_name=True)
arg_name = type.declaration_code("") arg_name = type.declaration_code("")
else: else:
arg_name = self.base_type.name arg_name = self.base_type.name
...@@ -811,9 +811,10 @@ class CArgDeclNode(Node): ...@@ -811,9 +811,10 @@ class CArgDeclNode(Node):
else: else:
could_be_name = False could_be_name = False
self.base_type.is_arg = True self.base_type.is_arg = True
base_type = self.base_type.analyse(env, could_be_name = could_be_name) base_type = self.base_type.analyse(env, could_be_name=could_be_name)
if hasattr(self.base_type, 'arg_name') and self.base_type.arg_name: if hasattr(self.base_type, 'arg_name') and self.base_type.arg_name:
self.declarator.name = self.base_type.arg_name self.declarator.name = self.base_type.arg_name
# The parser is unable to resolve the ambiguity of [] as part of the # The parser is unable to resolve the ambiguity of [] as part of the
# type (e.g. in buffers) or empty declarator (as with arrays). # type (e.g. in buffers) or empty declarator (as with arrays).
# This is only arises for empty multi-dimensional arrays. # This is only arises for empty multi-dimensional arrays.
...@@ -825,10 +826,45 @@ class CArgDeclNode(Node): ...@@ -825,10 +826,45 @@ class CArgDeclNode(Node):
declarator = declarator.base declarator = declarator.base
declarator.base = self.base_type.array_declarator declarator.base = self.base_type.array_declarator
base_type = base_type.base_type base_type = base_type.base_type
return self.declarator.analyse(base_type, env, nonempty = nonempty)
# inject type declaration from annotations
if self.annotation and env.directives['annotation_typing'] and self.base_type.name is None:
arg_type = self.inject_type_from_annotations(env)
if arg_type is not None:
base_type = arg_type
return self.declarator.analyse(base_type, env, nonempty=nonempty)
else: else:
return self.name_declarator, self.type return self.name_declarator, self.type
def inject_type_from_annotations(self, env):
annotation = self.annotation
if not annotation:
return
explicit_pytype = explicit_ctype = False
if annotation.is_dict_literal:
for name, value in annotation.key_value_pairs:
if not name.is_string_literal:
continue
if name.value == 'type':
explicit_pytype = True
if not explicit_ctype:
annotation = value
elif name.value == 'ctype':
explicit_ctype = True
annotation = value
if explicit_pytype and explicit_ctype:
warning(annotation.pos, "Duplicate type declarations found in signature annotation")
arg_type = annotation.analyse_as_type(env)
if arg_type is not None:
if explicit_pytype and not explicit_ctype and not arg_type.is_pyobject:
warning(annotation.pos,
"Python type declaration in signature annotation does not refer to a Python type")
self.base_type = CAnalysedBaseTypeNode(
annotation.pos, type=arg_type, is_arg=True)
else:
warning(annotation.pos, "Unknown type declaration found in signature annotation")
return arg_type
def calculate_default_value_code(self, code): def calculate_default_value_code(self, code):
if self.default_value is None: if self.default_value is None:
if self.default: if self.default:
...@@ -1524,15 +1560,21 @@ class FuncDefNode(StatNode, BlockNode): ...@@ -1524,15 +1560,21 @@ class FuncDefNode(StatNode, BlockNode):
error(arg.pos, "Non-default argument following default argument") error(arg.pos, "Non-default argument following default argument")
def align_argument_type(self, env, arg): def align_argument_type(self, env, arg):
# @cython.locals()
directive_locals = self.directive_locals directive_locals = self.directive_locals
type = arg.type orig_type = arg.type
if arg.name in directive_locals: if arg.name in directive_locals:
type_node = directive_locals[arg.name] type_node = directive_locals[arg.name]
other_type = type_node.analyse_as_type(env) other_type = type_node.analyse_as_type(env)
elif isinstance(arg, CArgDeclNode) and arg.annotation:
type_node = arg.annotation
other_type = arg.inject_type_from_annotations(env)
else:
return arg
if other_type is None: if other_type is None:
error(type_node.pos, "Not a type") error(type_node.pos, "Not a type")
elif (type is not PyrexTypes.py_object_type elif (orig_type is not PyrexTypes.py_object_type
and not type.same_as(other_type)): and not orig_type.same_as(other_type)):
error(arg.base_type.pos, "Signature does not agree with previous declaration") error(arg.base_type.pos, "Signature does not agree with previous declaration")
error(type_node.pos, "Previous declaration here") error(type_node.pos, "Previous declaration here")
else: else:
......
...@@ -101,6 +101,7 @@ directive_defaults = { ...@@ -101,6 +101,7 @@ directive_defaults = {
'profile': False, 'profile': False,
'no_gc_clear': False, 'no_gc_clear': False,
'linetrace': False, 'linetrace': False,
'annotation_typing': False, # read type declarations from Python function annotations
'infer_types': None, 'infer_types': None,
'infer_types.verbose': False, 'infer_types.verbose': False,
'autotestdict': True, 'autotestdict': True,
...@@ -228,6 +229,7 @@ directive_scopes = { # defaults to available everywhere ...@@ -228,6 +229,7 @@ directive_scopes = { # defaults to available everywhere
'test_assert_path_exists' : ('function', 'class', 'cclass'), 'test_assert_path_exists' : ('function', 'class', 'cclass'),
'test_fail_if_path_exists' : ('function', 'class', 'cclass'), 'test_fail_if_path_exists' : ('function', 'class', 'cclass'),
'freelist': ('cclass',), 'freelist': ('cclass',),
'annotation_typing': ('module',), # FIXME: analysis currently lacks more specific function scope
# Avoid scope-specific to/from_py_functions for c_string. # Avoid scope-specific to/from_py_functions for c_string.
'c_string_type': ('module',), 'c_string_type': ('module',),
'c_string_encoding': ('module',), 'c_string_encoding': ('module',),
......
...@@ -36,6 +36,16 @@ The currently supported attributes of the ``cython`` module are: ...@@ -36,6 +36,16 @@ The currently supported attributes of the ``cython`` module are:
def foo(a, b, x, y): def foo(a, b, x, y):
... ...
* Starting with Cython 0.21, Python signature annotations can be used to
declare argument types. Cython recognises three ways to do this, as
shown in the following example::
def func(plain_python_type: dict,
named_python_type: 'dict',
explicit_python_type: {'type': dict},
explicit_c_type: {'ctype': 'int'}):
...
* ``address`` is used in place of the ``&`` operator:: * ``address`` is used in place of the ``&`` operator::
cython.declare(x=cython.int, x_ptr=cython.p_int) cython.declare(x=cython.int, x_ptr=cython.p_int)
......
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