Skip to content

Commit 4eb826f

Browse files
committed
Merge remote-tracking branch 'upstream/main' into python314
2 parents e8172b4 + ba306af commit 4eb826f

File tree

14 files changed

+96
-59
lines changed

14 files changed

+96
-59
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ jobs:
2121
strategy:
2222
fail-fast: false
2323
matrix:
24-
os: ['macos-13', 'macos-latest', 'ubuntu-latest', 'windows-latest']
25-
python-version: ['3.12', '3.13', '3.14.0-rc.3']
24+
os: ['macos-14', 'macos-latest', 'ubuntu-latest', 'windows-latest']
25+
python-version: ['3.11', '3.12', '3.13', '3.14']
2626
steps:
2727
- name: Set up the repository
2828
uses: actions/checkout@v4

.github/workflows/dist.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
- ubuntu-24.04-arm
3636
- windows-latest
3737
- windows-11-arm
38-
- macos-13
38+
- macos-14
3939
- macos-latest
4040
steps:
4141
- uses: actions/checkout@v4

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Requirements
2323
------------
2424

2525
- Python >= 3.12
26-
- Cython >= 0.28
26+
- Cython >= 3.1
2727
- Sphinx >= 1.6 (for building the documentation)
2828

2929
Links

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[build-system]
2-
requires = ["meson-python", "cython>=0.28"]
2+
requires = ["meson-python", "cython>=3.1"]
33
build-backend = "mesonpy"
44

55
[project]

src/conftest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def pytest_collect_file(
3131
_, module_name = resolve_pkg_root_and_module_name(file_path)
3232
module = importlib.import_module(module_name)
3333
# delete __test__ injected by cython, to avoid duplicate tests
34-
del module.__test__
34+
if hasattr(module, '__test__'):
35+
del module.__test__
3536
return DoctestModule.from_parent(parent, path=file_path)
3637
return None

src/cysignals/alarm.pyx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# cython: freethreading_compatible = True
12
"""
23
Fine-grained alarm function
34
"""

src/cysignals/implementation.c

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ static void setup_cysignals_handlers(void);
120120
static void cysigs_interrupt_handler(int sig);
121121
static void cysigs_signal_handler(int sig);
122122

123-
static void do_raise_exception(int sig);
123+
static void _do_raise_exception(int sig);
124124
static void sigdie(int sig, const char* s);
125125

126126
#define BACKTRACELEN 1024
@@ -392,11 +392,10 @@ static void cysigs_interrupt_handler(int sig)
392392
{
393393
if (!cysigs.block_sigint && !custom_signal_is_blocked())
394394
{
395-
/* Raise an exception so Python can see it */
396-
do_raise_exception(sig);
397-
395+
/* Jump back to sig_on() (the first one if there is a stack).
396+
* The signal number is encoded in the return value of sigsetjmp.
397+
* Do NOT call Python code from signal handler! */
398398
#if !_WIN32
399-
/* Jump back to sig_on() (the first one if there is a stack) */
400399
siglongjmp(trampoline, sig);
401400
#endif
402401
}
@@ -449,10 +448,10 @@ static void cysigs_signal_handler(int sig)
449448
}
450449
#endif
451450

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

555554

556555
/* This calls sig_raise_exception() to actually raise the exception. */
557-
static void do_raise_exception(int sig)
556+
static void _do_raise_exception(int sig)
558557
{
559558
#if ENABLE_DEBUG_CYSIGNALS
560559
struct timespec raisetime;
561560
if (cysigs.debug_level >= 2) {
562561
get_monotonic_time(&raisetime);
563562
long delta_ms = (raisetime.tv_sec - sigtime.tv_sec)*1000L + (raisetime.tv_nsec - sigtime.tv_nsec)/1000000L;
564563
PyGILState_STATE gilstate = PyGILState_Ensure();
565-
print_stderr("do_raise_exception(sig=");
564+
print_stderr("_do_raise_exception(sig=");
566565
print_stderr_long(sig);
567566
print_stderr(")\nPyErr_Occurred() = ");
568567
print_stderr_ptr(PyErr_Occurred());
@@ -588,7 +587,7 @@ static void _sig_on_interrupt_received(void)
588587
sigprocmask(SIG_BLOCK, &sigmask_with_sigint, &oldset);
589588
#endif
590589

591-
do_raise_exception(cysigs.interrupt_received);
590+
_do_raise_exception(cysigs.interrupt_received);
592591
cysigs.sig_on_count = 0;
593592
cysigs.interrupt_received = 0;
594593
custom_set_pending_signal(0);

src/cysignals/macros.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,22 @@ static inline int _sig_on_prejmp(const char* message, CYTHON_UNUSED const char*
143143
/*
144144
* Process the return value of cysetjmp().
145145
* Return 0 if there was an exception, 1 otherwise.
146+
*
147+
* This function propagates the signal number naturally via jmpret
148+
* (the return value from sigsetjmp/cylongjmp), which is always nonzero
149+
* when a signal occurs. This avoids using global state and eliminates
150+
* potential desynchronization between the jump return value and any
151+
* stored signal number.
146152
*/
147153
static inline int _sig_on_postjmp(int jmpret)
148154
{
149155
if (unlikely(jmpret > 0))
150156
{
151-
/* An exception occurred */
157+
/* A signal occurred and we jumped back via longjmp.
158+
* jmpret contains the signal number that was passed to siglongjmp.
159+
* Now we're back in a safe context (not in signal handler),
160+
* so it's safe to call Python code to raise the exception. */
161+
_do_raise_exception(jmpret);
152162
_sig_on_recover();
153163
return 0;
154164
}

src/cysignals/pselect.pyx

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# cython: freethreading_compatible = True
12
"""
23
Interface to the ``pselect()`` and ``sigprocmask()`` system calls
34
=================================================================
@@ -33,13 +34,31 @@ We wait for a child created using the ``subprocess`` module::
3334
>>> p.poll() # p should be finished
3435
0
3536
36-
Now using the ``multiprocessing`` module::
37+
Now using the ``multiprocessing`` module with ANY start method::
3738
3839
>>> from cysignals.pselect import PSelecter
39-
>>> from multiprocessing import *
40-
>>> import time
40+
>>> from multiprocessing import get_context
41+
>>> import time, sys
42+
>>> # Works with any start method - uses process sentinel
43+
>>> ctx = get_context() # Uses default (forkserver on 3.14+, fork on older)
44+
>>> with PSelecter() as sel:
45+
... p = ctx.Process(target=time.sleep, args=(1,))
46+
... p.start()
47+
... # Monitor process.sentinel instead of SIGCHLD
48+
... r, w, x, t = sel.pselect(rlist=[p.sentinel], timeout=2)
49+
... p.is_alive() # p should be finished
50+
False
51+
52+
For SIGCHLD-based monitoring (requires 'fork' on Python 3.14+)::
53+
54+
>>> import signal
55+
>>> def dummy_handler(sig, frame):
56+
... pass
57+
>>> _ = signal.signal(signal.SIGCHLD, dummy_handler)
58+
>>> # Use 'fork' method for SIGCHLD to work properly
59+
>>> ctx = get_context('fork') if sys.version_info >= (3, 14) else get_context()
4160
>>> with PSelecter([signal.SIGCHLD]) as sel:
42-
... p = Process(target=time.sleep, args=(1,))
61+
... p = ctx.Process(target=time.sleep, args=(1,))
4362
... p.start()
4463
... _ = sel.sleep()
4564
... p.is_alive() # p should be finished
@@ -289,12 +308,14 @@ cdef class PSelecter:
289308
290309
Start a process which will cause a ``SIGCHLD`` signal::
291310
292-
>>> import time
293-
>>> from multiprocessing import *
311+
>>> import time, sys
312+
>>> from multiprocessing import get_context
294313
>>> from cysignals.pselect import PSelecter, interruptible_sleep
314+
>>> # For SIGCHLD, must use 'fork' on Python 3.14+
315+
>>> ctx = get_context('fork') if sys.version_info >= (3, 14) else get_context()
295316
>>> w = PSelecter([signal.SIGCHLD])
296317
>>> with w:
297-
... p = Process(target=time.sleep, args=(0.25,))
318+
... p = ctx.Process(target=time.sleep, args=(0.25,))
298319
... t0 = time.time()
299320
... p.start()
300321

src/cysignals/pysignals.pyx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# cython: freethreading_compatible = True
12
r"""
23
Python interface to signal handlers
34
===================================

0 commit comments

Comments
 (0)