rubyx-py 0.2.0 → 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 +61 -6
- data/ext/rubyx/src/lib.rs +1 -0
- data/ext/rubyx/src/python_api.rs +1027 -0
- data/ext/rubyx/src/rubyx_object.rs +1075 -8
- data/ext/rubyx/src/rubyx_stream.rs +168 -0
- data/ext/rubyx/src/stream.rs +148 -2
- data/lib/rubyx/railtie.rb +2 -1
- data/lib/rubyx/tasks/rubyx.rake +127 -0
- data/lib/rubyx/version.rb +2 -2
- metadata +2 -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);
|
|
@@ -95,7 +140,7 @@ pub(crate) fn python_to_sendable(
|
|
|
95
140
|
let name = std::ffi::CString::new("__dict__").unwrap();
|
|
96
141
|
api.object_has_attr_string(py_val, name.as_ptr()) != 0
|
|
97
142
|
};
|
|
98
|
-
if api.callable_check(py_val)
|
|
143
|
+
if has_dict || api.callable_check(py_val) != 0 {
|
|
99
144
|
api.incref(py_val);
|
|
100
145
|
return Ok(SendableValue::PyObjectRef(py_val as usize));
|
|
101
146
|
}
|
|
@@ -153,6 +198,24 @@ pub(crate) fn ruby_to_python(
|
|
|
153
198
|
return Ok(py_str);
|
|
154
199
|
}
|
|
155
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
|
|
156
219
|
let val = String::try_convert(value)?;
|
|
157
220
|
return val
|
|
158
221
|
.to_python(api)
|
|
@@ -762,6 +825,134 @@ impl RubyxObject {
|
|
|
762
825
|
result
|
|
763
826
|
}
|
|
764
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
|
+
|
|
765
956
|
pub fn py_type(&self) -> Result<String, magnus::Error> {
|
|
766
957
|
let gil = self.api.ensure_gil();
|
|
767
958
|
let result = self.api.type_name(self.py_obj);
|
|
@@ -1668,6 +1859,206 @@ mod tests {
|
|
|
1668
1859
|
});
|
|
1669
1860
|
}
|
|
1670
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
|
+
|
|
1671
2062
|
// ========== is_truthy / is_falsy tests ==========
|
|
1672
2063
|
|
|
1673
2064
|
#[test]
|
|
@@ -1960,15 +2351,16 @@ mod tests {
|
|
|
1960
2351
|
api.decref(os);
|
|
1961
2352
|
}
|
|
1962
2353
|
|
|
2354
|
+
// ========== python_to_sendable: set / frozenset ==========
|
|
2355
|
+
|
|
1963
2356
|
#[test]
|
|
1964
2357
|
#[serial]
|
|
1965
|
-
fn
|
|
2358
|
+
fn test_python_to_sendable_set_returns_set() {
|
|
1966
2359
|
use crate::test_helpers::skip_if_no_python;
|
|
1967
2360
|
let Some(guard) = skip_if_no_python() else {
|
|
1968
2361
|
return;
|
|
1969
2362
|
};
|
|
1970
2363
|
let api = guard.api();
|
|
1971
|
-
// Sets don't have __dict__, so they return Err (not PyObjectRef)
|
|
1972
2364
|
let globals = api.dict_new();
|
|
1973
2365
|
let builtins = api
|
|
1974
2366
|
.import_module("builtins")
|
|
@@ -1980,8 +2372,22 @@ mod tests {
|
|
|
1980
2372
|
let py_set = result.expect("set eval should succeed");
|
|
1981
2373
|
assert!(!py_set.is_null());
|
|
1982
2374
|
|
|
1983
|
-
let sendable = python_to_sendable(py_set, api);
|
|
1984
|
-
|
|
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
|
+
}
|
|
1985
2391
|
api.decref(py_set);
|
|
1986
2392
|
api.decref(builtins);
|
|
1987
2393
|
api.decref(globals);
|
|
@@ -1989,13 +2395,387 @@ mod tests {
|
|
|
1989
2395
|
|
|
1990
2396
|
#[test]
|
|
1991
2397
|
#[serial]
|
|
1992
|
-
fn
|
|
2398
|
+
fn test_python_to_sendable_frozenset_returns_set() {
|
|
1993
2399
|
use crate::test_helpers::skip_if_no_python;
|
|
1994
2400
|
let Some(guard) = skip_if_no_python() else {
|
|
1995
2401
|
return;
|
|
1996
2402
|
};
|
|
1997
2403
|
let api = guard.api();
|
|
1998
|
-
|
|
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
|
+
|
|
1999
2779
|
// int → Integer, not PyObjectRef
|
|
2000
2780
|
let py_int = api.long_from_i64(42);
|
|
2001
2781
|
assert!(matches!(
|
|
@@ -2016,4 +2796,291 @@ mod tests {
|
|
|
2016
2796
|
Ok(SendableValue::Nil)
|
|
2017
2797
|
));
|
|
2018
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
|
+
}
|
|
2019
3086
|
}
|