Skip to content

Commit c404d60

Browse files
committed
Python: Add support for sys.exit() from callbacks/timers/etc.
Instead of printing exceptions in callbacks to stderr, let's raise them as unraisable exceptions, but catch SystemExit and forward it through the event loop. Fixes #9416
1 parent 66376be commit c404d60

File tree

5 files changed

+102
-15
lines changed

5 files changed

+102
-15
lines changed

api/python/slint/interpreter.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -479,8 +479,12 @@ impl GcVisibleCallbacks {
479479
let result = match callable.call(py, py_args, None) {
480480
Ok(result) => result,
481481
Err(err) => {
482-
eprintln!(
483-
"Python: Invoking python callback for {name} threw an exception: {err}"
482+
crate::handle_unraisable(
483+
py,
484+
format!(
485+
"Python: Invoking python callback for {name} threw an exception"
486+
),
487+
err,
484488
);
485489
return Value::Void;
486490
}
@@ -493,7 +497,13 @@ impl GcVisibleCallbacks {
493497
) {
494498
Ok(value) => value,
495499
Err(err) => {
496-
eprintln!("Python: Unable to convert return value of Python callback for {name} to Slint value: {err}");
500+
crate::handle_unraisable(
501+
py,
502+
format!(
503+
"Python: Unable to convert return value of Python callback for {name} to Slint value"
504+
),
505+
err,
506+
);
497507
return Value::Void;
498508
}
499509
};

api/python/slint/lib.rs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright © SixtyFPS GmbH <[email protected]>
22
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
33

4+
use std::cell::{Cell, RefCell};
5+
46
use pyo3_stub_gen::{define_stub_info_gatherer, derive::gen_stub_pyfunction};
57

68
mod image;
@@ -15,10 +17,38 @@ mod models;
1517
mod timer;
1618
mod value;
1719

20+
fn handle_unraisable(py: Python<'_>, context: String, err: PyErr) {
21+
let exception = err.value(py);
22+
let __notes__ = exception
23+
.getattr(pyo3::intern!(py, "__notes__"))
24+
.unwrap_or_else(|_| pyo3::types::PyList::empty(py).into_any());
25+
if let Ok(notes_list) = __notes__.downcast::<pyo3::types::PyList>() {
26+
let _ = notes_list.append(context);
27+
let _ = exception.setattr(pyo3::intern!(py, "__notes__"), __notes__);
28+
}
29+
30+
if EVENT_LOOP_RUNNING.get() && err.is_instance_of::<pyo3::exceptions::PySystemExit>(py) {
31+
EVENT_LOOP_EXCEPTION.replace(Some(err));
32+
let _ = slint_interpreter::quit_event_loop();
33+
} else {
34+
err.write_unraisable(py, None);
35+
}
36+
}
37+
38+
thread_local! {
39+
static EVENT_LOOP_RUNNING: Cell<bool> = Cell::new(false);
40+
static EVENT_LOOP_EXCEPTION: RefCell<Option<PyErr>> = RefCell::new(None)
41+
}
42+
1843
#[gen_stub_pyfunction]
1944
#[pyfunction]
20-
fn run_event_loop() -> Result<(), errors::PyPlatformError> {
21-
slint_interpreter::run_event_loop().map_err(|e| e.into())
45+
fn run_event_loop() -> Result<(), PyErr> {
46+
EVENT_LOOP_EXCEPTION.replace(None);
47+
EVENT_LOOP_RUNNING.set(true);
48+
let result = slint_interpreter::run_event_loop();
49+
EVENT_LOOP_RUNNING.set(false);
50+
result.map_err(|e| errors::PyPlatformError::from(e))?;
51+
EVENT_LOOP_EXCEPTION.take().map_or(Ok(()), |err| Err(err))
2252
}
2353

2454
#[gen_stub_pyfunction]

api/python/slint/models.rs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,10 @@ impl i_slint_core::model::Model for PyModelShared {
9797
let result = match obj.call_method0(py, "row_count") {
9898
Ok(result) => result,
9999
Err(err) => {
100-
eprintln!(
101-
"Python: Model implementation of row_count() threw an exception: {err}"
100+
crate::handle_unraisable(
101+
py,
102+
"Python: Model implementation of row_count() threw an exception".into(),
103+
err,
102104
);
103105
return 0;
104106
}
@@ -107,7 +109,11 @@ impl i_slint_core::model::Model for PyModelShared {
107109
match result.extract::<usize>(py) {
108110
Ok(count) => count,
109111
Err(err) => {
110-
eprintln!("Python: Model implementation of row_count() returned value that cannot be cast to usize: {err}");
112+
crate::handle_unraisable(
113+
py,
114+
"Python: Model implementation of row_count() returned value that cannot be cast to usize".into(),
115+
err,
116+
);
111117
0
112118
}
113119
}
@@ -126,8 +132,10 @@ impl i_slint_core::model::Model for PyModelShared {
126132
Ok(result) => result,
127133
Err(err) if err.is_instance_of::<PyIndexError>(py) => return None,
128134
Err(err) => {
129-
eprintln!(
130-
"Python: Model implementation of row_data() threw an exception: {err}"
135+
crate::handle_unraisable(
136+
py,
137+
"Python: Model implementation of row_data() threw an exception".into(),
138+
err,
131139
);
132140
return None;
133141
}
@@ -140,7 +148,11 @@ impl i_slint_core::model::Model for PyModelShared {
140148
) {
141149
Ok(pv) => Some(pv),
142150
Err(err) => {
143-
eprintln!("Python: Model implementation of row_data() returned value that cannot be converted to Rust: {err}");
151+
crate::handle_unraisable(
152+
py,
153+
"Python: Model implementation of row_data() returned value that cannot be cast to usize".into(),
154+
err,
155+
);
144156
None
145157
}
146158
}
@@ -165,8 +177,10 @@ impl i_slint_core::model::Model for PyModelShared {
165177
if let Err(err) =
166178
obj.call_method1(py, "set_row_data", (row, type_collection.to_py_value(data)))
167179
{
168-
eprintln!(
169-
"Python: Model implementation of set_row_data() threw an exception: {err}"
180+
crate::handle_unraisable(
181+
py,
182+
"Python: Model implementation of set_row_data() threw an exception".into(),
183+
err,
170184
);
171185
};
172186
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright © SixtyFPS GmbH <[email protected]>
2+
# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3+
4+
import slint
5+
from slint import slint as native
6+
from datetime import timedelta
7+
import pytest
8+
import sys
9+
10+
11+
def test_sysexit_exception() -> None:
12+
def call_sys_exit() -> None:
13+
sys.exit(42)
14+
15+
slint.Timer.single_shot(timedelta(milliseconds=100), call_sys_exit)
16+
with pytest.raises(SystemExit) as exc_info:
17+
native.run_event_loop()
18+
assert (
19+
"unexpected failure running python singleshot timer callback"
20+
in exc_info.value.__notes__
21+
)

api/python/slint/timer.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,13 @@ impl PyTimer {
7272
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
7373
self.timer.start(mode.into(), interval, move || {
7474
Python::attach(|py| {
75-
callback.call0(py).expect("unexpected failure running python timer callback");
75+
if let Err(err) = callback.call0(py) {
76+
crate::handle_unraisable(
77+
py,
78+
"unexpected failure running python timer callback".into(),
79+
err,
80+
);
81+
}
7682
});
7783
});
7884
Ok(())
@@ -91,7 +97,13 @@ impl PyTimer {
9197
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
9298
i_slint_core::timers::Timer::single_shot(duration, move || {
9399
Python::attach(|py| {
94-
callback.call0(py).expect("unexpected failure running python timer callback");
100+
if let Err(err) = callback.call0(py) {
101+
crate::handle_unraisable(
102+
py,
103+
"unexpected failure running python singleshot timer callback".into(),
104+
err,
105+
);
106+
}
95107
});
96108
});
97109
Ok(())

0 commit comments

Comments
 (0)