diff --git a/364621f848.patch b/364621f848.patch new file mode 100644 index 0000000..7f95352 --- /dev/null +++ b/364621f848.patch @@ -0,0 +1,266 @@ +From 364621f8489d85c29b26fa5e754aaf2a5f8279fe Mon Sep 17 00:00:00 2001 +From: Matt Davis +Date: Mon, 16 Oct 2023 17:53:28 -0700 +Subject: [PATCH] update unraisable tests to use sys.unraisablehook + +* assert shape of calls to unraisablehook and sanity check traceback contents instead of (varying) stderr output from default unraisablehook impl +--- + src/c/test_c.py | 217 +++++++++++++++--------------------------------- + 1 file changed, 69 insertions(+), 148 deletions(-) + +diff --git a/src/c/test_c.py b/src/c/test_c.py +index 1cdab10f..10cc35cf 100644 +--- a/src/c/test_c.py ++++ b/src/c/test_c.py +@@ -1,5 +1,12 @@ ++from __future__ import annotations ++ ++import contextlib ++import traceback ++import unittest.mock ++ + import pytest + import sys ++import typing as t + + is_musl = False + if sys.platform == 'linux': +@@ -1337,27 +1344,37 @@ def cb(n): + e = pytest.raises(TypeError, f) + assert str(e.value) == "'int(*)(int)' expects 1 arguments, got 0" + ++@contextlib.contextmanager ++def _assert_unraisable(error_type: type[Exception] | None, message: str = '', traceback_tokens: list[str] | None = None): ++ """Assert that a given sys.unraisablehook interaction occurred (or did not occur, if error_type is None) while this context was active""" ++ raised_errors: list[Exception] = [] ++ raised_traceback: str = '' ++ ++ # sys.unraisablehook is called more than once for chained exceptions; accumulate the errors and tracebacks for inspection ++ def _capture_unraisable_hook(ur_args): ++ nonlocal raised_traceback ++ raised_errors.append(ur_args.exc_value) ++ ++ # NB: need to use the old etype/value/tb form until 3.10 is the minimum ++ raised_traceback += (ur_args.err_msg or '' + '\n') + ''.join(traceback.format_exception(None, ur_args.exc_value, ur_args.exc_traceback)) ++ ++ ++ with pytest.MonkeyPatch.context() as mp: ++ mp.setattr(sys, 'unraisablehook', _capture_unraisable_hook) ++ yield ++ ++ if error_type is None: ++ assert not raised_errors ++ assert not raised_traceback ++ return ++ ++ assert any(type(raised_error) is error_type for raised_error in raised_errors) ++ assert any(message in str(raised_error) for raised_error in raised_errors) ++ for t in traceback_tokens or []: ++ assert t in raised_traceback ++ ++ + def test_callback_exception(): +- try: +- import cStringIO +- except ImportError: +- import io as cStringIO # Python 3 +- import linecache +- def matches(istr, ipattern, ipattern38, ipattern311=None): +- if sys.version_info >= (3, 8): +- ipattern = ipattern38 +- if sys.version_info >= (3, 11): +- ipattern = ipattern311 or ipattern38 +- str, pattern = istr, ipattern +- while '$' in pattern: +- i = pattern.index('$') +- assert str[:i] == pattern[:i] +- j = str.find(pattern[i+1], i) +- assert i + 1 <= j <= str.find('\n', i) +- str = str[j:] +- pattern = pattern[i+1:] +- assert str == pattern +- return True + def check_value(x): + if x == 10000: + raise ValueError(42) +@@ -1366,148 +1383,52 @@ def Zcb1(x): + return x * 3 + BShort = new_primitive_type("short") + BFunc = new_function_type((BShort,), BShort, False) ++ + f = callback(BFunc, Zcb1, -42) +- # + seen = [] + oops_result = None + def oops(*args): + seen.append(args) + return oops_result + ff = callback(BFunc, Zcb1, -42, oops) +- # +- orig_stderr = sys.stderr +- orig_getline = linecache.getline +- try: +- linecache.getline = lambda *args: 'LINE' # hack: speed up PyPy tests +- sys.stderr = cStringIO.StringIO() +- if hasattr(sys, '__unraisablehook__'): # work around pytest +- sys.unraisablehook = sys.__unraisablehook__ # on recent CPythons ++ with _assert_unraisable(None): + assert f(100) == 300 +- assert sys.stderr.getvalue() == '' ++ with _assert_unraisable(ValueError, '42', ['in Zcb1', 'in check_value']): + assert f(10000) == -42 +- assert matches(sys.stderr.getvalue(), """\ +-From cffi callback : +-Traceback (most recent call last): +- File "$", line $, in Zcb1 +- $ +- File "$", line $, in check_value +- $ +-ValueError: 42 +-""", """\ +-Exception ignored from cffi callback : +-Traceback (most recent call last): +- File "$", line $, in Zcb1 +- $ +- File "$", line $, in check_value +- $ +-ValueError: 42 +-""") +- sys.stderr = cStringIO.StringIO() +- bigvalue = 20000 ++ ++ bigvalue = 20000 ++ with _assert_unraisable(OverflowError, "integer 60000 does not fit 'short'", ['callback', 'Zcb1']): + assert f(bigvalue) == -42 +- assert matches(sys.stderr.getvalue(), """\ +-From cffi callback : +-Trying to convert the result back to C: +-OverflowError: integer 60000 does not fit 'short' +-""", """\ +-Exception ignored from cffi callback , trying to convert the result back to C: +-Traceback (most recent call last): +- File "$", line $, in test_callback_exception +- $ +-OverflowError: integer 60000 does not fit 'short' +-""") +- sys.stderr = cStringIO.StringIO() +- bigvalue = 20000 +- assert len(seen) == 0 ++ assert len(seen) == 0 ++ ++ with _assert_unraisable(None): + assert ff(bigvalue) == -42 +- assert sys.stderr.getvalue() == "" +- assert len(seen) == 1 +- exc, val, tb = seen[0] +- assert exc is OverflowError +- assert str(val) == "integer 60000 does not fit 'short'" +- # +- sys.stderr = cStringIO.StringIO() +- bigvalue = 20000 +- del seen[:] +- oops_result = 81 ++ assert len(seen) == 1 ++ exc, val, tb = seen[0] ++ assert exc is OverflowError ++ assert str(val) == "integer 60000 does not fit 'short'" ++ ++ del seen[:] ++ oops_result = 81 ++ with _assert_unraisable(None): + assert ff(bigvalue) == 81 +- oops_result = None +- assert sys.stderr.getvalue() == "" +- assert len(seen) == 1 +- exc, val, tb = seen[0] +- assert exc is OverflowError +- assert str(val) == "integer 60000 does not fit 'short'" +- # +- sys.stderr = cStringIO.StringIO() +- bigvalue = 20000 +- del seen[:] +- oops_result = "xy" # not None and not an int! ++ ++ assert len(seen) == 1 ++ exc, val, tb = seen[0] ++ assert exc is OverflowError ++ assert str(val) == "integer 60000 does not fit 'short'" ++ ++ del seen[:] ++ oops_result = "xy" # not None and not an int! ++ ++ with _assert_unraisable(TypeError, "an integer is required", ["integer 60000 does not fit 'short'"]): + assert ff(bigvalue) == -42 +- oops_result = None +- assert matches(sys.stderr.getvalue(), """\ +-From cffi callback : +-Trying to convert the result back to C: +-OverflowError: integer 60000 does not fit 'short' +- +-During the call to 'onerror', another exception occurred: +- +-TypeError: $integer$ +-""", """\ +-Exception ignored from cffi callback , trying to convert the result back to C: +-Traceback (most recent call last): +- File "$", line $, in test_callback_exception +- $ +-OverflowError: integer 60000 does not fit 'short' +-Exception ignored during handling of the above exception by 'onerror': +-Traceback (most recent call last): +- File "$", line $, in test_callback_exception +- $ +-TypeError: $integer$ +-""") +- # +- sys.stderr = cStringIO.StringIO() +- seen = "not a list" # this makes the oops() function crash ++ ++ seen = "not a list" # this makes the oops() function crash ++ oops_result = None ++ with _assert_unraisable(AttributeError, "'str' object has no attribute 'append", ['Zcb1', 'ff', 'oops']): + assert ff(bigvalue) == -42 +- # the $ after the AttributeError message are for the suggestions that +- # will be added in Python 3.10 +- assert matches(sys.stderr.getvalue(), """\ +-From cffi callback : +-Trying to convert the result back to C: +-OverflowError: integer 60000 does not fit 'short' +- +-During the call to 'onerror', another exception occurred: +- +-Traceback (most recent call last): +- File "$", line $, in oops +- $ +-AttributeError: 'str' object has no attribute 'append$ +-""", """\ +-Exception ignored from cffi callback , trying to convert the result back to C: +-Traceback (most recent call last): +- File "$", line $, in test_callback_exception +- $ +-OverflowError: integer 60000 does not fit 'short' +-Exception ignored during handling of the above exception by 'onerror': +-Traceback (most recent call last): +- File "$", line $, in oops +- $ +-AttributeError: 'str' object has no attribute 'append$ +-""", """\ +-Exception ignored from cffi callback , trying to convert the result back to C: +-Traceback (most recent call last): +- File "$", line $, in test_callback_exception +- $ +-OverflowError: integer 60000 does not fit 'short' +-Exception ignored during handling of the above exception by 'onerror': +-Traceback (most recent call last): +- File "$", line $, in oops +- $ +- $ +-AttributeError: 'str' object has no attribute 'append$ +-""") +- finally: +- sys.stderr = orig_stderr +- linecache.getline = orig_getline ++ + + def test_callback_return_type(): + for rettype in ["signed char", "short", "int", "long", "long long", diff --git a/49127c6929.patch b/49127c6929.patch new file mode 100644 index 0000000..90767c3 --- /dev/null +++ b/49127c6929.patch @@ -0,0 +1,30 @@ +From 49127c6929bfc7186fbfd3819dd5e058ad888de4 Mon Sep 17 00:00:00 2001 +From: Victor Stinner +Date: Thu, 16 Nov 2023 17:26:12 +0100 +Subject: [PATCH] Use PyErr_FormatUnraisable() on Python 3.13 (#34) + +Use the new public PyErr_FormatUnraisable() on Python 3.13. + +The private _PyErr_WriteUnraisableMsg() function was removed in +Python 3.13: +https://github.com/python/cpython/pull/111643 +--- + src/c/_cffi_backend.c | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/src/c/_cffi_backend.c b/src/c/_cffi_backend.c +index 76ed8f09..5e284e00 100644 +--- a/src/c/_cffi_backend.c ++++ b/src/c/_cffi_backend.c +@@ -6118,7 +6118,11 @@ static void _my_PyErr_WriteUnraisable(PyObject *t, PyObject *v, PyObject *tb, + + PyErr_Restore(t, v, tb); + if (s != NULL) { ++#if PY_VERSION_HEX >= 0x030D0000 ++ PyErr_FormatUnraisable("Exception ignored %S", s); ++#else + _PyErr_WriteUnraisableMsg(PyText_AS_UTF8(s), NULL); ++#endif + Py_DECREF(s); + } + else diff --git a/python-cffi.spec b/python-cffi.spec index 79e5db3..b2f5559 100644 --- a/python-cffi.spec +++ b/python-cffi.spec @@ -8,6 +8,15 @@ License: MIT AND PSF-2.0 URL: https://github.com/python-cffi/cffi Source: %{url}/archive/v%{version}/cffi-%{version}.tar.gz +# Use PyErr_FormatUnraisable() on Python 3.13+ +# The private _PyErr_WriteUnraisableMsg() function was removed. +# Merged upstream. +Patch: https://github.com/python-cffi/cffi/commit/49127c6929.patch + +# Update unraisable tests to use sys.unraisablehook (for Python 3.13+ compatibility) +# From https://github.com/python-cffi/cffi/pull/24 -- proposed upstream. +Patch: https://github.com/python-cffi/cffi/commit/364621f848.patch + BuildRequires: python3-devel BuildRequires: python3-pytest BuildRequires: make