Commit c372d2db authored by Stefan Behnel's avatar Stefan Behnel

Implement @cython.exceptval() decorator to make the "except x" signature...

Implement @cython.exceptval() decorator to make the "except x" signature declaration available in pure Python mode.
Closes #1653.
parent 4ed0f663
...@@ -27,6 +27,10 @@ Features added ...@@ -27,6 +27,10 @@ Features added
types. This can be disabled with the directive ``annotation_typing=False``. types. This can be disabled with the directive ``annotation_typing=False``.
(Github issue #1850) (Github issue #1850)
* New decorator ``@cython.exceptval(x=None, check=False)`` that makes the signature
declarations ``except x``, ``except? x`` and ``except *`` available to pure Python
code. Original patch by Antonio Cuni. (Github issue #1653)
* Signature annotations are now included in the signature docstring generated by * Signature annotations are now included in the signature docstring generated by
the ``embedsignature`` directive. Patch by Lisandro Dalcin (Github issue #1781). the ``embedsignature`` directive. Patch by Lisandro Dalcin (Github issue #1781).
......
...@@ -2710,11 +2710,13 @@ class DefNode(FuncDefNode): ...@@ -2710,11 +2710,13 @@ class DefNode(FuncDefNode):
self.num_required_kw_args = rk self.num_required_kw_args = rk
self.num_required_args = r self.num_required_args = r
def as_cfunction(self, cfunc=None, scope=None, overridable=True, returns=None, modifiers=None): def as_cfunction(self, cfunc=None, scope=None, overridable=True, returns=None, except_val=None, modifiers=None):
if self.star_arg: if self.star_arg:
error(self.star_arg.pos, "cdef function cannot have star argument") error(self.star_arg.pos, "cdef function cannot have star argument")
if self.starstar_arg: if self.starstar_arg:
error(self.starstar_arg.pos, "cdef function cannot have starstar argument") error(self.starstar_arg.pos, "cdef function cannot have starstar argument")
exception_value, exception_check = except_val or (None, False)
if cfunc is None: if cfunc is None:
cfunc_args = [] cfunc_args = []
for formal_arg in self.args: for formal_arg in self.args:
...@@ -2727,7 +2729,7 @@ class DefNode(FuncDefNode): ...@@ -2727,7 +2729,7 @@ class DefNode(FuncDefNode):
args=cfunc_args, args=cfunc_args,
has_varargs=False, has_varargs=False,
exception_value=None, exception_value=None,
exception_check=False, exception_check=exception_check,
nogil=False, nogil=False,
with_gil=False, with_gil=False,
is_overridable=overridable) is_overridable=overridable)
...@@ -2745,11 +2747,10 @@ class DefNode(FuncDefNode): ...@@ -2745,11 +2747,10 @@ class DefNode(FuncDefNode):
if type is None or type is PyrexTypes.py_object_type: if type is None or type is PyrexTypes.py_object_type:
formal_arg.type = type_arg.type formal_arg.type = type_arg.type
formal_arg.name_declarator = name_declarator formal_arg.name_declarator = name_declarator
from . import ExprNodes
if cfunc_type.exception_value is None: if exception_value is None and cfunc_type.exception_value is not None:
exception_value = None from .ExprNodes import ConstNode
else: exception_value = ConstNode(
exception_value = ExprNodes.ConstNode(
self.pos, value=cfunc_type.exception_value, type=cfunc_type.return_type) self.pos, value=cfunc_type.exception_value, type=cfunc_type.return_type)
declarator = CFuncDeclaratorNode(self.pos, declarator = CFuncDeclaratorNode(self.pos,
base=CNameDeclaratorNode(self.pos, name=self.name, cname=None), base=CNameDeclaratorNode(self.pos, name=self.name, cname=None),
......
...@@ -143,6 +143,7 @@ _directive_defaults = { ...@@ -143,6 +143,7 @@ _directive_defaults = {
'initializedcheck' : True, 'initializedcheck' : True,
'embedsignature' : False, 'embedsignature' : False,
'locals' : {}, 'locals' : {},
'exceptval' : (None, False), # (except value, check=False)
'auto_cpdef': False, 'auto_cpdef': False,
'auto_pickle': None, 'auto_pickle': None,
'cdivision': False, # was True before 0.12 'cdivision': False, # was True before 0.12
...@@ -292,6 +293,9 @@ directive_scopes = { # defaults to available everywhere ...@@ -292,6 +293,9 @@ directive_scopes = { # defaults to available everywhere
'auto_pickle': ('module', 'cclass'), 'auto_pickle': ('module', 'cclass'),
'final' : ('cclass', 'function'), 'final' : ('cclass', 'function'),
'inline' : ('function',), 'inline' : ('function',),
'returns' : ('function',),
'exceptval' : ('function',),
'locals' : ('function',),
'staticmethod' : ('function',), # FIXME: analysis currently lacks more specific function scope 'staticmethod' : ('function',), # FIXME: analysis currently lacks more specific function scope
'no_gc_clear' : ('cclass',), 'no_gc_clear' : ('cclass',),
'no_gc' : ('cclass',), 'no_gc' : ('cclass',),
......
...@@ -902,9 +902,25 @@ class InterpretCompilerDirectives(CythonTransform, SkipDeclarations): ...@@ -902,9 +902,25 @@ class InterpretCompilerDirectives(CythonTransform, SkipDeclarations):
return None return None
def try_to_parse_directive(self, optname, args, kwds, pos): def try_to_parse_directive(self, optname, args, kwds, pos):
directivetype = Options.directive_types.get(optname)
if optname == 'np_pythran' and not self.context.cpp: if optname == 'np_pythran' and not self.context.cpp:
raise PostParseError(pos, 'The %s directive can only be used in C++ mode.' % optname) raise PostParseError(pos, 'The %s directive can only be used in C++ mode.' % optname)
elif optname == 'exceptval':
arg_error = len(args) > 1
check = False
if kwds and kwds.key_value_pairs:
kw = kwds.key_value_pairs[0]
if (len(kwds.key_value_pairs) == 1 and
kw.key.is_string_literal and kw.key.value == 'check' and
isinstance(kw.value, ExprNodes.BoolNode)):
check = kw.value.value
else:
arg_error = True
if arg_error:
raise PostParseError(
pos, 'The exceptval directive takes 0 or 1 positional arguments and the boolean keyword "check"')
return ('exceptval', (args[0] if args else None, check))
directivetype = Options.directive_types.get(optname)
if len(args) == 1 and isinstance(args[0], ExprNodes.NoneNode): if len(args) == 1 and isinstance(args[0], ExprNodes.NoneNode):
return optname, Options.get_directive_defaults()[optname] return optname, Options.get_directive_defaults()[optname]
elif directivetype is bool: elif directivetype is bool:
...@@ -934,7 +950,7 @@ class InterpretCompilerDirectives(CythonTransform, SkipDeclarations): ...@@ -934,7 +950,7 @@ class InterpretCompilerDirectives(CythonTransform, SkipDeclarations):
'The %s directive takes no prepositional arguments' % optname) 'The %s directive takes no prepositional arguments' % optname)
return optname, dict([(key.value, value) for key, value in kwds.key_value_pairs]) return optname, dict([(key.value, value) for key, value in kwds.key_value_pairs])
elif directivetype is list: elif directivetype is list:
if kwds and len(kwds) != 0: if kwds and len(kwds.key_value_pairs) != 0:
raise PostParseError(pos, raise PostParseError(pos,
'The %s directive takes no keyword arguments' % optname) 'The %s directive takes no keyword arguments' % optname)
return optname, [ str(arg.value) for arg in args ] return optname, [ str(arg.value) for arg in args ]
...@@ -2324,19 +2340,22 @@ class AdjustDefByDirectives(CythonTransform, SkipDeclarations): ...@@ -2324,19 +2340,22 @@ class AdjustDefByDirectives(CythonTransform, SkipDeclarations):
modifiers = [] modifiers = []
if 'inline' in self.directives: if 'inline' in self.directives:
modifiers.append('inline') modifiers.append('inline')
except_val = self.directives.get('exceptval')
return_type_node = self.directives.get('returns') return_type_node = self.directives.get('returns')
if return_type_node is None and self.directives['annotation_typing']: if return_type_node is None and self.directives['annotation_typing']:
return_type_node = node.return_type_annotation return_type_node = node.return_type_annotation
if 'ccall' in self.directives: if 'ccall' in self.directives:
node = node.as_cfunction( node = node.as_cfunction(
overridable=True, returns=return_type_node, modifiers=modifiers) overridable=True, modifiers=modifiers,
returns=return_type_node, except_val=except_val)
return self.visit(node) return self.visit(node)
if 'cfunc' in self.directives: if 'cfunc' in self.directives:
if self.in_py_class: if self.in_py_class:
error(node.pos, "cfunc directive is not allowed here") error(node.pos, "cfunc directive is not allowed here")
else: else:
node = node.as_cfunction( node = node.as_cfunction(
overridable=False, returns=return_type_node, modifiers=modifiers) overridable=False, modifiers=modifiers,
returns=return_type_node, except_val=except_val)
return self.visit(node) return self.visit(node)
if 'inline' in modifiers: if 'inline' in modifiers:
error(node.pos, "Python functions cannot be declared 'inline'") error(node.pos, "Python functions cannot be declared 'inline'")
......
...@@ -111,7 +111,9 @@ returns = wraparound = boundscheck = initializedcheck = nonecheck = \ ...@@ -111,7 +111,9 @@ returns = wraparound = boundscheck = initializedcheck = nonecheck = \
overflowcheck = embedsignature = cdivision = cdivision_warnings = \ overflowcheck = embedsignature = cdivision = cdivision_warnings = \
always_allows_keywords = profile = linetrace = infer_type = \ always_allows_keywords = profile = linetrace = infer_type = \
unraisable_tracebacks = freelist = \ unraisable_tracebacks = freelist = \
lambda arg: _EmptyDecoratorAndManager() lambda _: _EmptyDecoratorAndManager()
exceptval = lambda _=None, check=False: _EmptyDecoratorAndManager()
optimization = _Optimization() optimization = _Optimization()
......
...@@ -195,6 +195,14 @@ Static typing ...@@ -195,6 +195,14 @@ Static typing
* ``@cython.returns(<type>)`` specifies the function's return type. * ``@cython.returns(<type>)`` specifies the function's return type.
* ``@cython.exceptval(value=None, *, check=False)`` specifies the function's exception
return value and exception check semantics as follows::
@exceptval(-1) # cdef int func() except -1:
@exceptval(-1, check=False) # cdef int func() except -1:
@exceptval(check=True) # cdef int func() except *:
@exceptval(-1, check=True) # cdef int func() except? -1:
* Python annotations can be used to declare argument types, as shown in the * Python annotations can be used to declare argument types, as shown in the
following example. To avoid conflicts with other kinds of annotation following example. To avoid conflicts with other kinds of annotation
usages, this can be disabled with the directive ``annotation_typing=False``. usages, this can be disabled with the directive ``annotation_typing=False``.
...@@ -204,6 +212,15 @@ Static typing ...@@ -204,6 +212,15 @@ Static typing
def func(a_pydict: dict, a_cint: cython.int) -> tuple: def func(a_pydict: dict, a_cint: cython.int) -> tuple:
... ...
This can be combined with the ``@cython.exceptval()`` decorator for non-Python
return types::
@cython.exceptval(-1):
def func(x : cython.int) -> cython.int:
if x < 0:
raise ValueError("need integer >= 0")
return x+1
Since version 0.27, Cython also supports the variable annotations defined Since version 0.27, Cython also supports the variable annotations defined
in `PEP 526 <https://www.python.org/dev/peps/pep-0526/>`_. This allows to in `PEP 526 <https://www.python.org/dev/peps/pep-0526/>`_. This allows to
declare types of variables in a Python 3.6 compatible way as follows:: declare types of variables in a Python 3.6 compatible way as follows::
......
...@@ -266,3 +266,78 @@ def count_digits_in_carray(digits): ...@@ -266,3 +266,78 @@ def count_digits_in_carray(digits):
assert 0 <= digit <= 9 assert 0 <= digit <= 9
counts[digit] += 1 counts[digit] += 1
return counts return counts
@cython.test_assert_path_exists("//CFuncDeclaratorNode//IntNode[@value = '-1']")
@cython.ccall
@cython.returns(cython.long)
@cython.exceptval(-1)
def ccall_except(x):
"""
>>> ccall_except(41)
42
>>> ccall_except(0)
Traceback (most recent call last):
ValueError
"""
if x == 0:
raise ValueError
return x+1
@cython.test_assert_path_exists("//CFuncDeclaratorNode//IntNode[@value = '-1']")
@cython.cfunc
@cython.returns(cython.long)
@cython.exceptval(-1)
def cdef_except(x):
"""
>>> call_cdef_except(41)
42
>>> call_cdef_except(0)
Traceback (most recent call last):
ValueError
"""
if x == 0:
raise ValueError
return x+1
def call_cdef_except(x):
return cdef_except(x)
@cython.test_assert_path_exists("//CFuncDeclaratorNode//IntNode[@value = '-1']")
@cython.ccall
@cython.returns(cython.long)
@cython.exceptval(-1, check=True)
def ccall_except_check(x):
"""
>>> ccall_except_check(41)
42
>>> ccall_except_check(-2)
-1
>>> ccall_except_check(0)
Traceback (most recent call last):
ValueError
"""
if x == 0:
raise ValueError
return x+1
@cython.test_fail_if_path_exists("//CFuncDeclaratorNode//IntNode[@value = '-1']")
@cython.test_assert_path_exists("//CFuncDeclaratorNode")
@cython.ccall
@cython.returns(cython.long)
@cython.exceptval(check=True)
def ccall_except_check_always(x):
"""
>>> ccall_except_check_always(41)
42
>>> ccall_except_check_always(0)
Traceback (most recent call last):
ValueError
"""
if x == 0:
raise ValueError
return x+1
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