summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatus Valo <matusvalo@users.noreply.github.com>2022-12-06 11:14:42 +0100
committerGitHub <noreply@github.com>2022-12-06 11:14:42 +0100
commitc7aba0e94ff78a4a5e569d412ac72b479534cf64 (patch)
tree775ffc07820aea565c5834a8c5e6ae98c5bd9bd4
parent099faf83e4f5cc351496afef300853e9b2a57ac0 (diff)
downloadcython-c7aba0e94ff78a4a5e569d412ac72b479534cf64.tar.gz
Add compiler directive to disable the default exception propagation for legacy code (GH-5094)
-rw-r--r--Cython/Compiler/Main.py2
-rw-r--r--Cython/Compiler/Options.py5
-rw-r--r--Cython/Compiler/Parsing.py6
-rw-r--r--docs/src/userguide/language_basics.rst4
-rw-r--r--docs/src/userguide/migrating_to_cy30.rst6
-rw-r--r--docs/src/userguide/source_files_and_compilation.rst11
-rw-r--r--tests/run/legacy_implicit_noexcept.pyx143
-rw-r--r--tests/run/legacy_implicit_noexcept_build.srctree33
8 files changed, 205 insertions, 5 deletions
diff --git a/Cython/Compiler/Main.py b/Cython/Compiler/Main.py
index d5985457d..e2aac8ef6 100644
--- a/Cython/Compiler/Main.py
+++ b/Cython/Compiler/Main.py
@@ -91,6 +91,8 @@ class Context(object):
if language_level is not None:
self.set_language_level(language_level)
+ self.legacy_implicit_noexcept = self.compiler_directives.get('legacy_implicit_noexcept', False)
+
self.gdb_debug_outputwriter = None
@classmethod
diff --git a/Cython/Compiler/Options.py b/Cython/Compiler/Options.py
index 73778aaf9..f388fe2b4 100644
--- a/Cython/Compiler/Options.py
+++ b/Cython/Compiler/Options.py
@@ -218,6 +218,7 @@ _directive_defaults = {
'np_pythran': False,
'fast_gil': False,
'cpp_locals': False, # uses std::optional for C++ locals, so that they work more like Python locals
+ 'legacy_implicit_noexcept': False,
# set __file__ and/or __path__ to known source/target path at import time (instead of not having them available)
'set_initial_path' : None, # SOURCEFILE or "/full/path/to/module"
@@ -385,6 +386,7 @@ directive_scopes = { # defaults to available everywhere
'total_ordering': ('cclass', ),
'dataclasses.dataclass' : ('class', 'cclass',),
'cpp_locals': ('module', 'function', 'cclass'), # I don't think they make sense in a with_statement
+ 'legacy_implicit_noexcept': ('module', ),
}
@@ -776,5 +778,6 @@ default_options = dict(
build_dir=None,
cache=None,
create_extension=None,
- np_pythran=False
+ np_pythran=False,
+ legacy_implicit_noexcept=None,
)
diff --git a/Cython/Compiler/Parsing.py b/Cython/Compiler/Parsing.py
index 30d73588d..7c7b7f8a8 100644
--- a/Cython/Compiler/Parsing.py
+++ b/Cython/Compiler/Parsing.py
@@ -3120,6 +3120,9 @@ def p_exception_value_clause(s, ctx):
exc_check = False
# exc_val can be non-None even if exc_check is False, c.f. "except -1"
exc_val = p_test(s)
+ if not exc_clause and ctx.visibility != 'extern' and s.context.legacy_implicit_noexcept:
+ exc_check = False
+ warning(s.position(), "Implicit noexcept declaration is deprecated. Function declaration should contain 'noexcept' keyword.", level=2)
return exc_val, exc_check, exc_clause
c_arg_list_terminators = cython.declare(frozenset, frozenset((
@@ -3888,6 +3891,9 @@ def p_compiler_directive_comments(s):
if 'language_level' in new_directives:
# Make sure we apply the language level already to the first token that follows the comments.
s.context.set_language_level(new_directives['language_level'])
+ if 'legacy_implicit_noexcept' in new_directives:
+ s.context.legacy_implicit_noexcept = new_directives['legacy_implicit_noexcept']
+
result.update(new_directives)
diff --git a/docs/src/userguide/language_basics.rst b/docs/src/userguide/language_basics.rst
index 11561e1ee..ff7007760 100644
--- a/docs/src/userguide/language_basics.rst
+++ b/docs/src/userguide/language_basics.rst
@@ -672,8 +672,8 @@ error return value.
While this is always the case for Python functions, functions
defined as C functions or ``cpdef``/``@ccall`` functions can return arbitrary C types,
which do not have such a well-defined error return value.
-Extra care must be taken to ensure Python exceptions are correctly
-propagated from such functions.
+By default Cython uses a dedicated return value to signal that an exception has been raised from non-external ``cpdef``/``@ccall``
+functions. However, how Cython handles exceptions from these functions can be changed if needed.
A ``cdef`` function may be declared with an exception return value for it
as a contract with the caller. Here is an example:
diff --git a/docs/src/userguide/migrating_to_cy30.rst b/docs/src/userguide/migrating_to_cy30.rst
index 4576ce864..bf0b7972a 100644
--- a/docs/src/userguide/migrating_to_cy30.rst
+++ b/docs/src/userguide/migrating_to_cy30.rst
@@ -210,6 +210,12 @@ The behaviour for any ``cdef`` function that is declared with an
explicit exception value (e.g., ``cdef int spam(int x) except -1``) is
also unchanged.
+.. note::
+ The unsafe legacy behaviour of not propagating exceptions by default can be enabled by
+ setting ``legacy_implicit_noexcept`` :ref:`compiler directive<compiler-directives>`
+ to ``True``.
+
+
Annotation typing
=================
diff --git a/docs/src/userguide/source_files_and_compilation.rst b/docs/src/userguide/source_files_and_compilation.rst
index d1c8f696c..42e092d0a 100644
--- a/docs/src/userguide/source_files_and_compilation.rst
+++ b/docs/src/userguide/source_files_and_compilation.rst
@@ -945,7 +945,7 @@ Cython code. Here is the list of currently supported directives:
asyncio before Python 3.5. This directive can be applied in modules or
selectively as decorator on an async-def coroutine to make the affected
coroutine(s) iterable and thus directly interoperable with yield-from.
-
+
``annotation_typing`` (True / False)
Uses function argument annotations to determine the type of variables. Default
is True, but can be disabled. Since Python does not enforce types given in
@@ -957,12 +957,19 @@ Cython code. Here is the list of currently supported directives:
Copy the original source code line by line into C code comments in the generated
code file to help with understanding the output.
This is also required for coverage analysis.
-
+
``cpp_locals`` (True / False)
Make C++ variables behave more like Python variables by allowing them to be
"unbound" instead of always default-constructing them at the start of a
function. See :ref:`cpp_locals directive` for more detail.
+``legacy_implicit_noexcept`` (True / False)
+ When enabled, ``cdef`` functions will not propagate raised exceptions by default. Hence,
+ the function will behave in the same way as if declared with `noexcept` keyword. See
+ :ref:`error_return_values` for details. Setting this directive to ``True`` will
+ cause Cython 3.0 to have the same semantics as Cython 0.x. This directive was solely added
+ to help migrate legacy code written before Cython 3. It will be removed in a future release.
+
.. _configurable_optimisations:
diff --git a/tests/run/legacy_implicit_noexcept.pyx b/tests/run/legacy_implicit_noexcept.pyx
new file mode 100644
index 000000000..b6799df46
--- /dev/null
+++ b/tests/run/legacy_implicit_noexcept.pyx
@@ -0,0 +1,143 @@
+# cython: legacy_implicit_noexcept=True
+# mode: run
+# tag: warnings
+import sys
+import functools
+import cython
+try:
+ from StringIO import StringIO
+except ImportError:
+ from io import StringIO
+
+cdef int func_implicit(int a, int b):
+ raise RuntimeError
+
+cdef int func_noexcept(int a, int b) noexcept:
+ raise RuntimeError
+
+cdef int func_star(int a, int b) except *:
+ raise RuntimeError
+
+cdef int func_value(int a, int b) except -1:
+ raise RuntimeError
+
+cdef func_return_obj_implicit(int a, int b):
+ raise RuntimeError
+
+cdef int(*ptr_func_implicit)(int, int)
+ptr_func_implicit = func_implicit
+
+cdef int(*ptr_func_noexcept)(int, int) noexcept
+ptr_func_noexcept = func_noexcept
+
+@cython.cfunc
+def func_pure_implicit() -> cython.int:
+ raise RuntimeError
+
+@cython.excetval(check=False)
+@cython.cfunc
+def func_pure_noexcept() -> cython.int:
+ raise RuntimeError
+
+def return_stderr(func):
+ @functools.wraps(func)
+ def testfunc():
+ old_stderr = sys.stderr
+ stderr = sys.stderr = StringIO()
+ try:
+ func()
+ finally:
+ sys.stderr = old_stderr
+ return stderr.getvalue().strip()
+
+ return testfunc
+
+@return_stderr
+def test_noexcept():
+ """
+ >>> print(test_noexcept()) # doctest: +ELLIPSIS
+ RuntimeError
+ Exception...ignored...
+ """
+ func_noexcept(3, 5)
+
+@return_stderr
+def test_ptr_noexcept():
+ """
+ >>> print(test_ptr_noexcept()) # doctest: +ELLIPSIS
+ RuntimeError
+ Exception...ignored...
+ """
+ ptr_func_noexcept(3, 5)
+
+@return_stderr
+def test_implicit():
+ """
+ >>> print(test_implicit()) # doctest: +ELLIPSIS
+ RuntimeError
+ Exception...ignored...
+ """
+ func_implicit(1, 2)
+
+@return_stderr
+def test_ptr_implicit():
+ """
+ >>> print(test_ptr_implicit()) # doctest: +ELLIPSIS
+ RuntimeError
+ Exception...ignored...
+ """
+ ptr_func_implicit(1, 2)
+
+def test_star():
+ """
+ >>> test_star()
+ Traceback (most recent call last):
+ ...
+ RuntimeError
+ """
+ func_star(1, 2)
+
+def test_value():
+ """
+ >>> test_value()
+ Traceback (most recent call last):
+ ...
+ RuntimeError
+ """
+ func_value(1, 2)
+
+
+def test_return_obj_implicit():
+ """
+ >>> test_return_obj_implicit()
+ Traceback (most recent call last):
+ ...
+ RuntimeError
+ """
+ func_return_obj_implicit(1, 2)
+
+def test_pure_implicit():
+ """
+ >>> test_pure_implicit()
+ Traceback (most recent call last):
+ ...
+ RuntimeError
+ """
+ func_pure_implicit()
+
+def test_pure_noexcept():
+ """
+ >>> test_pure_noexcept()
+ Traceback (most recent call last):
+ ...
+ RuntimeError
+ """
+ func_pure_noexcept()
+
+_WARNINGS = """
+12:5: Unraisable exception in function 'legacy_implicit_noexcept.func_implicit'.
+12:36: Implicit noexcept declaration is deprecated. Function declaration should contain 'noexcept' keyword.
+15:5: Unraisable exception in function 'legacy_implicit_noexcept.func_noexcept'.
+24:43: Implicit noexcept declaration is deprecated. Function declaration should contain 'noexcept' keyword.
+27:38: Implicit noexcept declaration is deprecated. Function declaration should contain 'noexcept' keyword.
+"""
diff --git a/tests/run/legacy_implicit_noexcept_build.srctree b/tests/run/legacy_implicit_noexcept_build.srctree
new file mode 100644
index 000000000..c7b30693d
--- /dev/null
+++ b/tests/run/legacy_implicit_noexcept_build.srctree
@@ -0,0 +1,33 @@
+PYTHON setup.py build_ext --inplace
+PYTHON -c "import bar"
+
+######## setup.py ########
+
+from Cython.Build.Dependencies import cythonize
+from distutils.core import setup
+
+setup(
+ ext_modules = cythonize("*.pyx", compiler_directives={'legacy_implicit_noexcept': True}),
+)
+
+
+######## bar.pyx ########
+
+cdef int func_noexcept() noexcept:
+ raise RuntimeError()
+
+cdef int func_implicit():
+ raise RuntimeError()
+
+cdef int func_return_value() except -1:
+ raise RuntimeError()
+
+func_noexcept()
+func_implicit()
+
+try:
+ func_return_value()
+except RuntimeError:
+ pass
+else:
+ assert False, 'Exception not raised'