From 14723b0bbd127790c450945099db31018d80fa83 Mon Sep 17 00:00:00 2001 From: Matt Davis <6775756+nitzmahone@users.noreply.github.com> Date: Thu, 30 Nov 2023 18:27:52 -0800 Subject: [PATCH] Python 3.13 compatibility (#24) * fix obsolete private API aliases for 3.13 compat * 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/_cffi_backend.c | 4 +- src/c/misc_thread_common.h | 4 +- src/c/test_c.py | 217 ++++++++++++------------------------- 3 files changed, 74 insertions(+), 151 deletions(-) diff --git a/src/c/_cffi_backend.c b/src/c/_cffi_backend.c index 5e284e00..7c72ffe0 100644 --- a/src/c/_cffi_backend.c +++ b/src/c/_cffi_backend.c @@ -134,8 +134,8 @@ # define PyText_Check PyUnicode_Check # define PyTextAny_Check PyUnicode_Check # define PyText_FromFormat PyUnicode_FromFormat -# define PyText_AsUTF8 _PyUnicode_AsString /* PyUnicode_AsUTF8 in Py3.3 */ -# define PyText_AS_UTF8 _PyUnicode_AsString +# define PyText_AsUTF8 PyUnicode_AsUTF8 +# define PyText_AS_UTF8 PyUnicode_AsUTF8 # if PY_VERSION_HEX >= 0x03030000 # define PyText_GetSize PyUnicode_GetLength # else diff --git a/src/c/misc_thread_common.h b/src/c/misc_thread_common.h index ead9c83c..7d29634b 100644 --- a/src/c/misc_thread_common.h +++ b/src/c/misc_thread_common.h @@ -331,7 +331,9 @@ PyAPI_DATA(void *volatile) _PyThreadState_Current; static PyThreadState *get_current_ts(void) { -#if PY_VERSION_HEX >= 0x03060000 +#if PY_VERSION_HEX >= 0x030D0000 + return PyThreadState_GetUnchecked(); +#elif PY_VERSION_HEX >= 0x03060000 return _PyThreadState_UncheckedGet(); #elif defined(_Py_atomic_load_relaxed) return (PyThreadState*)_Py_atomic_load_relaxed(&_PyThreadState_Current); 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",