diff options
author | Matus Valo <matusvalo@users.noreply.github.com> | 2022-12-06 11:14:42 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-12-06 11:14:42 +0100 |
commit | c7aba0e94ff78a4a5e569d412ac72b479534cf64 (patch) | |
tree | 775ffc07820aea565c5834a8c5e6ae98c5bd9bd4 | |
parent | 099faf83e4f5cc351496afef300853e9b2a57ac0 (diff) | |
download | cython-c7aba0e94ff78a4a5e569d412ac72b479534cf64.tar.gz |
Add compiler directive to disable the default exception propagation for legacy code (GH-5094)
-rw-r--r-- | Cython/Compiler/Main.py | 2 | ||||
-rw-r--r-- | Cython/Compiler/Options.py | 5 | ||||
-rw-r--r-- | Cython/Compiler/Parsing.py | 6 | ||||
-rw-r--r-- | docs/src/userguide/language_basics.rst | 4 | ||||
-rw-r--r-- | docs/src/userguide/migrating_to_cy30.rst | 6 | ||||
-rw-r--r-- | docs/src/userguide/source_files_and_compilation.rst | 11 | ||||
-rw-r--r-- | tests/run/legacy_implicit_noexcept.pyx | 143 | ||||
-rw-r--r-- | tests/run/legacy_implicit_noexcept_build.srctree | 33 |
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' |