Skip to content

Commit

Permalink
Python 3.13 compatibility (#24)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
nitzmahone committed Dec 1, 2023
1 parent 49127c6 commit 14723b0
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 151 deletions.
4 changes: 2 additions & 2 deletions src/c/_cffi_backend.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion src/c/misc_thread_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
217 changes: 69 additions & 148 deletions src/c/test_c.py
Original file line number Diff line number Diff line change
@@ -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':
Expand Down Expand Up @@ -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)
Expand All @@ -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 <function$Zcb1 at 0x$>:
Traceback (most recent call last):
File "$", line $, in Zcb1
$
File "$", line $, in check_value
$
ValueError: 42
""", """\
Exception ignored from cffi callback <function$Zcb1 at 0x$>:
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 <function$Zcb1 at 0x$>:
Trying to convert the result back to C:
OverflowError: integer 60000 does not fit 'short'
""", """\
Exception ignored from cffi callback <function$Zcb1 at 0x$>, 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 <function$Zcb1 at 0x$>:
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 <function$Zcb1 at 0x$>, 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 <function$Zcb1 at 0x$>:
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 <function$Zcb1 at 0x$>, 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 <function$Zcb1 at 0x$>, 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",
Expand Down

0 comments on commit 14723b0

Please sign in to comment.