From c3079e881bae34bf7adcbc1a56d2d5889bc1612e Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Mon, 1 Jun 2026 11:20:09 +0200 Subject: [PATCH 1/7] Use PyO3 main & enable c-api tests --- .github/workflows/ci.yaml | 1 + .github/workflows/update-caches.yml | 1 + Cargo.toml | 1 + crates/capi/pyo3-rustpython.config | 2 +- crates/capi/src/abstract_.rs | 37 +++++++++++++++++++ crates/capi/src/abstract_/iter.rs | 2 +- crates/capi/src/abstract_/mapping.rs | 2 +- crates/capi/src/abstract_/number.rs | 2 +- crates/capi/src/abstract_/sequence.rs | 2 +- crates/capi/src/bytearrayobject.rs | 2 +- crates/capi/src/bytesobject.rs | 9 +++-- crates/capi/src/ceval.rs | 6 ++-- crates/capi/src/complexobject.rs | 4 +-- crates/capi/src/descrobject.rs | 4 +-- crates/capi/src/dictobject.rs | 45 +++++++++++++++++++++--- crates/capi/src/floatobject.rs | 4 +-- crates/capi/src/import.rs | 35 ++++++++++++++++-- crates/capi/src/listobject.rs | 2 +- crates/capi/src/longobject.rs | 9 +++-- crates/capi/src/methodobject.rs | 8 ++--- crates/capi/src/object.rs | 2 +- crates/capi/src/pycapsule.rs | 4 +-- crates/capi/src/pyerrors.rs | 41 +++++++++++++++++++-- crates/capi/src/pylifecycle.rs | 33 +++++++++++++++-- crates/capi/src/refcount.rs | 31 +++++++++++++++- crates/capi/src/setobject.rs | 2 +- crates/capi/src/sliceobject.rs | 2 +- crates/capi/src/tupleobject.rs | 8 ++--- crates/capi/src/unicodeobject.rs | 2 +- crates/capi/src/warnings.rs | 2 +- crates/capi/src/weakrefobject.rs | 2 +- crates/vm/src/stdlib/_ctypes/function.rs | 23 ++++++++++++ 32 files changed, 276 insertions(+), 54 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index abc2173c6e7..ff31606a540 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -342,6 +342,7 @@ jobs: with: openssl: true + # Keep features in sync with update-caches.yml CARGO_ARGS. - name: build rustpython run: cargo build --release --verbose --features=threading,jit ${{ env.CARGO_ARGS }} diff --git a/.github/workflows/update-caches.yml b/.github/workflows/update-caches.yml index fc524fa738e..2a245a30edd 100644 --- a/.github/workflows/update-caches.yml +++ b/.github/workflows/update-caches.yml @@ -19,6 +19,7 @@ env: CARGO_PROFILE_TEST_DEBUG: 0 CARGO_PROFILE_DEV_DEBUG: 0 CARGO_PROFILE_RELEASE_DEBUG: 0 + # Keep feature list in sync with CI's release build in .github/workflows/ci.yaml. CARGO_ARGS: --workspace --no-default-features --features stdlib,importlib,stdio,encodings,sqlite,ssl-rustls-aws-lc,host_env,threading,jit --exclude rustpython_wasm --exclude rustpython-compiler-source --exclude rustpython-venvlauncher jobs: diff --git a/Cargo.toml b/Cargo.toml index 280b64f01e5..288a49d5851 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -127,6 +127,7 @@ panic = "abort" [patch.crates-io] parking_lot_core = { git = "https://github.com/youknowone/parking_lot", branch = "rustpython" } +pyo3-ffi = { git = "https://github.com/PyO3/pyo3" } # REDOX START, Uncomment when you want to compile/check with redoxer # REDOX END diff --git a/crates/capi/pyo3-rustpython.config b/crates/capi/pyo3-rustpython.config index fe59e46e895..ce725b9bf46 100644 --- a/crates/capi/pyo3-rustpython.config +++ b/crates/capi/pyo3-rustpython.config @@ -1,4 +1,4 @@ -implementation=CPython +implementation=RustPython version=3.14 shared=true abi3=true diff --git a/crates/capi/src/abstract_.rs b/crates/capi/src/abstract_.rs index d01e31e9626..cd390135be4 100644 --- a/crates/capi/src/abstract_.rs +++ b/crates/capi/src/abstract_.rs @@ -178,3 +178,40 @@ pub unsafe extern "C" fn PyObject_Size(obj: *mut PyObject) -> isize { obj.length(vm) }) } + +#[cfg(test)] +mod tests { + use pyo3::prelude::*; + use pyo3::types::{PyDict, PyString}; + + #[test] + fn call_method1() { + Python::attach(|py| { + let string = PyString::new(py, "Hello, World!"); + assert!( + string + .call_method1("endswith", ("!",)) + .unwrap() + .is_truthy() + .unwrap() + ); + }) + } + + #[test] + fn object_set_get_del_item() { + Python::attach(|py| { + let obj = PyDict::new(py).into_any(); + obj.set_item("key", "value").unwrap(); + assert_eq!( + obj.get_item("key") + .unwrap() + .cast_into::() + .unwrap(), + "value" + ); + obj.del_item("key").unwrap(); + assert!(obj.get_item("key").is_err()); + }) + } +} diff --git a/crates/capi/src/abstract_/iter.rs b/crates/capi/src/abstract_/iter.rs index 1ba5bd04d19..fbd1440e0d0 100644 --- a/crates/capi/src/abstract_/iter.rs +++ b/crates/capi/src/abstract_/iter.rs @@ -89,7 +89,7 @@ pub unsafe extern "C" fn PyIter_Send( }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::{PyAnyMethods, PyIterator, PyList, PySendResult}; diff --git a/crates/capi/src/abstract_/mapping.rs b/crates/capi/src/abstract_/mapping.rs index 18b188613dc..54cf175ff7b 100644 --- a/crates/capi/src/abstract_/mapping.rs +++ b/crates/capi/src/abstract_/mapping.rs @@ -37,7 +37,7 @@ pub unsafe extern "C" fn PyMapping_Items(obj: *mut PyObject) -> *mut PyObject { }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::{PyDict, PyMapping, PyMappingMethods, PyTuple}; diff --git a/crates/capi/src/abstract_/number.rs b/crates/capi/src/abstract_/number.rs index c5ec492a73d..912ac677682 100644 --- a/crates/capi/src/abstract_/number.rs +++ b/crates/capi/src/abstract_/number.rs @@ -30,7 +30,7 @@ pub unsafe extern "C" fn PyNumber_Subtract(o1: *mut PyObject, o2: *mut PyObject) with_vm(|vm| vm._sub(unsafe { &*o1 }, unsafe { &*o2 })) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; diff --git a/crates/capi/src/abstract_/sequence.rs b/crates/capi/src/abstract_/sequence.rs index f6022b93e91..d011dfdb8eb 100644 --- a/crates/capi/src/abstract_/sequence.rs +++ b/crates/capi/src/abstract_/sequence.rs @@ -177,7 +177,7 @@ pub unsafe extern "C" fn PySequence_In(obj: *mut PyObject, value: *mut PyObject) unsafe { PySequence_Contains(obj, value) } } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::{PyAnyMethods, PyDict, PyList, PySequence, PySequenceMethods, PyTuple}; diff --git a/crates/capi/src/bytearrayobject.rs b/crates/capi/src/bytearrayobject.rs index 2bc56895ba8..cc9db5dd50e 100644 --- a/crates/capi/src/bytearrayobject.rs +++ b/crates/capi/src/bytearrayobject.rs @@ -71,7 +71,7 @@ pub unsafe extern "C" fn PyByteArray_Resize(bytearray: *mut PyObject, len: isize }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::{PyByteArray, PyBytes}; diff --git a/crates/capi/src/bytesobject.rs b/crates/capi/src/bytesobject.rs index 1fe535efba5..f4db16af6b3 100644 --- a/crates/capi/src/bytesobject.rs +++ b/crates/capi/src/bytesobject.rs @@ -1,6 +1,5 @@ -use crate::PyObject; use crate::object::define_py_check; -use crate::pystate::with_vm; +use crate::{PyObject, pystate::with_vm}; use core::ffi::c_char; use rustpython_vm::builtins::PyBytes; @@ -46,13 +45,13 @@ pub unsafe extern "C" fn PyBytes_AsString(bytes: *mut PyObject) -> *mut c_char { }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::PyBytes; #[test] - fn test_bytes() { + fn bytes() { Python::attach(|py| { let bytes = PyBytes::new(py, b"Hello, World!"); assert_eq!(bytes.as_bytes(), b"Hello, World!"); @@ -60,7 +59,7 @@ mod tests { } #[test] - fn test_bytes_uninit() { + fn bytes_uninit() { Python::attach(|py| { let bytes = PyBytes::new_with(py, 13, |data| { data.copy_from_slice(b"Hello, World!"); diff --git a/crates/capi/src/ceval.rs b/crates/capi/src/ceval.rs index d28dad4d6df..babaa1650d5 100644 --- a/crates/capi/src/ceval.rs +++ b/crates/capi/src/ceval.rs @@ -72,13 +72,13 @@ pub extern "C" fn PyEval_GetBuiltins() -> *mut PyObject { }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::exceptions::PyException; use pyo3::prelude::*; #[test] - fn test_code_eval() { + fn code_eval() { Python::attach(|py| { let result = py.eval(c"1 + 1", None, None).unwrap(); assert_eq!(result.extract::().unwrap(), 2); @@ -86,7 +86,7 @@ mod tests { } #[test] - fn test_code_run_exception() { + fn code_run_exception() { Python::attach(|py| { let err = py.run(c"raise Exception()", None, None).unwrap_err(); assert!(err.is_instance_of::(py)); diff --git a/crates/capi/src/complexobject.rs b/crates/capi/src/complexobject.rs index a6b2bb731a0..79f1804d1bd 100644 --- a/crates/capi/src/complexobject.rs +++ b/crates/capi/src/complexobject.rs @@ -36,13 +36,13 @@ pub unsafe extern "C" fn PyComplex_ImagAsDouble(obj: *mut PyObject) -> c_double with_vm(|vm| try_to_complex(vm, unsafe { &*obj }).map(|complex| complex.im)) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::PyComplex; #[test] - fn test_py_int() { + fn py_int() { Python::attach(|py| { let number = PyComplex::from_doubles(py, 1.0, 2.0); assert_eq!(number.real(), 1.0); diff --git a/crates/capi/src/descrobject.rs b/crates/capi/src/descrobject.rs index 5232634fabb..b0d24667dc7 100644 --- a/crates/capi/src/descrobject.rs +++ b/crates/capi/src/descrobject.rs @@ -11,7 +11,7 @@ pub unsafe extern "C" fn PyDictProxy_New(mapping: *mut PyObject) -> *mut PyObjec }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::{PyDict, PyInt, PyMappingProxy}; @@ -23,7 +23,7 @@ mod tests { dict.set_item("x", 7).unwrap(); let mapping = dict.as_mapping(); - let proxy = PyMappingProxy::new(py, &mapping); + let proxy = PyMappingProxy::new(py, mapping); let value = proxy.get_item("x").unwrap().cast_into::().unwrap(); assert_eq!(value, 7); }) diff --git a/crates/capi/src/dictobject.rs b/crates/capi/src/dictobject.rs index ebc4a827f36..e326ba87e3a 100644 --- a/crates/capi/src/dictobject.rs +++ b/crates/capi/src/dictobject.rs @@ -57,6 +57,43 @@ pub unsafe extern "C" fn PyDict_GetItemRef( }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyDict_SetDefaultRef( + dict: *mut PyObject, + key: *mut PyObject, + default_value: *mut PyObject, + result: *mut *mut PyObject, +) -> c_int { + with_vm(|vm| { + let result = NonNull::new(result); + if let Some(result) = result { + unsafe { + result.write(core::ptr::null_mut()); + } + } + let dict = unsafe { &*dict }.try_downcast_ref::(vm)?; + let key = unsafe { &*key }; + + if let Some(value) = dict.inner_getitem_opt(key, vm)? { + if let Some(result) = result { + unsafe { + result.write(value.into_raw().as_ptr()); + } + } + Ok(true) + } else { + let value = unsafe { &*default_value }.to_owned(); + dict.inner_setitem(key, value.clone(), vm)?; + if let Some(result) = result { + unsafe { + result.write(value.into_raw().as_ptr()); + } + } + Ok(false) + } + }) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyDict_Size(dict: *mut PyObject) -> isize { with_vm(|vm| { @@ -187,13 +224,13 @@ pub unsafe extern "C" fn PyDict_Next( }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::{IntoPyDict, PyDict, PyDictMethods, PyInt, PyList}; #[test] - fn test_create_empty_dict() { + fn create_empty_dict() { Python::attach(|py| { let dict = PyDict::new(py); assert!(dict.is_instance_of::()); @@ -201,7 +238,7 @@ mod tests { } #[test] - fn test_create_dict_with_items() { + fn create_dict_with_items() { Python::attach(|py| { let dict = [(1, 2), (3, 4)].into_py_dict(py)?; let value = dict.get_item(1)?.unwrap().cast_into::()?; @@ -214,7 +251,7 @@ mod tests { } #[test] - fn test_dict_iter() { + fn dict_iter() { Python::attach(|py| { let dict = [(1, 2), (3, 4)].into_py_dict(py).unwrap(); let values = dict diff --git a/crates/capi/src/floatobject.rs b/crates/capi/src/floatobject.rs index f1bb078106d..d02d2f0fd20 100644 --- a/crates/capi/src/floatobject.rs +++ b/crates/capi/src/floatobject.rs @@ -24,14 +24,14 @@ pub unsafe extern "C" fn PyFloat_AsDouble(obj: *mut PyObject) -> c_double { }) } -#[cfg(false)] +#[cfg(test)] mod tests { use core::f64::consts::PI; use pyo3::prelude::*; use pyo3::types::PyFloat; #[test] - fn test_py_float() { + fn py_float() { Python::attach(|py| { let pi = PyFloat::new(py, PI); assert!(pi.is_instance_of::()); diff --git a/crates/capi/src/import.rs b/crates/capi/src/import.rs index d380d1f8266..fa2e53b9d5d 100644 --- a/crates/capi/src/import.rs +++ b/crates/capi/src/import.rs @@ -1,5 +1,6 @@ use crate::{PyObject, pystate::with_vm}; -use rustpython_vm::builtins::PyStr; +use core::ffi::{CStr, c_char}; +use rustpython_vm::builtins::{PyDict, PyModule, PyStr}; #[unsafe(no_mangle)] pub unsafe extern "C" fn PyImport_Import(name: *mut PyObject) -> *mut PyObject { @@ -9,12 +10,40 @@ pub unsafe extern "C" fn PyImport_Import(name: *mut PyObject) -> *mut PyObject { }) } -#[cfg(false)] +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyImport_AddModuleRef(name: *const c_char) -> *mut PyObject { + with_vm(|vm| { + let name = unsafe { CStr::from_ptr(name) } + .to_str() + .map_err(|_| vm.new_system_error("PyImport_AddModuleRef called with non utf8 name"))?; + + let sys_modules = vm + .sys_module + .get_attr(rustpython_vm::identifier!(vm, modules), vm)?; + + sys_modules + .try_downcast_ref::(vm)? + .get_item_opt(name, vm)? + .map_or_else( + || { + let module = vm.new_module(name, vm.ctx.new_dict(), None); + sys_modules.set_item(name, module.clone().into(), vm)?; + Ok(module) + }, + |module| { + let module = module.try_downcast_ref::(vm)?; + Ok(module.to_owned()) + }, + ) + }) +} + +#[cfg(test)] mod tests { use pyo3::prelude::*; #[test] - fn test_import() { + fn import() { Python::attach(|py| { let _module = py.import("sys").unwrap(); }) diff --git a/crates/capi/src/listobject.rs b/crates/capi/src/listobject.rs index 796720f99d7..03069b0495b 100644 --- a/crates/capi/src/listobject.rs +++ b/crates/capi/src/listobject.rs @@ -165,7 +165,7 @@ pub unsafe extern "C" fn PyList_Sort(list: *mut PyObject) -> c_int { }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::exceptions::PyIndexError; use pyo3::prelude::*; diff --git a/crates/capi/src/longobject.rs b/crates/capi/src/longobject.rs index 8c9fe5e1acb..0bb8183b493 100644 --- a/crates/capi/src/longobject.rs +++ b/crates/capi/src/longobject.rs @@ -1,6 +1,5 @@ -use crate::PyObject; use crate::object::define_py_check; -use crate::pystate::with_vm; +use crate::{PyObject, pystate::with_vm}; use core::ffi::{c_long, c_longlong, c_ulong, c_ulonglong}; use rustpython_vm::PyResult; use rustpython_vm::builtins::PyInt; @@ -64,13 +63,13 @@ pub unsafe extern "C" fn PyLong_AsUnsignedLongLong(obj: *mut PyObject) -> c_ulon }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::PyInt; #[test] - fn test_py_int_u32() { + fn py_int_u32() { Python::attach(|py| { let number = PyInt::new(py, 123); assert!(number.is_instance_of::()); @@ -79,7 +78,7 @@ mod tests { } #[test] - fn test_py_int_u64() { + fn py_int_u64() { Python::attach(|py| { let number = PyInt::new(py, 123u64); assert!(number.is_instance_of::()); diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index c0a6611a01f..b234ba76a9c 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -306,7 +306,7 @@ pub unsafe extern "C" fn PyCFunction_NewEx( unsafe { PyCMethod_New(ml, slf, module, core::ptr::null_mut()) } } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::exceptions::PyException; use pyo3::ffi::{PyLong_FromLong, PyObject}; @@ -314,7 +314,7 @@ mod tests { use pyo3::types::{PyCFunction, PyInt, PyString}; #[test] - fn test_closure_function() { + fn closure_function() { Python::attach(|py| { let f = PyCFunction::new_closure(py, None, None, |_args, _kwargs| "Hello from Rust!") .unwrap(); @@ -327,7 +327,7 @@ mod tests { } #[test] - fn test_function_no_args() { + fn function_no_args() { Python::attach(|py| { unsafe extern "C" fn c_fn(_self: *mut PyObject, _args: *mut PyObject) -> *mut PyObject { assert!(_self.is_null()); @@ -352,7 +352,7 @@ mod tests { } #[test] - fn test_closure_function_error() { + fn closure_function_error() { Python::attach(|py| { let f = PyCFunction::new_closure(py, None, None, |_args, _kwargs| { Err::<(), _>(PyException::new_err("Something went wrong")) diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 5e601d3817d..f30d229a924 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -325,7 +325,7 @@ pub unsafe extern "C" fn PyObject_GenericSetDict( }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::class::basic::CompareOp; use pyo3::prelude::*; diff --git a/crates/capi/src/pycapsule.rs b/crates/capi/src/pycapsule.rs index 7a5d599c851..a1b5effd88c 100644 --- a/crates/capi/src/pycapsule.rs +++ b/crates/capi/src/pycapsule.rs @@ -142,13 +142,13 @@ fn checked_capsule<'a>( Ok(capsule) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::PyCapsule; #[test] - fn test_capsule_new() { + fn capsule_new() { Python::attach(|py| { let value = String::from("Some data"); let capsule = PyCapsule::new_with_value(py, value, c"my_capsule").unwrap(); diff --git a/crates/capi/src/pyerrors.rs b/crates/capi/src/pyerrors.rs index b767b7c4090..52343566e35 100644 --- a/crates/capi/src/pyerrors.rs +++ b/crates/capi/src/pyerrors.rs @@ -299,9 +299,31 @@ pub unsafe extern "C" fn PyException_GetContext(exc: *mut PyObject) -> *mut PyOb }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyException_SetCause(exc: *mut PyObject, cause: *mut PyObject) { + with_vm(|vm| { + let exc = unsafe { &*exc }.try_downcast_ref::(vm)?; + let cause = NonNull::new(cause) + .map(|obj| unsafe { PyObjectRef::from_raw(obj).downcast_unchecked() }); + exc.set___cause__(cause); + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyException_SetTraceback(exc: *mut PyObject, tb: *mut PyObject) -> c_int { + with_vm(|vm| { + let exc = unsafe { &*exc }.try_downcast_ref::(vm)?; + let traceback = unsafe { tb.as_ref() }.map(|obj| obj.to_owned()); + exc.set___traceback__(vm.unwrap_or_none(traceback), vm) + }) +} + #[cfg(test)] mod tests { - use pyo3::exceptions::PyTypeError; + use pyo3::PyTypeInfo; + use pyo3::create_exception; + use pyo3::exceptions::{PyException, PyTypeError}; use pyo3::prelude::*; #[test] @@ -309,7 +331,7 @@ mod tests { Python::attach(|py| { PyTypeError::new_err(py.None()).restore(py); assert!(PyErr::occurred(py)); - assert!(unsafe { !pyo3::ffi::PyErr_GetRaisedException().is_null() }); + assert!(PyErr::take(py).is_some()); assert!(!PyErr::occurred(py)); }) } @@ -321,4 +343,19 @@ mod tests { assert!(err.is_instance_of::(py)); }) } + + #[test] + fn new_exception_type() { + create_exception!(my_module, MyError, PyException, "Some description."); + + Python::attach(|py| { + let exc = MyError::new_err("This is a new exception"); + assert!(exc.is_instance_of::(py)); + let exc_type = MyError::type_object(py); + assert_eq!( + exc_type.fully_qualified_name().unwrap(), + "my_module.MyError" + ); + }) + } } diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index 6760b2822a3..8ba533e8ecd 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -1,10 +1,12 @@ use crate::get_main_interpreter; use crate::pyerrors::init_exception_statics; use crate::pystate::ensure_thread_has_vm_attached; -use core::ffi::c_int; +use alloc::ffi::CString; +use core::ffi::{c_char, c_int, c_ulong}; +use rustpython_vm::version::{MAJOR, MICRO, MINOR, VERSION_HEX}; use rustpython_vm::vm::thread::ThreadedVirtualMachine; use rustpython_vm::{Context, Interpreter}; -use std::sync::Mutex; +use std::sync::{LazyLock, Mutex}; pub(crate) static MAIN_INTERP: Mutex