rubyx-py 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +97 -28
- data/docs/assets/logo.png +0 -0
- data/ext/rubyx/src/context.rs +45 -14
- data/ext/rubyx/src/eval.rs +14 -60
- data/ext/rubyx/src/future.rs +534 -14
- data/ext/rubyx/src/gvl.rs +70 -0
- data/ext/rubyx/src/lib.rs +83 -32
- data/ext/rubyx/src/nonblocking_stream.rs +5 -78
- data/ext/rubyx/src/python_api.rs +1027 -0
- data/ext/rubyx/src/rubyx_object.rs +1159 -4
- data/ext/rubyx/src/rubyx_stream.rs +168 -0
- data/ext/rubyx/src/stream.rs +225 -3
- data/lib/rubyx/railtie.rb +2 -1
- data/lib/rubyx/tasks/rubyx.rake +127 -0
- data/lib/rubyx/version.rb +2 -2
- metadata +4 -1
|
@@ -4,10 +4,11 @@ use crate::python_ffi::PyObject;
|
|
|
4
4
|
use crate::python_guard::PyGuard;
|
|
5
5
|
use crate::ruby_helpers;
|
|
6
6
|
use crate::stream::SendableValue;
|
|
7
|
+
use magnus::encoding::EncodingCapable;
|
|
7
8
|
use magnus::r_hash::ForEach;
|
|
8
9
|
use magnus::typed_data::Obj;
|
|
9
10
|
use magnus::value::ReprValue;
|
|
10
|
-
use magnus::{Class, IntoValue, RHash, Ruby, Symbol, TryConvert, Value};
|
|
11
|
+
use magnus::{Class, IntoValue, RHash, RString, Ruby, Symbol, TryConvert, Value};
|
|
11
12
|
use std::ffi::CString;
|
|
12
13
|
|
|
13
14
|
const RUBY_IMPLICIT_CONVERSIONS: &[&str] = &[
|
|
@@ -51,6 +52,24 @@ pub(crate) fn python_to_sendable(
|
|
|
51
52
|
};
|
|
52
53
|
return Ok(SendableValue::Str(val));
|
|
53
54
|
}
|
|
55
|
+
if api.bytes_check(py_val) {
|
|
56
|
+
let Some(val) = api.bytes_to_vec(py_val) else {
|
|
57
|
+
if api.has_error() {
|
|
58
|
+
api.clear_error();
|
|
59
|
+
}
|
|
60
|
+
return Err("Cannot decode Python bytes".to_string());
|
|
61
|
+
};
|
|
62
|
+
return Ok(SendableValue::Bytes(val));
|
|
63
|
+
}
|
|
64
|
+
if api.bytearray_check(py_val) {
|
|
65
|
+
let Some(val) = api.bytearray_to_vec(py_val) else {
|
|
66
|
+
if api.has_error() {
|
|
67
|
+
api.clear_error();
|
|
68
|
+
}
|
|
69
|
+
return Err("Cannot read Python bytearray".to_string());
|
|
70
|
+
};
|
|
71
|
+
return Ok(SendableValue::Bytes(val));
|
|
72
|
+
}
|
|
54
73
|
if api.tuple_check(py_val) {
|
|
55
74
|
let len = api.tuple_size(py_val);
|
|
56
75
|
let mut items = Vec::with_capacity(len as usize);
|
|
@@ -61,6 +80,32 @@ pub(crate) fn python_to_sendable(
|
|
|
61
80
|
return Ok(SendableValue::List(items));
|
|
62
81
|
}
|
|
63
82
|
|
|
83
|
+
if api.is_set(py_val) || api.is_frozen_set(py_val) {
|
|
84
|
+
let len = api.set_size(py_val);
|
|
85
|
+
let mut items = Vec::with_capacity(len as usize);
|
|
86
|
+
let iter = api.object_get_iter(py_val);
|
|
87
|
+
loop {
|
|
88
|
+
let item = api.iter_next(iter);
|
|
89
|
+
if item.is_null() {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
let result = python_to_sendable(item, api);
|
|
93
|
+
api.decref(item);
|
|
94
|
+
match result {
|
|
95
|
+
Ok(val) => items.push(val),
|
|
96
|
+
Err(e) => {
|
|
97
|
+
api.decref(iter);
|
|
98
|
+
return Err(e);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if api.has_error() {
|
|
103
|
+
api.clear_error();
|
|
104
|
+
}
|
|
105
|
+
api.decref(iter);
|
|
106
|
+
return Ok(SendableValue::Set(items));
|
|
107
|
+
}
|
|
108
|
+
|
|
64
109
|
if api.list_check(py_val) {
|
|
65
110
|
let len = api.list_size(py_val);
|
|
66
111
|
let mut items = Vec::with_capacity(len as usize);
|
|
@@ -91,6 +136,14 @@ pub(crate) fn python_to_sendable(
|
|
|
91
136
|
if py_val == api.py_false {
|
|
92
137
|
return Ok(SendableValue::Bool(false));
|
|
93
138
|
}
|
|
139
|
+
let has_dict = {
|
|
140
|
+
let name = std::ffi::CString::new("__dict__").unwrap();
|
|
141
|
+
api.object_has_attr_string(py_val, name.as_ptr()) != 0
|
|
142
|
+
};
|
|
143
|
+
if has_dict || api.callable_check(py_val) != 0 {
|
|
144
|
+
api.incref(py_val);
|
|
145
|
+
return Ok(SendableValue::PyObjectRef(py_val as usize));
|
|
146
|
+
}
|
|
94
147
|
Err("Cannot convert Python value to Ruby".to_string())
|
|
95
148
|
}
|
|
96
149
|
pub(crate) fn ruby_to_python(
|
|
@@ -145,6 +198,24 @@ pub(crate) fn ruby_to_python(
|
|
|
145
198
|
return Ok(py_str);
|
|
146
199
|
}
|
|
147
200
|
if value.is_kind_of(ruby.class_string()) {
|
|
201
|
+
let rstr = RString::try_convert(value)?;
|
|
202
|
+
// ASCII-8BIT means bytes/bytearray
|
|
203
|
+
let enc = rstr.enc_get();
|
|
204
|
+
let ascii_8_bit = ruby
|
|
205
|
+
.find_encindex("ASCII-8BIT")
|
|
206
|
+
.map_err(|_| magnus::Error::new(ruby_helpers::runtime_error(), "No ASCII-8BIT"))?;
|
|
207
|
+
if enc == ascii_8_bit {
|
|
208
|
+
let bytes = unsafe { rstr.as_slice() };
|
|
209
|
+
let py_bytes = api.bytes_from_slice(bytes);
|
|
210
|
+
if py_bytes.is_null() {
|
|
211
|
+
return Err(magnus::Error::new(
|
|
212
|
+
ruby_helpers::runtime_error(),
|
|
213
|
+
"Failed to create Python bytes from String",
|
|
214
|
+
));
|
|
215
|
+
}
|
|
216
|
+
return Ok(py_bytes);
|
|
217
|
+
}
|
|
218
|
+
// UTF-8 string conversion
|
|
148
219
|
let val = String::try_convert(value)?;
|
|
149
220
|
return val
|
|
150
221
|
.to_python(api)
|
|
@@ -754,6 +825,134 @@ impl RubyxObject {
|
|
|
754
825
|
result
|
|
755
826
|
}
|
|
756
827
|
|
|
828
|
+
pub fn call(&self, args: &[magnus::Value]) -> Result<Value, magnus::Error> {
|
|
829
|
+
let api = self.api;
|
|
830
|
+
let gil = api.ensure_gil();
|
|
831
|
+
let result = (|| -> Result<Value, magnus::Error> {
|
|
832
|
+
if api.callable_check(self.py_obj) == 0 {
|
|
833
|
+
return Err(magnus::Error::new(
|
|
834
|
+
ruby_helpers::type_error(),
|
|
835
|
+
"Python object is not callable",
|
|
836
|
+
));
|
|
837
|
+
}
|
|
838
|
+
let ruby = Ruby::get().map_err(|e| {
|
|
839
|
+
magnus::Error::new(
|
|
840
|
+
ruby_helpers::runtime_error(),
|
|
841
|
+
format!("Ruby VM handle unavailable: {e}"),
|
|
842
|
+
)
|
|
843
|
+
})?;
|
|
844
|
+
|
|
845
|
+
// Extract positional and keyword arguments
|
|
846
|
+
let (positional, kwargs) = if let Some(last) = args.last() {
|
|
847
|
+
if last.is_kind_of(ruby.class_hash()) {
|
|
848
|
+
(&args[..args.len() - 1], Some(RHash::try_convert(*last)?))
|
|
849
|
+
} else {
|
|
850
|
+
(args, None)
|
|
851
|
+
}
|
|
852
|
+
} else {
|
|
853
|
+
(args, None)
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
// Args Tuple for args
|
|
857
|
+
let py_args = api.tuple_new(positional.len() as isize);
|
|
858
|
+
if py_args.is_null() {
|
|
859
|
+
return Err(magnus::Error::new(
|
|
860
|
+
ruby_helpers::runtime_error(),
|
|
861
|
+
"Failed to allocate Python args tuple",
|
|
862
|
+
));
|
|
863
|
+
}
|
|
864
|
+
let py_args_guard = PyGuard::new(py_args, api).ok_or_else(|| {
|
|
865
|
+
magnus::Error::new(ruby_helpers::runtime_error(), "Null Python args tuple")
|
|
866
|
+
})?;
|
|
867
|
+
for (i, arg) in positional.iter().enumerate() {
|
|
868
|
+
let py_arg = ruby_to_python(*arg, api)?;
|
|
869
|
+
if api.tuple_set_item(py_args_guard.ptr(), i as isize, py_arg) != 0 {
|
|
870
|
+
api.decref(py_arg);
|
|
871
|
+
if let Some(py_err) = PythonApi::extract_exception(api) {
|
|
872
|
+
return Err(magnus::Error::from(py_err));
|
|
873
|
+
}
|
|
874
|
+
return Err(magnus::Error::new(
|
|
875
|
+
ruby_helpers::runtime_error(),
|
|
876
|
+
"Failed to set tuple argument",
|
|
877
|
+
));
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// kwargs dict
|
|
882
|
+
let py_kwargs_guard = if let Some(hash) = kwargs {
|
|
883
|
+
let dict = api.dict_new();
|
|
884
|
+
if dict.is_null() {
|
|
885
|
+
return Err(magnus::Error::new(
|
|
886
|
+
ruby_helpers::runtime_error(),
|
|
887
|
+
"Failed to allocate kwargs dict",
|
|
888
|
+
));
|
|
889
|
+
}
|
|
890
|
+
let guard = PyGuard::new(dict, api).ok_or_else(|| {
|
|
891
|
+
magnus::Error::new(ruby_helpers::runtime_error(), "Null kwargs dict")
|
|
892
|
+
})?;
|
|
893
|
+
hash.foreach(|k: Value, v: Value| {
|
|
894
|
+
let key = if let Ok(s) = String::try_convert(k) {
|
|
895
|
+
s
|
|
896
|
+
} else if let Ok(sym) = Symbol::try_convert(k) {
|
|
897
|
+
sym.name()?.to_string()
|
|
898
|
+
} else {
|
|
899
|
+
return Err(magnus::Error::new(
|
|
900
|
+
ruby_helpers::type_error(),
|
|
901
|
+
"kwargs keys must be String or Symbol",
|
|
902
|
+
));
|
|
903
|
+
};
|
|
904
|
+
let py_key = key.to_python(api).map_err(|e| {
|
|
905
|
+
magnus::Error::new(ruby_helpers::runtime_error(), format!("{e:?}"))
|
|
906
|
+
})?;
|
|
907
|
+
let py_val = ruby_to_python(v, api)?;
|
|
908
|
+
let rc = api.dict_set_item(guard.ptr(), py_key, py_val);
|
|
909
|
+
api.decref(py_key);
|
|
910
|
+
api.decref(py_val);
|
|
911
|
+
if rc != 0 {
|
|
912
|
+
if let Some(py_err) = PythonApi::extract_exception(api) {
|
|
913
|
+
return Err(magnus::Error::from(py_err));
|
|
914
|
+
}
|
|
915
|
+
return Err(magnus::Error::new(
|
|
916
|
+
ruby_helpers::runtime_error(),
|
|
917
|
+
"Failed to set kwargs item",
|
|
918
|
+
));
|
|
919
|
+
}
|
|
920
|
+
Ok(ForEach::Continue)
|
|
921
|
+
})?;
|
|
922
|
+
Some(guard)
|
|
923
|
+
} else {
|
|
924
|
+
None
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
let py_kwargs_ptr = py_kwargs_guard
|
|
928
|
+
.as_ref()
|
|
929
|
+
.map_or(std::ptr::null_mut(), |g| g.ptr());
|
|
930
|
+
// call the python callable with args and kwargs
|
|
931
|
+
let py_result = api.object_call(self.py_obj, py_args_guard.ptr(), py_kwargs_ptr);
|
|
932
|
+
if py_result.is_null() {
|
|
933
|
+
if let Some(py_err) = PythonApi::extract_exception(api) {
|
|
934
|
+
return Err(magnus::Error::from(py_err));
|
|
935
|
+
}
|
|
936
|
+
return Err(magnus::Error::new(
|
|
937
|
+
ruby_helpers::runtime_error(),
|
|
938
|
+
"Python call failed",
|
|
939
|
+
));
|
|
940
|
+
}
|
|
941
|
+
let py_result_guard = PyGuard::new(py_result, api).ok_or_else(|| {
|
|
942
|
+
magnus::Error::new(ruby_helpers::runtime_error(), "Null Python result")
|
|
943
|
+
})?;
|
|
944
|
+
let wrapper = RubyxObject::new(py_result_guard.ptr(), api).ok_or_else(|| {
|
|
945
|
+
magnus::Error::new(
|
|
946
|
+
ruby_helpers::runtime_error(),
|
|
947
|
+
"Failed to wrap Python result",
|
|
948
|
+
)
|
|
949
|
+
})?;
|
|
950
|
+
Ok(wrapper.into_value_with(&ruby))
|
|
951
|
+
})();
|
|
952
|
+
api.release_gil(gil);
|
|
953
|
+
result
|
|
954
|
+
}
|
|
955
|
+
|
|
757
956
|
pub fn py_type(&self) -> Result<String, magnus::Error> {
|
|
758
957
|
let gil = self.api.ensure_gil();
|
|
759
958
|
let result = self.api.type_name(self.py_obj);
|
|
@@ -1183,13 +1382,13 @@ mod tests {
|
|
|
1183
1382
|
|
|
1184
1383
|
#[test]
|
|
1185
1384
|
#[serial]
|
|
1186
|
-
fn
|
|
1385
|
+
fn test_to_ruby_wraps_module_as_rubyx_object() {
|
|
1187
1386
|
with_ruby_python(|_ruby, api| {
|
|
1188
1387
|
let module = api.import_module("os").expect("os should import");
|
|
1189
1388
|
let wrapper = RubyxObject::new(module, api).unwrap();
|
|
1190
1389
|
assert!(
|
|
1191
|
-
wrapper.to_ruby().
|
|
1192
|
-
"module should
|
|
1390
|
+
wrapper.to_ruby().is_ok(),
|
|
1391
|
+
"module should convert to RubyxObject via PyObjectRef"
|
|
1193
1392
|
);
|
|
1194
1393
|
drop(wrapper);
|
|
1195
1394
|
api.decref(module);
|
|
@@ -1660,6 +1859,206 @@ mod tests {
|
|
|
1660
1859
|
});
|
|
1661
1860
|
}
|
|
1662
1861
|
|
|
1862
|
+
// ========== call tests ==========
|
|
1863
|
+
|
|
1864
|
+
#[test]
|
|
1865
|
+
#[serial]
|
|
1866
|
+
fn test_call_lambda_no_args() {
|
|
1867
|
+
with_ruby_python(|_ruby, api| {
|
|
1868
|
+
let globals = crate::eval::make_globals(api);
|
|
1869
|
+
let py_func = api
|
|
1870
|
+
.run_string("lambda: 42", 258, globals.ptr(), globals.ptr())
|
|
1871
|
+
.expect("lambda eval should succeed");
|
|
1872
|
+
let wrapper = RubyxObject::new(py_func, api).unwrap();
|
|
1873
|
+
|
|
1874
|
+
let result = wrapper.call(&[]).expect("call should succeed");
|
|
1875
|
+
let obj = Obj::<RubyxObject>::try_convert(result).expect("should be RubyxObject");
|
|
1876
|
+
assert_eq!(api.long_to_i64(obj.as_ptr()), 42);
|
|
1877
|
+
|
|
1878
|
+
drop(wrapper);
|
|
1879
|
+
api.decref(py_func);
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
#[test]
|
|
1884
|
+
#[serial]
|
|
1885
|
+
fn test_call_lambda_with_args() {
|
|
1886
|
+
with_ruby_python(|ruby, api| {
|
|
1887
|
+
let globals = crate::eval::make_globals(api);
|
|
1888
|
+
let py_func = api
|
|
1889
|
+
.run_string("lambda x, y: x + y", 258, globals.ptr(), globals.ptr())
|
|
1890
|
+
.expect("lambda eval should succeed");
|
|
1891
|
+
let wrapper = RubyxObject::new(py_func, api).unwrap();
|
|
1892
|
+
|
|
1893
|
+
let args = vec![3_i64.into_value_with(ruby), 4_i64.into_value_with(ruby)];
|
|
1894
|
+
let result = wrapper.call(&args).expect("call should succeed");
|
|
1895
|
+
let obj = Obj::<RubyxObject>::try_convert(result).expect("should be RubyxObject");
|
|
1896
|
+
assert_eq!(api.long_to_i64(obj.as_ptr()), 7);
|
|
1897
|
+
|
|
1898
|
+
drop(wrapper);
|
|
1899
|
+
api.decref(py_func);
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
#[test]
|
|
1904
|
+
#[serial]
|
|
1905
|
+
fn test_call_builtin_function() {
|
|
1906
|
+
with_ruby_python(|ruby, api| {
|
|
1907
|
+
let builtins = api
|
|
1908
|
+
.import_module("builtins")
|
|
1909
|
+
.expect("builtins should import");
|
|
1910
|
+
let len_func = api.object_get_attr_string(builtins, "len");
|
|
1911
|
+
let wrapper = RubyxObject::new(len_func, api).unwrap();
|
|
1912
|
+
|
|
1913
|
+
let globals = crate::eval::make_globals(api);
|
|
1914
|
+
let py_list = api
|
|
1915
|
+
.run_string("[1, 2, 3]", 258, globals.ptr(), globals.ptr())
|
|
1916
|
+
.expect("list eval should succeed");
|
|
1917
|
+
let list_wrapper = RubyxObject::new(py_list, api).unwrap();
|
|
1918
|
+
|
|
1919
|
+
let args = vec![list_wrapper.into_value_with(ruby)];
|
|
1920
|
+
let result = wrapper.call(&args).expect("call should succeed");
|
|
1921
|
+
let obj = Obj::<RubyxObject>::try_convert(result).expect("should be RubyxObject");
|
|
1922
|
+
assert_eq!(api.long_to_i64(obj.as_ptr()), 3);
|
|
1923
|
+
|
|
1924
|
+
drop(wrapper);
|
|
1925
|
+
api.decref(len_func);
|
|
1926
|
+
api.decref(builtins);
|
|
1927
|
+
api.decref(py_list);
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
#[test]
|
|
1932
|
+
#[serial]
|
|
1933
|
+
fn test_call_with_kwargs() {
|
|
1934
|
+
with_ruby_python(|ruby, api| {
|
|
1935
|
+
let globals = crate::eval::make_globals(api);
|
|
1936
|
+
let _ = api.run_string(
|
|
1937
|
+
"def greet(name, greeting='Hello'): return f'{greeting}, {name}!'",
|
|
1938
|
+
257,
|
|
1939
|
+
globals.ptr(),
|
|
1940
|
+
globals.ptr(),
|
|
1941
|
+
);
|
|
1942
|
+
let key = api.string_from_str("greet");
|
|
1943
|
+
let func = api.dict_get_item(globals.ptr(), key);
|
|
1944
|
+
api.decref(key);
|
|
1945
|
+
let wrapper = RubyxObject::new(func, api).unwrap();
|
|
1946
|
+
|
|
1947
|
+
let kwargs = ruby.hash_new();
|
|
1948
|
+
kwargs
|
|
1949
|
+
.aset(ruby.sym_new("greeting"), "Hi".into_value_with(ruby))
|
|
1950
|
+
.unwrap();
|
|
1951
|
+
let args = vec!["Alice".into_value_with(ruby), kwargs.into_value_with(ruby)];
|
|
1952
|
+
let result = wrapper
|
|
1953
|
+
.call(&args)
|
|
1954
|
+
.expect("call with kwargs should succeed");
|
|
1955
|
+
let obj = Obj::<RubyxObject>::try_convert(result).expect("should be RubyxObject");
|
|
1956
|
+
assert_eq!(
|
|
1957
|
+
api.string_to_string(obj.as_ptr()),
|
|
1958
|
+
Some("Hi, Alice!".to_string())
|
|
1959
|
+
);
|
|
1960
|
+
|
|
1961
|
+
drop(wrapper);
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
#[test]
|
|
1966
|
+
#[serial]
|
|
1967
|
+
fn test_call_class_as_constructor() {
|
|
1968
|
+
with_ruby_python(|ruby, api| {
|
|
1969
|
+
let globals = crate::eval::make_globals(api);
|
|
1970
|
+
let _ = api.run_string(
|
|
1971
|
+
"class Pt:\n def __init__(self, x):\n self.x = x",
|
|
1972
|
+
257,
|
|
1973
|
+
globals.ptr(),
|
|
1974
|
+
globals.ptr(),
|
|
1975
|
+
);
|
|
1976
|
+
let key = api.string_from_str("Pt");
|
|
1977
|
+
let cls = api.dict_get_item(globals.ptr(), key);
|
|
1978
|
+
api.decref(key);
|
|
1979
|
+
let wrapper = RubyxObject::new(cls, api).unwrap();
|
|
1980
|
+
|
|
1981
|
+
let args = vec![10_i64.into_value_with(ruby)];
|
|
1982
|
+
let result = wrapper.call(&args).expect("class call should succeed");
|
|
1983
|
+
let obj = Obj::<RubyxObject>::try_convert(result).expect("should be RubyxObject");
|
|
1984
|
+
|
|
1985
|
+
let x_attr = api.object_get_attr_string(obj.as_ptr(), "x");
|
|
1986
|
+
assert!(!x_attr.is_null());
|
|
1987
|
+
assert_eq!(api.long_to_i64(x_attr), 10);
|
|
1988
|
+
api.decref(x_attr);
|
|
1989
|
+
|
|
1990
|
+
drop(wrapper);
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
#[test]
|
|
1995
|
+
#[serial]
|
|
1996
|
+
fn test_call_non_callable_raises_error() {
|
|
1997
|
+
with_ruby_python(|_ruby, api| {
|
|
1998
|
+
let py_int = api.long_from_i64(42);
|
|
1999
|
+
let wrapper = RubyxObject::new(py_int, api).unwrap();
|
|
2000
|
+
|
|
2001
|
+
let result = wrapper.call(&[]);
|
|
2002
|
+
assert!(result.is_err(), "calling non-callable should error");
|
|
2003
|
+
let err_msg = result.unwrap_err().to_string();
|
|
2004
|
+
assert!(
|
|
2005
|
+
err_msg.contains("not callable"),
|
|
2006
|
+
"error should mention not callable, got: {err_msg}"
|
|
2007
|
+
);
|
|
2008
|
+
|
|
2009
|
+
drop(wrapper);
|
|
2010
|
+
api.decref(py_int);
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
#[test]
|
|
2015
|
+
#[serial]
|
|
2016
|
+
fn test_call_propagates_python_error() {
|
|
2017
|
+
with_ruby_python(|_ruby, api| {
|
|
2018
|
+
let globals = crate::eval::make_globals(api);
|
|
2019
|
+
let _ = api.run_string(
|
|
2020
|
+
"def explode(): raise ValueError('boom')",
|
|
2021
|
+
257,
|
|
2022
|
+
globals.ptr(),
|
|
2023
|
+
globals.ptr(),
|
|
2024
|
+
);
|
|
2025
|
+
let key = api.string_from_str("explode");
|
|
2026
|
+
let func = api.dict_get_item(globals.ptr(), key);
|
|
2027
|
+
api.decref(key);
|
|
2028
|
+
let wrapper = RubyxObject::new(func, api).unwrap();
|
|
2029
|
+
|
|
2030
|
+
let result = wrapper.call(&[]);
|
|
2031
|
+
assert!(result.is_err(), "call that raises should return error");
|
|
2032
|
+
let err_msg = result.unwrap_err().to_string();
|
|
2033
|
+
assert!(
|
|
2034
|
+
err_msg.contains("boom"),
|
|
2035
|
+
"error should contain Python message, got: {err_msg}"
|
|
2036
|
+
);
|
|
2037
|
+
|
|
2038
|
+
drop(wrapper);
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
#[test]
|
|
2043
|
+
#[serial]
|
|
2044
|
+
fn test_call_returns_rubyx_object() {
|
|
2045
|
+
with_ruby_python(|_ruby, api| {
|
|
2046
|
+
let globals = crate::eval::make_globals(api);
|
|
2047
|
+
let py_func = api
|
|
2048
|
+
.run_string("lambda: [1, 2, 3]", 258, globals.ptr(), globals.ptr())
|
|
2049
|
+
.expect("lambda eval should succeed");
|
|
2050
|
+
let wrapper = RubyxObject::new(py_func, api).unwrap();
|
|
2051
|
+
|
|
2052
|
+
let result = wrapper.call(&[]).expect("call should succeed");
|
|
2053
|
+
let obj = Obj::<RubyxObject>::try_convert(result).expect("should be RubyxObject");
|
|
2054
|
+
assert!(api.list_check(obj.as_ptr()));
|
|
2055
|
+
assert_eq!(api.list_size(obj.as_ptr()), 3);
|
|
2056
|
+
|
|
2057
|
+
drop(wrapper);
|
|
2058
|
+
api.decref(py_func);
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2061
|
+
|
|
1663
2062
|
// ========== is_truthy / is_falsy tests ==========
|
|
1664
2063
|
|
|
1665
2064
|
#[test]
|
|
@@ -1928,4 +2327,760 @@ mod tests {
|
|
|
1928
2327
|
drop(w);
|
|
1929
2328
|
api.decref(os);
|
|
1930
2329
|
}
|
|
2330
|
+
|
|
2331
|
+
// ========== python_to_sendable: PyObjectRef fallback ==========
|
|
2332
|
+
|
|
2333
|
+
#[test]
|
|
2334
|
+
#[serial]
|
|
2335
|
+
fn test_python_to_sendable_module_returns_py_object_ref() {
|
|
2336
|
+
use crate::test_helpers::skip_if_no_python;
|
|
2337
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2338
|
+
return;
|
|
2339
|
+
};
|
|
2340
|
+
let api = guard.api();
|
|
2341
|
+
let os = api.import_module("os").expect("os should import");
|
|
2342
|
+
let sendable = python_to_sendable(os, api).unwrap();
|
|
2343
|
+
match &sendable {
|
|
2344
|
+
SendableValue::PyObjectRef(addr) => {
|
|
2345
|
+
assert_eq!(*addr, os as usize);
|
|
2346
|
+
}
|
|
2347
|
+
other => panic!("expected PyObjectRef, got {other:?}"),
|
|
2348
|
+
}
|
|
2349
|
+
// Clean up: decref once for the sendable's incref, once for import_module
|
|
2350
|
+
api.decref(os);
|
|
2351
|
+
api.decref(os);
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
// ========== python_to_sendable: set / frozenset ==========
|
|
2355
|
+
|
|
2356
|
+
#[test]
|
|
2357
|
+
#[serial]
|
|
2358
|
+
fn test_python_to_sendable_set_returns_set() {
|
|
2359
|
+
use crate::test_helpers::skip_if_no_python;
|
|
2360
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2361
|
+
return;
|
|
2362
|
+
};
|
|
2363
|
+
let api = guard.api();
|
|
2364
|
+
let globals = api.dict_new();
|
|
2365
|
+
let builtins = api
|
|
2366
|
+
.import_module("builtins")
|
|
2367
|
+
.expect("builtins should import");
|
|
2368
|
+
let key = api.string_from_str("__builtins__");
|
|
2369
|
+
api.dict_set_item(globals, key, builtins);
|
|
2370
|
+
api.decref(key);
|
|
2371
|
+
let result = api.run_string("{1, 2, 3}", 258, globals, globals);
|
|
2372
|
+
let py_set = result.expect("set eval should succeed");
|
|
2373
|
+
assert!(!py_set.is_null());
|
|
2374
|
+
|
|
2375
|
+
let sendable = python_to_sendable(py_set, api).expect("set should convert to Set");
|
|
2376
|
+
match &sendable {
|
|
2377
|
+
SendableValue::Set(items) => {
|
|
2378
|
+
assert_eq!(items.len(), 3);
|
|
2379
|
+
let mut vals: Vec<i64> = items
|
|
2380
|
+
.iter()
|
|
2381
|
+
.map(|item| match item {
|
|
2382
|
+
SendableValue::Integer(n) => *n,
|
|
2383
|
+
other => panic!("expected Integer, got {other:?}"),
|
|
2384
|
+
})
|
|
2385
|
+
.collect();
|
|
2386
|
+
vals.sort();
|
|
2387
|
+
assert_eq!(vals, vec![1, 2, 3]);
|
|
2388
|
+
}
|
|
2389
|
+
other => panic!("expected Set, got {other:?}"),
|
|
2390
|
+
}
|
|
2391
|
+
api.decref(py_set);
|
|
2392
|
+
api.decref(builtins);
|
|
2393
|
+
api.decref(globals);
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
#[test]
|
|
2397
|
+
#[serial]
|
|
2398
|
+
fn test_python_to_sendable_frozenset_returns_set() {
|
|
2399
|
+
use crate::test_helpers::skip_if_no_python;
|
|
2400
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2401
|
+
return;
|
|
2402
|
+
};
|
|
2403
|
+
let api = guard.api();
|
|
2404
|
+
let globals = api.dict_new();
|
|
2405
|
+
let builtins = api
|
|
2406
|
+
.import_module("builtins")
|
|
2407
|
+
.expect("builtins should import");
|
|
2408
|
+
let key = api.string_from_str("__builtins__");
|
|
2409
|
+
api.dict_set_item(globals, key, builtins);
|
|
2410
|
+
api.decref(key);
|
|
2411
|
+
let result = api.run_string("frozenset({10, 20})", 258, globals, globals);
|
|
2412
|
+
let py_fset = result.expect("frozenset eval should succeed");
|
|
2413
|
+
assert!(!py_fset.is_null());
|
|
2414
|
+
|
|
2415
|
+
let sendable = python_to_sendable(py_fset, api).expect("frozenset should convert to Set");
|
|
2416
|
+
match &sendable {
|
|
2417
|
+
SendableValue::Set(items) => {
|
|
2418
|
+
assert_eq!(items.len(), 2);
|
|
2419
|
+
let mut vals: Vec<i64> = items
|
|
2420
|
+
.iter()
|
|
2421
|
+
.map(|item| match item {
|
|
2422
|
+
SendableValue::Integer(n) => *n,
|
|
2423
|
+
other => panic!("expected Integer, got {other:?}"),
|
|
2424
|
+
})
|
|
2425
|
+
.collect();
|
|
2426
|
+
vals.sort();
|
|
2427
|
+
assert_eq!(vals, vec![10, 20]);
|
|
2428
|
+
}
|
|
2429
|
+
other => panic!("expected Set, got {other:?}"),
|
|
2430
|
+
}
|
|
2431
|
+
api.decref(py_fset);
|
|
2432
|
+
api.decref(builtins);
|
|
2433
|
+
api.decref(globals);
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
#[test]
|
|
2437
|
+
#[serial]
|
|
2438
|
+
fn test_python_to_sendable_empty_set() {
|
|
2439
|
+
use crate::test_helpers::skip_if_no_python;
|
|
2440
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2441
|
+
return;
|
|
2442
|
+
};
|
|
2443
|
+
let api = guard.api();
|
|
2444
|
+
let globals = api.dict_new();
|
|
2445
|
+
let builtins = api
|
|
2446
|
+
.import_module("builtins")
|
|
2447
|
+
.expect("builtins should import");
|
|
2448
|
+
let key = api.string_from_str("__builtins__");
|
|
2449
|
+
api.dict_set_item(globals, key, builtins);
|
|
2450
|
+
api.decref(key);
|
|
2451
|
+
let result = api.run_string("set()", 258, globals, globals);
|
|
2452
|
+
let py_set = result.expect("empty set eval should succeed");
|
|
2453
|
+
assert!(!py_set.is_null());
|
|
2454
|
+
|
|
2455
|
+
let sendable = python_to_sendable(py_set, api).expect("empty set should convert");
|
|
2456
|
+
match &sendable {
|
|
2457
|
+
SendableValue::Set(items) => assert!(items.is_empty()),
|
|
2458
|
+
other => panic!("expected empty Set, got {other:?}"),
|
|
2459
|
+
}
|
|
2460
|
+
api.decref(py_set);
|
|
2461
|
+
api.decref(builtins);
|
|
2462
|
+
api.decref(globals);
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
#[test]
|
|
2466
|
+
#[serial]
|
|
2467
|
+
fn test_python_to_sendable_set_with_mixed_types() {
|
|
2468
|
+
use crate::test_helpers::skip_if_no_python;
|
|
2469
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2470
|
+
return;
|
|
2471
|
+
};
|
|
2472
|
+
let api = guard.api();
|
|
2473
|
+
let globals = api.dict_new();
|
|
2474
|
+
let builtins = api
|
|
2475
|
+
.import_module("builtins")
|
|
2476
|
+
.expect("builtins should import");
|
|
2477
|
+
let key = api.string_from_str("__builtins__");
|
|
2478
|
+
api.dict_set_item(globals, key, builtins);
|
|
2479
|
+
api.decref(key);
|
|
2480
|
+
let result = api.run_string("{42, 'hello', 3.14, True}", 258, globals, globals);
|
|
2481
|
+
let py_set = result.expect("mixed set eval should succeed");
|
|
2482
|
+
assert!(!py_set.is_null());
|
|
2483
|
+
|
|
2484
|
+
let sendable = python_to_sendable(py_set, api).expect("mixed set should convert");
|
|
2485
|
+
match &sendable {
|
|
2486
|
+
SendableValue::Set(items) => {
|
|
2487
|
+
assert_eq!(items.len(), 4);
|
|
2488
|
+
let has_int = items
|
|
2489
|
+
.iter()
|
|
2490
|
+
.any(|i| matches!(i, SendableValue::Integer(42)));
|
|
2491
|
+
let has_str = items
|
|
2492
|
+
.iter()
|
|
2493
|
+
.any(|i| matches!(i, SendableValue::Str(s) if s == "hello"));
|
|
2494
|
+
let has_float = items.iter().any(|i| matches!(i, SendableValue::Float(_)));
|
|
2495
|
+
let has_bool = items.iter().any(|i| matches!(i, SendableValue::Bool(true)));
|
|
2496
|
+
assert!(has_int, "set should contain integer 42");
|
|
2497
|
+
assert!(has_str, "set should contain string 'hello'");
|
|
2498
|
+
assert!(has_float, "set should contain float 3.14");
|
|
2499
|
+
assert!(has_bool, "set should contain bool True");
|
|
2500
|
+
}
|
|
2501
|
+
other => panic!("expected Set, got {other:?}"),
|
|
2502
|
+
}
|
|
2503
|
+
api.decref(py_set);
|
|
2504
|
+
api.decref(builtins);
|
|
2505
|
+
api.decref(globals);
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
#[test]
|
|
2509
|
+
#[serial]
|
|
2510
|
+
fn test_python_to_sendable_set_with_strings() {
|
|
2511
|
+
use crate::test_helpers::skip_if_no_python;
|
|
2512
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2513
|
+
return;
|
|
2514
|
+
};
|
|
2515
|
+
let api = guard.api();
|
|
2516
|
+
let globals = api.dict_new();
|
|
2517
|
+
let builtins = api
|
|
2518
|
+
.import_module("builtins")
|
|
2519
|
+
.expect("builtins should import");
|
|
2520
|
+
let key = api.string_from_str("__builtins__");
|
|
2521
|
+
api.dict_set_item(globals, key, builtins);
|
|
2522
|
+
api.decref(key);
|
|
2523
|
+
let result = api.run_string("{'apple', 'banana', 'cherry'}", 258, globals, globals);
|
|
2524
|
+
let py_set = result.expect("string set eval should succeed");
|
|
2525
|
+
assert!(!py_set.is_null());
|
|
2526
|
+
|
|
2527
|
+
let sendable = python_to_sendable(py_set, api).expect("string set should convert");
|
|
2528
|
+
match &sendable {
|
|
2529
|
+
SendableValue::Set(items) => {
|
|
2530
|
+
assert_eq!(items.len(), 3);
|
|
2531
|
+
let mut vals: Vec<&str> = items
|
|
2532
|
+
.iter()
|
|
2533
|
+
.map(|item| match item {
|
|
2534
|
+
SendableValue::Str(s) => s.as_str(),
|
|
2535
|
+
other => panic!("expected Str, got {other:?}"),
|
|
2536
|
+
})
|
|
2537
|
+
.collect();
|
|
2538
|
+
vals.sort();
|
|
2539
|
+
assert_eq!(vals, vec!["apple", "banana", "cherry"]);
|
|
2540
|
+
}
|
|
2541
|
+
other => panic!("expected Set, got {other:?}"),
|
|
2542
|
+
}
|
|
2543
|
+
api.decref(py_set);
|
|
2544
|
+
api.decref(builtins);
|
|
2545
|
+
api.decref(globals);
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
// ========== python_to_sendable: callable ==========
|
|
2549
|
+
|
|
2550
|
+
#[test]
|
|
2551
|
+
#[serial]
|
|
2552
|
+
fn test_python_to_sendable_user_defined_function() {
|
|
2553
|
+
use crate::test_helpers::skip_if_no_python;
|
|
2554
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2555
|
+
return;
|
|
2556
|
+
};
|
|
2557
|
+
let api = guard.api();
|
|
2558
|
+
let globals = crate::eval::make_globals(api);
|
|
2559
|
+
let py_func = api.run_string(
|
|
2560
|
+
"def greet(name): return f'Hello {name}'\ngreet",
|
|
2561
|
+
257, // Py_file_input for statements
|
|
2562
|
+
globals.ptr(),
|
|
2563
|
+
globals.ptr(),
|
|
2564
|
+
);
|
|
2565
|
+
// file_input returns None; retrieve the function from globals
|
|
2566
|
+
drop(py_func);
|
|
2567
|
+
let key = api.string_from_str("greet");
|
|
2568
|
+
let func = api.dict_get_item(globals.ptr(), key);
|
|
2569
|
+
api.decref(key);
|
|
2570
|
+
assert!(!func.is_null(), "greet function should exist in globals");
|
|
2571
|
+
assert!(api.callable_check(func) != 0, "greet should be callable");
|
|
2572
|
+
|
|
2573
|
+
let sendable = python_to_sendable(func, api)
|
|
2574
|
+
.expect("user-defined function should convert via PyObjectRef");
|
|
2575
|
+
match &sendable {
|
|
2576
|
+
SendableValue::PyObjectRef(addr) => {
|
|
2577
|
+
assert_eq!(*addr, func as usize);
|
|
2578
|
+
}
|
|
2579
|
+
other => panic!("expected PyObjectRef for function, got {other:?}"),
|
|
2580
|
+
}
|
|
2581
|
+
// Clean up: decref the incref from python_to_sendable
|
|
2582
|
+
// dict_get_item returns a borrowed ref, so only the sendable's incref needs cleanup
|
|
2583
|
+
api.decref(func);
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
#[test]
|
|
2587
|
+
#[serial]
|
|
2588
|
+
fn test_python_to_sendable_lambda() {
|
|
2589
|
+
use crate::test_helpers::skip_if_no_python;
|
|
2590
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2591
|
+
return;
|
|
2592
|
+
};
|
|
2593
|
+
let api = guard.api();
|
|
2594
|
+
let globals = crate::eval::make_globals(api);
|
|
2595
|
+
let py_lambda = api
|
|
2596
|
+
.run_string("lambda x: x * 2", 258, globals.ptr(), globals.ptr())
|
|
2597
|
+
.expect("lambda eval should succeed");
|
|
2598
|
+
assert!(!py_lambda.is_null());
|
|
2599
|
+
assert!(
|
|
2600
|
+
api.callable_check(py_lambda) != 0,
|
|
2601
|
+
"lambda should be callable"
|
|
2602
|
+
);
|
|
2603
|
+
|
|
2604
|
+
let sendable =
|
|
2605
|
+
python_to_sendable(py_lambda, api).expect("lambda should convert via PyObjectRef");
|
|
2606
|
+
match &sendable {
|
|
2607
|
+
SendableValue::PyObjectRef(addr) => {
|
|
2608
|
+
assert_eq!(*addr, py_lambda as usize);
|
|
2609
|
+
}
|
|
2610
|
+
other => panic!("expected PyObjectRef for lambda, got {other:?}"),
|
|
2611
|
+
}
|
|
2612
|
+
api.decref(py_lambda); // sendable's incref
|
|
2613
|
+
api.decref(py_lambda); // run_string's ref
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
#[test]
|
|
2617
|
+
#[serial]
|
|
2618
|
+
fn test_python_to_sendable_builtin_function() {
|
|
2619
|
+
use crate::test_helpers::skip_if_no_python;
|
|
2620
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2621
|
+
return;
|
|
2622
|
+
};
|
|
2623
|
+
let api = guard.api();
|
|
2624
|
+
let builtins = api
|
|
2625
|
+
.import_module("builtins")
|
|
2626
|
+
.expect("builtins should import");
|
|
2627
|
+
let len_func = api.object_get_attr_string(builtins, "len");
|
|
2628
|
+
assert!(!len_func.is_null(), "len should be accessible");
|
|
2629
|
+
assert!(api.callable_check(len_func) != 0, "len should be callable");
|
|
2630
|
+
|
|
2631
|
+
let sendable = python_to_sendable(len_func, api)
|
|
2632
|
+
.expect("builtin function should convert via PyObjectRef");
|
|
2633
|
+
match &sendable {
|
|
2634
|
+
SendableValue::PyObjectRef(addr) => {
|
|
2635
|
+
assert_eq!(*addr, len_func as usize);
|
|
2636
|
+
}
|
|
2637
|
+
other => panic!("expected PyObjectRef for builtin function, got {other:?}"),
|
|
2638
|
+
}
|
|
2639
|
+
api.decref(len_func); // sendable's incref
|
|
2640
|
+
api.decref(len_func); // get_attr_string ref
|
|
2641
|
+
api.decref(builtins);
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
#[test]
|
|
2645
|
+
#[serial]
|
|
2646
|
+
fn test_python_to_sendable_class() {
|
|
2647
|
+
use crate::test_helpers::skip_if_no_python;
|
|
2648
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2649
|
+
return;
|
|
2650
|
+
};
|
|
2651
|
+
let api = guard.api();
|
|
2652
|
+
let globals = crate::eval::make_globals(api);
|
|
2653
|
+
// Define a class (classes are callable — calling them constructs instances)
|
|
2654
|
+
let _ = api.run_string(
|
|
2655
|
+
"class Greeter:\n def __init__(self, name):\n self.name = name",
|
|
2656
|
+
257,
|
|
2657
|
+
globals.ptr(),
|
|
2658
|
+
globals.ptr(),
|
|
2659
|
+
);
|
|
2660
|
+
let key = api.string_from_str("Greeter");
|
|
2661
|
+
let cls = api.dict_get_item(globals.ptr(), key);
|
|
2662
|
+
api.decref(key);
|
|
2663
|
+
assert!(!cls.is_null(), "Greeter class should exist in globals");
|
|
2664
|
+
assert!(api.callable_check(cls) != 0, "classes should be callable");
|
|
2665
|
+
|
|
2666
|
+
let sendable = python_to_sendable(cls, api).expect("class should convert via PyObjectRef");
|
|
2667
|
+
match &sendable {
|
|
2668
|
+
SendableValue::PyObjectRef(addr) => {
|
|
2669
|
+
assert_eq!(*addr, cls as usize);
|
|
2670
|
+
}
|
|
2671
|
+
other => panic!("expected PyObjectRef for class, got {other:?}"),
|
|
2672
|
+
}
|
|
2673
|
+
api.decref(cls); // sendable's incref
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
#[test]
|
|
2677
|
+
#[serial]
|
|
2678
|
+
fn test_python_to_sendable_instance_with_call() {
|
|
2679
|
+
use crate::test_helpers::skip_if_no_python;
|
|
2680
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2681
|
+
return;
|
|
2682
|
+
};
|
|
2683
|
+
let api = guard.api();
|
|
2684
|
+
let globals = crate::eval::make_globals(api);
|
|
2685
|
+
// Create an instance with __call__
|
|
2686
|
+
let py_obj = api
|
|
2687
|
+
.run_string(
|
|
2688
|
+
"type('Adder', (), {'__call__': lambda self, x, y: x + y})()",
|
|
2689
|
+
258,
|
|
2690
|
+
globals.ptr(),
|
|
2691
|
+
globals.ptr(),
|
|
2692
|
+
)
|
|
2693
|
+
.expect("callable instance eval should succeed");
|
|
2694
|
+
assert!(!py_obj.is_null());
|
|
2695
|
+
assert!(
|
|
2696
|
+
api.callable_check(py_obj) != 0,
|
|
2697
|
+
"instance with __call__ should be callable"
|
|
2698
|
+
);
|
|
2699
|
+
|
|
2700
|
+
let sendable = python_to_sendable(py_obj, api)
|
|
2701
|
+
.expect("callable instance should convert via PyObjectRef");
|
|
2702
|
+
match &sendable {
|
|
2703
|
+
SendableValue::PyObjectRef(addr) => {
|
|
2704
|
+
assert_eq!(*addr, py_obj as usize);
|
|
2705
|
+
}
|
|
2706
|
+
other => panic!("expected PyObjectRef for callable instance, got {other:?}"),
|
|
2707
|
+
}
|
|
2708
|
+
api.decref(py_obj); // sendable's incref
|
|
2709
|
+
api.decref(py_obj); // run_string ref
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
#[test]
|
|
2713
|
+
#[serial]
|
|
2714
|
+
fn test_python_to_sendable_callable_is_callable_check() {
|
|
2715
|
+
use crate::test_helpers::skip_if_no_python;
|
|
2716
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2717
|
+
return;
|
|
2718
|
+
};
|
|
2719
|
+
let api = guard.api();
|
|
2720
|
+
let globals = crate::eval::make_globals(api);
|
|
2721
|
+
let py_lambda = api
|
|
2722
|
+
.run_string("lambda: 42", 258, globals.ptr(), globals.ptr())
|
|
2723
|
+
.expect("lambda eval should succeed");
|
|
2724
|
+
|
|
2725
|
+
let sendable = python_to_sendable(py_lambda, api).expect("lambda should convert");
|
|
2726
|
+
match &sendable {
|
|
2727
|
+
SendableValue::PyObjectRef(addr) => {
|
|
2728
|
+
// Verify the wrapped object is still callable
|
|
2729
|
+
let ptr = *addr as *mut PyObject;
|
|
2730
|
+
assert!(
|
|
2731
|
+
api.callable_check(ptr) != 0,
|
|
2732
|
+
"wrapped callable should still report callable"
|
|
2733
|
+
);
|
|
2734
|
+
}
|
|
2735
|
+
other => panic!("expected PyObjectRef, got {other:?}"),
|
|
2736
|
+
}
|
|
2737
|
+
api.decref(py_lambda);
|
|
2738
|
+
api.decref(py_lambda);
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
#[test]
|
|
2742
|
+
#[serial]
|
|
2743
|
+
fn test_python_to_sendable_callable_method() {
|
|
2744
|
+
use crate::test_helpers::skip_if_no_python;
|
|
2745
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2746
|
+
return;
|
|
2747
|
+
};
|
|
2748
|
+
let api = guard.api();
|
|
2749
|
+
// Get a bound method: "hello".upper
|
|
2750
|
+
let globals = crate::eval::make_globals(api);
|
|
2751
|
+
let py_method = api
|
|
2752
|
+
.run_string("'hello'.upper", 258, globals.ptr(), globals.ptr())
|
|
2753
|
+
.expect("bound method eval should succeed");
|
|
2754
|
+
assert!(!py_method.is_null());
|
|
2755
|
+
assert!(
|
|
2756
|
+
api.callable_check(py_method) != 0,
|
|
2757
|
+
"bound method should be callable"
|
|
2758
|
+
);
|
|
2759
|
+
|
|
2760
|
+
let sendable = python_to_sendable(py_method, api)
|
|
2761
|
+
.expect("bound method should convert via PyObjectRef");
|
|
2762
|
+
assert!(
|
|
2763
|
+
matches!(sendable, SendableValue::PyObjectRef(_)),
|
|
2764
|
+
"bound method should be PyObjectRef"
|
|
2765
|
+
);
|
|
2766
|
+
api.decref(py_method); // sendable's incref
|
|
2767
|
+
api.decref(py_method); // run_string ref
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
#[test]
|
|
2771
|
+
#[serial]
|
|
2772
|
+
fn test_python_to_sendable_primitives_not_py_object_ref() {
|
|
2773
|
+
use crate::test_helpers::skip_if_no_python;
|
|
2774
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2775
|
+
return;
|
|
2776
|
+
};
|
|
2777
|
+
let api = guard.api();
|
|
2778
|
+
|
|
2779
|
+
// int → Integer, not PyObjectRef
|
|
2780
|
+
let py_int = api.long_from_i64(42);
|
|
2781
|
+
assert!(matches!(
|
|
2782
|
+
python_to_sendable(py_int, api),
|
|
2783
|
+
Ok(SendableValue::Integer(42))
|
|
2784
|
+
));
|
|
2785
|
+
|
|
2786
|
+
// str → Str, not PyObjectRef
|
|
2787
|
+
let py_str = api.string_from_str("hello");
|
|
2788
|
+
assert!(matches!(
|
|
2789
|
+
python_to_sendable(py_str, api),
|
|
2790
|
+
Ok(SendableValue::Str(_))
|
|
2791
|
+
));
|
|
2792
|
+
|
|
2793
|
+
// None → Nil, not PyObjectRef
|
|
2794
|
+
assert!(matches!(
|
|
2795
|
+
python_to_sendable(api.py_none, api),
|
|
2796
|
+
Ok(SendableValue::Nil)
|
|
2797
|
+
));
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
// ========== python_to_sendable: bytes/bytearray ==========
|
|
2801
|
+
|
|
2802
|
+
#[test]
|
|
2803
|
+
#[serial]
|
|
2804
|
+
fn test_python_to_sendable_bytes() {
|
|
2805
|
+
use crate::test_helpers::skip_if_no_python;
|
|
2806
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2807
|
+
return;
|
|
2808
|
+
};
|
|
2809
|
+
let api = guard.api();
|
|
2810
|
+
|
|
2811
|
+
let py_bytes = api.bytes_from_slice(b"hello");
|
|
2812
|
+
let result = python_to_sendable(py_bytes, api);
|
|
2813
|
+
assert!(
|
|
2814
|
+
matches!(&result, Ok(SendableValue::Bytes(v)) if v == b"hello"),
|
|
2815
|
+
"Python bytes should convert to SendableValue::Bytes, got: {:?}",
|
|
2816
|
+
result
|
|
2817
|
+
);
|
|
2818
|
+
api.decref(py_bytes);
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
#[test]
|
|
2822
|
+
#[serial]
|
|
2823
|
+
fn test_python_to_sendable_bytes_empty() {
|
|
2824
|
+
use crate::test_helpers::skip_if_no_python;
|
|
2825
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2826
|
+
return;
|
|
2827
|
+
};
|
|
2828
|
+
let api = guard.api();
|
|
2829
|
+
|
|
2830
|
+
let py_bytes = api.bytes_from_slice(b"");
|
|
2831
|
+
let result = python_to_sendable(py_bytes, api);
|
|
2832
|
+
assert!(
|
|
2833
|
+
matches!(&result, Ok(SendableValue::Bytes(v)) if v.is_empty()),
|
|
2834
|
+
"Empty Python bytes should convert to empty Bytes, got: {:?}",
|
|
2835
|
+
result
|
|
2836
|
+
);
|
|
2837
|
+
api.decref(py_bytes);
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
#[test]
|
|
2841
|
+
#[serial]
|
|
2842
|
+
fn test_python_to_sendable_bytes_with_nulls() {
|
|
2843
|
+
use crate::test_helpers::skip_if_no_python;
|
|
2844
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2845
|
+
return;
|
|
2846
|
+
};
|
|
2847
|
+
let api = guard.api();
|
|
2848
|
+
|
|
2849
|
+
let data: &[u8] = b"\x00\x01\xff\x00\xfe";
|
|
2850
|
+
let py_bytes = api.bytes_from_slice(data);
|
|
2851
|
+
let result = python_to_sendable(py_bytes, api);
|
|
2852
|
+
assert!(
|
|
2853
|
+
matches!(&result, Ok(SendableValue::Bytes(v)) if v.as_slice() == data),
|
|
2854
|
+
"Bytes with NULs should roundtrip, got: {:?}",
|
|
2855
|
+
result
|
|
2856
|
+
);
|
|
2857
|
+
api.decref(py_bytes);
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
#[test]
|
|
2861
|
+
#[serial]
|
|
2862
|
+
fn test_python_to_sendable_bytearray() {
|
|
2863
|
+
use crate::test_helpers::skip_if_no_python;
|
|
2864
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2865
|
+
return;
|
|
2866
|
+
};
|
|
2867
|
+
let api = guard.api();
|
|
2868
|
+
|
|
2869
|
+
let py_ba = api.bytearray_from_slice(b"world");
|
|
2870
|
+
let result = python_to_sendable(py_ba, api);
|
|
2871
|
+
assert!(
|
|
2872
|
+
matches!(&result, Ok(SendableValue::Bytes(v)) if v == b"world"),
|
|
2873
|
+
"Python bytearray should convert to SendableValue::Bytes, got: {:?}",
|
|
2874
|
+
result
|
|
2875
|
+
);
|
|
2876
|
+
api.decref(py_ba);
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
#[test]
|
|
2880
|
+
#[serial]
|
|
2881
|
+
fn test_python_to_sendable_bytearray_empty() {
|
|
2882
|
+
use crate::test_helpers::skip_if_no_python;
|
|
2883
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2884
|
+
return;
|
|
2885
|
+
};
|
|
2886
|
+
let api = guard.api();
|
|
2887
|
+
|
|
2888
|
+
let py_ba = api.bytearray_from_slice(b"");
|
|
2889
|
+
let result = python_to_sendable(py_ba, api);
|
|
2890
|
+
assert!(
|
|
2891
|
+
matches!(&result, Ok(SendableValue::Bytes(v)) if v.is_empty()),
|
|
2892
|
+
"Empty bytearray should convert to empty Bytes, got: {:?}",
|
|
2893
|
+
result
|
|
2894
|
+
);
|
|
2895
|
+
api.decref(py_ba);
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
#[test]
|
|
2899
|
+
#[serial]
|
|
2900
|
+
fn test_python_to_sendable_bytes_not_string() {
|
|
2901
|
+
use crate::test_helpers::skip_if_no_python;
|
|
2902
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2903
|
+
return;
|
|
2904
|
+
};
|
|
2905
|
+
let api = guard.api();
|
|
2906
|
+
|
|
2907
|
+
// Python bytes must not produce SendableValue::Str
|
|
2908
|
+
let py_bytes = api.bytes_from_slice(b"hello");
|
|
2909
|
+
let result = python_to_sendable(py_bytes, api);
|
|
2910
|
+
assert!(
|
|
2911
|
+
!matches!(&result, Ok(SendableValue::Str(_))),
|
|
2912
|
+
"Python bytes must not become Str"
|
|
2913
|
+
);
|
|
2914
|
+
assert!(
|
|
2915
|
+
!matches!(&result, Ok(SendableValue::PyObjectRef(_))),
|
|
2916
|
+
"Python bytes must not fall through to PyObjectRef"
|
|
2917
|
+
);
|
|
2918
|
+
api.decref(py_bytes);
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
#[test]
|
|
2922
|
+
#[serial]
|
|
2923
|
+
fn test_python_to_sendable_bytes_in_list() {
|
|
2924
|
+
use crate::test_helpers::skip_if_no_python;
|
|
2925
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2926
|
+
return;
|
|
2927
|
+
};
|
|
2928
|
+
let api = guard.api();
|
|
2929
|
+
|
|
2930
|
+
// [b"hello", 42, b"\x00\xff"]
|
|
2931
|
+
let py_list = api.list_new(3);
|
|
2932
|
+
api.list_set_item(py_list, 0, api.bytes_from_slice(b"hello"));
|
|
2933
|
+
api.list_set_item(py_list, 1, api.long_from_i64(42));
|
|
2934
|
+
api.list_set_item(py_list, 2, api.bytes_from_slice(b"\x00\xff"));
|
|
2935
|
+
|
|
2936
|
+
let result = python_to_sendable(py_list, api).expect("list with bytes should convert");
|
|
2937
|
+
if let SendableValue::List(items) = result {
|
|
2938
|
+
assert_eq!(items.len(), 3);
|
|
2939
|
+
assert!(matches!(&items[0], SendableValue::Bytes(v) if v == b"hello"));
|
|
2940
|
+
assert!(matches!(&items[1], SendableValue::Integer(42)));
|
|
2941
|
+
assert!(matches!(&items[2], SendableValue::Bytes(v) if v == b"\x00\xff"));
|
|
2942
|
+
} else {
|
|
2943
|
+
panic!("Expected List, got: {:?}", result);
|
|
2944
|
+
}
|
|
2945
|
+
api.decref(py_list);
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
#[test]
|
|
2949
|
+
#[serial]
|
|
2950
|
+
fn test_python_to_sendable_bytes_as_dict_value() {
|
|
2951
|
+
use crate::test_helpers::skip_if_no_python;
|
|
2952
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2953
|
+
return;
|
|
2954
|
+
};
|
|
2955
|
+
let api = guard.api();
|
|
2956
|
+
|
|
2957
|
+
let py_dict = api.dict_new();
|
|
2958
|
+
let key = api.string_from_str("data");
|
|
2959
|
+
let val = api.bytes_from_slice(b"\xde\xad\xbe\xef");
|
|
2960
|
+
api.dict_set_item(py_dict, key, val);
|
|
2961
|
+
api.decref(key);
|
|
2962
|
+
api.decref(val);
|
|
2963
|
+
|
|
2964
|
+
let result =
|
|
2965
|
+
python_to_sendable(py_dict, api).expect("dict with bytes value should convert");
|
|
2966
|
+
if let SendableValue::Dict(entries) = result {
|
|
2967
|
+
assert_eq!(entries.len(), 1);
|
|
2968
|
+
assert!(matches!(&entries[0].0, SendableValue::Str(s) if s == "data"));
|
|
2969
|
+
assert!(matches!(&entries[0].1, SendableValue::Bytes(v) if v == b"\xde\xad\xbe\xef"));
|
|
2970
|
+
} else {
|
|
2971
|
+
panic!("Expected Dict, got: {:?}", result);
|
|
2972
|
+
}
|
|
2973
|
+
api.decref(py_dict);
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
// ========== ruby_to_python: ASCII-8BIT String → Python bytes ==========
|
|
2977
|
+
|
|
2978
|
+
#[test]
|
|
2979
|
+
#[serial]
|
|
2980
|
+
fn test_ruby_to_python_ascii_8bit_string_becomes_bytes() {
|
|
2981
|
+
with_ruby_python(|ruby, api| {
|
|
2982
|
+
let s = ruby.str_from_slice(b"hello");
|
|
2983
|
+
s.enc_associate(
|
|
2984
|
+
ruby.find_encoding("ASCII-8BIT")
|
|
2985
|
+
.expect("ASCII-8BIT must exist"),
|
|
2986
|
+
)
|
|
2987
|
+
.unwrap();
|
|
2988
|
+
|
|
2989
|
+
let py_obj = ruby_to_python(s.as_value(), api)
|
|
2990
|
+
.expect("ASCII-8BIT string conversion should succeed");
|
|
2991
|
+
|
|
2992
|
+
assert!(api.bytes_check(py_obj), "Should produce Python bytes");
|
|
2993
|
+
assert!(!api.is_string(py_obj), "Should NOT produce Python str");
|
|
2994
|
+
|
|
2995
|
+
let back = api.bytes_to_vec(py_obj).unwrap();
|
|
2996
|
+
assert_eq!(back, b"hello");
|
|
2997
|
+
api.decref(py_obj);
|
|
2998
|
+
});
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
#[test]
|
|
3002
|
+
#[serial]
|
|
3003
|
+
fn test_ruby_to_python_ascii_8bit_empty() {
|
|
3004
|
+
with_ruby_python(|ruby, api| {
|
|
3005
|
+
let s = ruby.str_from_slice(b"");
|
|
3006
|
+
s.enc_associate(
|
|
3007
|
+
ruby.find_encoding("ASCII-8BIT")
|
|
3008
|
+
.expect("ASCII-8BIT must exist"),
|
|
3009
|
+
)
|
|
3010
|
+
.unwrap();
|
|
3011
|
+
|
|
3012
|
+
let py_obj =
|
|
3013
|
+
ruby_to_python(s.as_value(), api).expect("empty ASCII-8BIT should convert");
|
|
3014
|
+
|
|
3015
|
+
assert!(api.bytes_check(py_obj));
|
|
3016
|
+
let back = api.bytes_to_vec(py_obj).unwrap();
|
|
3017
|
+
assert!(back.is_empty());
|
|
3018
|
+
api.decref(py_obj);
|
|
3019
|
+
});
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
#[test]
|
|
3023
|
+
#[serial]
|
|
3024
|
+
fn test_ruby_to_python_ascii_8bit_with_nulls() {
|
|
3025
|
+
with_ruby_python(|ruby, api| {
|
|
3026
|
+
let data: &[u8] = b"\x00\x01\x02\xff\xfe\x00";
|
|
3027
|
+
let s = ruby.str_from_slice(data);
|
|
3028
|
+
s.enc_associate(
|
|
3029
|
+
ruby.find_encoding("ASCII-8BIT")
|
|
3030
|
+
.expect("ASCII-8BIT must exist"),
|
|
3031
|
+
)
|
|
3032
|
+
.unwrap();
|
|
3033
|
+
|
|
3034
|
+
let py_obj =
|
|
3035
|
+
ruby_to_python(s.as_value(), api).expect("binary data with NULs should convert");
|
|
3036
|
+
|
|
3037
|
+
assert!(api.bytes_check(py_obj));
|
|
3038
|
+
let back = api.bytes_to_vec(py_obj).unwrap();
|
|
3039
|
+
assert_eq!(back, data);
|
|
3040
|
+
api.decref(py_obj);
|
|
3041
|
+
});
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
#[test]
|
|
3045
|
+
#[serial]
|
|
3046
|
+
fn test_ruby_to_python_utf8_string_stays_str() {
|
|
3047
|
+
with_ruby_python(|ruby, api| {
|
|
3048
|
+
// Regular UTF-8 string must still become Python str, not bytes
|
|
3049
|
+
let py_str = ruby_to_python("hello".into_value_with(ruby), api)
|
|
3050
|
+
.expect("UTF-8 string should convert");
|
|
3051
|
+
|
|
3052
|
+
assert!(api.is_string(py_str), "UTF-8 should produce Python str");
|
|
3053
|
+
assert!(!api.bytes_check(py_str), "UTF-8 should NOT produce bytes");
|
|
3054
|
+
assert_eq!(api.string_to_string(py_str), Some("hello".to_string()));
|
|
3055
|
+
api.decref(py_str);
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
#[test]
|
|
3060
|
+
#[serial]
|
|
3061
|
+
fn test_ruby_to_python_ascii_8bit_roundtrip_via_sendable() {
|
|
3062
|
+
with_ruby_python(|ruby, api| {
|
|
3063
|
+
// Ruby ASCII-8BIT String → Python bytes → SendableValue::Bytes
|
|
3064
|
+
let data: &[u8] = b"\xca\xfe\xba\xbe";
|
|
3065
|
+
let s = ruby.str_from_slice(data);
|
|
3066
|
+
s.enc_associate(
|
|
3067
|
+
ruby.find_encoding("ASCII-8BIT")
|
|
3068
|
+
.expect("ASCII-8BIT must exist"),
|
|
3069
|
+
)
|
|
3070
|
+
.unwrap();
|
|
3071
|
+
|
|
3072
|
+
// Ruby → Python
|
|
3073
|
+
let py_obj = ruby_to_python(s.as_value(), api).expect("should convert to Python");
|
|
3074
|
+
assert!(api.bytes_check(py_obj));
|
|
3075
|
+
|
|
3076
|
+
// Python → SendableValue
|
|
3077
|
+
let sendable = python_to_sendable(py_obj, api).expect("should convert to sendable");
|
|
3078
|
+
assert!(
|
|
3079
|
+
matches!(&sendable, SendableValue::Bytes(v) if v == data),
|
|
3080
|
+
"Full roundtrip should preserve bytes, got: {:?}",
|
|
3081
|
+
sendable
|
|
3082
|
+
);
|
|
3083
|
+
api.decref(py_obj);
|
|
3084
|
+
});
|
|
3085
|
+
}
|
|
1931
3086
|
}
|