Skip to content
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5f35844
Fix Python 3.14 compatibility: Set multiprocessing start method to 'f…
Oct 1, 2025
6153369
Fix Python 3.14 compatibility: defer exception raising from signal ha…
Oct 2, 2025
1389e13
CI: Use specific Python 3.14.0-rc.3 version for testing
Oct 2, 2025
19c9434
Python 3.14: Support forkserver multiprocessing start method
Oct 3, 2025
546217f
Fix Python 3.14t free-threaded build: use _Py_REFCNT() instead of ob_…
Oct 3, 2025
0f15195
Fix Python 3.14t: Use Py_REFCNT declared from Python.h instead of _Py…
Oct 3, 2025
68b7713
Remove unused sys import from conftest.py
cxzhong Oct 4, 2025
c298389
Update reference count check for cysigs.exc_value
cxzhong Oct 4, 2025
7dd96f1
Refactor: Remove signal_to_raise global variable
Oct 4, 2025
b128872
Remove do_raise_exception declaration from signals.pxd
cxzhong Oct 4, 2025
1c1bcfd
Revert "Remove do_raise_exception declaration from signals.pxd"
Oct 4, 2025
e119a0c
Update macOS version in CI workflow
cxzhong Oct 5, 2025
33533c5
Rename do_raise_exception to _do_raise_exception for consistency with…
cxzhong Oct 5, 2025
385d66c
Remove wait_for_process method from pselect.pyx
cxzhong Oct 5, 2025
cf49a75
Update macOS version in dist workflow
cxzhong Oct 5, 2025
7c92255
Add freethreading compatibility declaration to Cython files
cxzhong Oct 6, 2025
8f32f8b
Merge branch 'main' of github.com:cxzhong/cysignals
cxzhong Oct 6, 2025
ba28e30
Update Python version in CI workflow
cxzhong Oct 8, 2025
d6bc361
Update Python version requirements in pyproject.toml
cxzhong Oct 8, 2025
7af95ed
change pyproject.toml and uv.lock and modify redame
cxzhong Oct 8, 2025
b5d71e9
Remove unused
cxzhong Oct 8, 2025
105ff80
Update .github/workflows/ci.yml
cxzhong Oct 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ jobs:
strategy:
fail-fast: false
matrix:
os: ['macos-13', 'macos-latest', 'ubuntu-latest', 'windows-latest']
python-version: ['3.10', '3.11', '3.12', '3.13']
os: ['macos-14', 'macos-latest', 'ubuntu-latest', 'windows-latest']
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
steps:
- name: Set up the repository
uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/dist.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
- ubuntu-24.04-arm
- windows-latest
- windows-11-arm
- macos-13
- macos-14
- macos-latest
steps:
- uses: actions/checkout@v4
Expand Down
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ signals and errors) in Cython code.
Requirements
------------

- Python >= 3.9
- Cython >= 0.28
- Python >= 3.11
- Cython >= 3.1
- Sphinx >= 1.6 (for building the documentation)

Links
Expand Down
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ["meson-python", "cython>=0.28"]
requires = ["meson-python", "cython>=3.1"]
build-backend = "mesonpy"

[project]
Expand All @@ -21,16 +21,16 @@ classifiers = [
"Programming Language :: Cython",
"Programming Language :: Python",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: Implementation :: CPython",
"Topic :: System",
"Topic :: Software Development :: Debuggers",
]
urls = { Homepage = "https://github.com/sagemath/cysignals" }
requires-python = ">=3.9,<3.14"
requires-python = ">=3.11,<3.15"

[project.entry-points.pkg_config]
cysignals = 'cysignals'
Expand Down
3 changes: 2 additions & 1 deletion src/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def pytest_collect_file(
_, module_name = resolve_pkg_root_and_module_name(file_path)
module = importlib.import_module(module_name)
# delete __test__ injected by cython, to avoid duplicate tests
del module.__test__
if hasattr(module, '__test__'):
del module.__test__
return DoctestModule.from_parent(parent, path=file_path)
return None
1 change: 1 addition & 0 deletions src/cysignals/alarm.pyx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# cython: freethreading_compatible = True
"""
Fine-grained alarm function
"""
Expand Down
21 changes: 10 additions & 11 deletions src/cysignals/implementation.c
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ static void setup_cysignals_handlers(void);
static void cysigs_interrupt_handler(int sig);
static void cysigs_signal_handler(int sig);

static void do_raise_exception(int sig);
static void _do_raise_exception(int sig);
Copy link
Preview

Copilot AI Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_do_raise_exception is declared static here, but it is invoked from macros.h's inline _sig_on_postjmp, which is compiled into other translation units. With static linkage, other TUs cannot resolve this symbol, causing link errors. Remove static (make it have external linkage) and provide a prototype in a shared header (e.g., macros.h) so all TUs see the declaration.

Suggested change
static void _do_raise_exception(int sig);
void _do_raise_exception(int sig);

Copilot uses AI. Check for mistakes.

static void sigdie(int sig, const char* s);

#define BACKTRACELEN 1024
Expand Down Expand Up @@ -392,11 +392,10 @@ static void cysigs_interrupt_handler(int sig)
{
if (!cysigs.block_sigint && !custom_signal_is_blocked())
{
/* Raise an exception so Python can see it */
do_raise_exception(sig);

/* Jump back to sig_on() (the first one if there is a stack).
* The signal number is encoded in the return value of sigsetjmp.
* Do NOT call Python code from signal handler! */
#if !_WIN32
/* Jump back to sig_on() (the first one if there is a stack) */
siglongjmp(trampoline, sig);
#endif
}
Expand Down Expand Up @@ -449,10 +448,10 @@ static void cysigs_signal_handler(int sig)
}
#endif

/* Raise an exception so Python can see it */
do_raise_exception(sig);
/* Jump back to sig_on() (the first one if there is a stack).
* The signal number is encoded in the return value of sigsetjmp.
* Do NOT call Python code from signal handler! */
#if !_WIN32
/* Jump back to sig_on() (the first one if there is a stack) */
siglongjmp(trampoline, sig);
#endif
}
Expand Down Expand Up @@ -554,15 +553,15 @@ static void setup_trampoline(void)


/* This calls sig_raise_exception() to actually raise the exception. */
static void do_raise_exception(int sig)
static void _do_raise_exception(int sig)
{
#if ENABLE_DEBUG_CYSIGNALS
struct timespec raisetime;
if (cysigs.debug_level >= 2) {
get_monotonic_time(&raisetime);
long delta_ms = (raisetime.tv_sec - sigtime.tv_sec)*1000L + (raisetime.tv_nsec - sigtime.tv_nsec)/1000000L;
PyGILState_STATE gilstate = PyGILState_Ensure();
print_stderr("do_raise_exception(sig=");
print_stderr("_do_raise_exception(sig=");
print_stderr_long(sig);
print_stderr(")\nPyErr_Occurred() = ");
print_stderr_ptr(PyErr_Occurred());
Expand All @@ -588,7 +587,7 @@ static void _sig_on_interrupt_received(void)
sigprocmask(SIG_BLOCK, &sigmask_with_sigint, &oldset);
#endif

do_raise_exception(cysigs.interrupt_received);
_do_raise_exception(cysigs.interrupt_received);
cysigs.sig_on_count = 0;
cysigs.interrupt_received = 0;
custom_set_pending_signal(0);
Expand Down
12 changes: 11 additions & 1 deletion src/cysignals/macros.h
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,22 @@ static inline int _sig_on_prejmp(const char* message, CYTHON_UNUSED const char*
/*
* Process the return value of cysetjmp().
* Return 0 if there was an exception, 1 otherwise.
*
* This function propagates the signal number naturally via jmpret
* (the return value from sigsetjmp/cylongjmp), which is always nonzero
Copy link
Preview

Copilot AI Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct 'cylongjmp' to 'siglongjmp'.

Suggested change
* (the return value from sigsetjmp/cylongjmp), which is always nonzero
* (the return value from sigsetjmp/siglongjmp), which is always nonzero

Copilot uses AI. Check for mistakes.

* when a signal occurs. This avoids using global state and eliminates
* potential desynchronization between the jump return value and any
* stored signal number.
*/
static inline int _sig_on_postjmp(int jmpret)
{
if (unlikely(jmpret > 0))
{
/* An exception occurred */
/* A signal occurred and we jumped back via longjmp.
* jmpret contains the signal number that was passed to siglongjmp.
* Now we're back in a safe context (not in signal handler),
* so it's safe to call Python code to raise the exception. */
_do_raise_exception(jmpret);
Comment on lines +157 to +161
Copy link
Preview

Copilot AI Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This inline function calls _do_raise_exception but there's no C declaration visible in this header. Without an extern prototype, this will be an implicit declaration (or an error under modern compilers). Add an extern prototype for _do_raise_exception(int) in a header included by all consumers (e.g., declare it at the top of macros.h) and ensure the function has external linkage.

Copilot uses AI. Check for mistakes.

_sig_on_recover();
return 0;
}
Expand Down
35 changes: 28 additions & 7 deletions src/cysignals/pselect.pyx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# cython: freethreading_compatible = True
"""
Interface to the ``pselect()`` and ``sigprocmask()`` system calls
=================================================================
Expand Down Expand Up @@ -33,13 +34,31 @@ We wait for a child created using the ``subprocess`` module::
>>> p.poll() # p should be finished
0

Now using the ``multiprocessing`` module::
Now using the ``multiprocessing`` module with ANY start method::

>>> from cysignals.pselect import PSelecter
>>> from multiprocessing import *
>>> import time
>>> from multiprocessing import get_context
>>> import time, sys
>>> # Works with any start method - uses process sentinel
>>> ctx = get_context() # Uses default (forkserver on 3.14+, fork on older)
>>> with PSelecter() as sel:
... p = ctx.Process(target=time.sleep, args=(0.5,))
... p.start()
... # Monitor process.sentinel instead of SIGCHLD
... r, w, x, t = sel.pselect(rlist=[p.sentinel], timeout=2)
... p.is_alive() # p should be finished
False

For SIGCHLD-based monitoring (requires 'fork' on Python 3.14+)::

>>> import signal
>>> def dummy_handler(sig, frame):
... pass
>>> _ = signal.signal(signal.SIGCHLD, dummy_handler)
>>> # Use 'fork' method for SIGCHLD to work properly
>>> ctx = get_context('fork') if sys.version_info >= (3, 14) else get_context()
>>> with PSelecter([signal.SIGCHLD]) as sel:
... p = Process(target=time.sleep, args=(1,))
... p = ctx.Process(target=time.sleep, args=(0.5,))
... p.start()
... _ = sel.sleep()
... p.is_alive() # p should be finished
Expand Down Expand Up @@ -289,12 +308,14 @@ cdef class PSelecter:

Start a process which will cause a ``SIGCHLD`` signal::

>>> import time
>>> from multiprocessing import *
>>> import time, sys
>>> from multiprocessing import get_context
>>> from cysignals.pselect import PSelecter, interruptible_sleep
>>> # For SIGCHLD, must use 'fork' on Python 3.14+
>>> ctx = get_context('fork') if sys.version_info >= (3, 14) else get_context()
>>> w = PSelecter([signal.SIGCHLD])
>>> with w:
... p = Process(target=time.sleep, args=(0.25,))
... p = ctx.Process(target=time.sleep, args=(0.25,))
... t0 = time.time()
... p.start()

Expand Down
1 change: 1 addition & 0 deletions src/cysignals/pysignals.pyx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# cython: freethreading_compatible = True
r"""
Python interface to signal handlers
===================================
Expand Down
2 changes: 2 additions & 0 deletions src/cysignals/signals.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ cdef nogil:
cysigs_t cysigs "cysigs"
void _sig_on_interrupt_received "_sig_on_interrupt_received"() noexcept
void _sig_on_recover "_sig_on_recover"() noexcept
void _do_raise_exception "_do_raise_exception"(int sig) noexcept
void _sig_off_warning "_sig_off_warning"(const char*, int) noexcept
void print_backtrace "print_backtrace"() noexcept

Expand All @@ -99,5 +100,6 @@ cdef inline void __generate_declarations() noexcept:
cysigs
_sig_on_interrupt_received
_sig_on_recover
_do_raise_exception
_sig_off_warning
print_backtrace
8 changes: 5 additions & 3 deletions src/cysignals/signals.pyx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# cython: freethreading_compatible = True
# cython: preliminary_late_includes_cy28=True
r"""
Interrupt and signal handling
Expand Down Expand Up @@ -25,7 +26,7 @@ See ``tests.pyx`` for extensive tests.

from libc.signal cimport *
from libc.stdio cimport freopen, stdin
from cpython.ref cimport Py_XINCREF, Py_CLEAR
from cpython.ref cimport Py_XINCREF, Py_CLEAR, _Py_REFCNT
from cpython.exc cimport (PyErr_Occurred, PyErr_NormalizeException,
PyErr_Fetch, PyErr_Restore)
from cpython.version cimport PY_MAJOR_VERSION
Expand Down Expand Up @@ -56,6 +57,7 @@ cdef extern from "implementation.c":
void print_backtrace() nogil
void _sig_on_interrupt_received() nogil
void _sig_on_recover() nogil
void _do_raise_exception(int sig) nogil
void _sig_off_warning(const char*, int) nogil

# Python library functions for raising exceptions without "except"
Expand Down Expand Up @@ -360,7 +362,7 @@ cdef void verify_exc_value() noexcept:
Check that ``cysigs.exc_value`` is still the exception being raised.
Clear ``cysigs.exc_value`` if not.
"""
if cysigs.exc_value.ob_refcnt == 1:
if cysigs.exc_value != NULL and _Py_REFCNT(cysigs.exc_value) == 1:
# No other references => exception is certainly gone
Py_CLEAR(cysigs.exc_value)
return
Expand Down Expand Up @@ -408,5 +410,5 @@ cdef void verify_exc_value() noexcept:
# Make sure we still have cysigs.exc_value at all; if this function was
# called again during garbage collection it might have already been set
# to NULL; see https://github.com/sagemath/cysignals/issues/126
if cysigs.exc_value != NULL and cysigs.exc_value.ob_refcnt == 1:
if cysigs.exc_value != NULL and _Py_REFCNT(cysigs.exc_value) == 1:
Py_CLEAR(cysigs.exc_value)
1 change: 1 addition & 0 deletions src/cysignals/tests.pyx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# cython: freethreading_compatible = True
# cython: preliminary_late_includes_cy28=True, show_performance_hints=False
"""
Test interrupt and signal handling
Expand Down
Loading