rubyx-py 0.1.0
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 +7 -0
- data/Cargo.toml +19 -0
- data/README.md +469 -0
- data/ext/rubyx/Cargo.toml +19 -0
- data/ext/rubyx/extconf.rb +22 -0
- data/ext/rubyx/src/async_gen.rs +1298 -0
- data/ext/rubyx/src/context.rs +812 -0
- data/ext/rubyx/src/convert.rs +1498 -0
- data/ext/rubyx/src/eval.rs +377 -0
- data/ext/rubyx/src/exception.rs +184 -0
- data/ext/rubyx/src/future.rs +126 -0
- data/ext/rubyx/src/import.rs +34 -0
- data/ext/rubyx/src/lib.rs +4212 -0
- data/ext/rubyx/src/nonblocking_stream.rs +1422 -0
- data/ext/rubyx/src/pipe_notify.rs +232 -0
- data/ext/rubyx/src/python/sync_adapter.py +31 -0
- data/ext/rubyx/src/python_api.rs +6029 -0
- data/ext/rubyx/src/python_ffi.rs +18 -0
- data/ext/rubyx/src/python_finder.rs +119 -0
- data/ext/rubyx/src/python_guard.rs +25 -0
- data/ext/rubyx/src/ruby_helpers.rs +74 -0
- data/ext/rubyx/src/rubyx_object.rs +1931 -0
- data/ext/rubyx/src/rubyx_stream.rs +950 -0
- data/ext/rubyx/src/stream.rs +713 -0
- data/ext/rubyx/src/test_helpers.rs +351 -0
- data/lib/generators/rubyx/install_generator.rb +24 -0
- data/lib/generators/rubyx/templates/rubyx_initializer.rb +17 -0
- data/lib/rubyx/context.rb +27 -0
- data/lib/rubyx/error.rb +30 -0
- data/lib/rubyx/rails.rb +105 -0
- data/lib/rubyx/railtie.rb +20 -0
- data/lib/rubyx/uv.rb +261 -0
- data/lib/rubyx/version.rb +4 -0
- data/lib/rubyx-py.rb +1 -0
- data/lib/rubyx.rb +136 -0
- metadata +123 -0
|
@@ -0,0 +1,1931 @@
|
|
|
1
|
+
use crate::convert::ToPython;
|
|
2
|
+
use crate::python_api::PythonApi;
|
|
3
|
+
use crate::python_ffi::PyObject;
|
|
4
|
+
use crate::python_guard::PyGuard;
|
|
5
|
+
use crate::ruby_helpers;
|
|
6
|
+
use crate::stream::SendableValue;
|
|
7
|
+
use magnus::r_hash::ForEach;
|
|
8
|
+
use magnus::typed_data::Obj;
|
|
9
|
+
use magnus::value::ReprValue;
|
|
10
|
+
use magnus::{Class, IntoValue, RHash, Ruby, Symbol, TryConvert, Value};
|
|
11
|
+
use std::ffi::CString;
|
|
12
|
+
|
|
13
|
+
const RUBY_IMPLICIT_CONVERSIONS: &[&str] = &[
|
|
14
|
+
"to_ary",
|
|
15
|
+
"to_str",
|
|
16
|
+
"to_hash",
|
|
17
|
+
"to_int",
|
|
18
|
+
"to_float",
|
|
19
|
+
"to_io",
|
|
20
|
+
"to_proc",
|
|
21
|
+
"to_path",
|
|
22
|
+
"to_regexp",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
pub(crate) fn python_to_sendable(
|
|
26
|
+
py_val: *mut PyObject,
|
|
27
|
+
api: &PythonApi,
|
|
28
|
+
) -> Result<SendableValue, String> {
|
|
29
|
+
// Nil
|
|
30
|
+
if py_val == api.py_none {
|
|
31
|
+
return Ok(SendableValue::Nil);
|
|
32
|
+
}
|
|
33
|
+
// Bool must be checked before long, because Python bool is a subclass of int
|
|
34
|
+
if api.is_bool(py_val) {
|
|
35
|
+
return Ok(SendableValue::Bool(py_val == api.py_true));
|
|
36
|
+
}
|
|
37
|
+
if api.is_long(py_val) {
|
|
38
|
+
let val = api.long_to_i64(py_val);
|
|
39
|
+
return Ok(SendableValue::Integer(val));
|
|
40
|
+
}
|
|
41
|
+
if api.is_float(py_val) {
|
|
42
|
+
let val = api.float_to_f64(py_val);
|
|
43
|
+
return Ok(SendableValue::Float(val));
|
|
44
|
+
}
|
|
45
|
+
if api.is_string(py_val) {
|
|
46
|
+
let Some(val) = api.string_to_string(py_val) else {
|
|
47
|
+
if api.has_error() {
|
|
48
|
+
api.clear_error();
|
|
49
|
+
}
|
|
50
|
+
return Err("Cannot decode Python string as UTF-8".to_string());
|
|
51
|
+
};
|
|
52
|
+
return Ok(SendableValue::Str(val));
|
|
53
|
+
}
|
|
54
|
+
if api.tuple_check(py_val) {
|
|
55
|
+
let len = api.tuple_size(py_val);
|
|
56
|
+
let mut items = Vec::with_capacity(len as usize);
|
|
57
|
+
for i in 0..len {
|
|
58
|
+
let item = api.tuple_get_item(py_val, i);
|
|
59
|
+
items.push(python_to_sendable(item, api)?);
|
|
60
|
+
}
|
|
61
|
+
return Ok(SendableValue::List(items));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if api.list_check(py_val) {
|
|
65
|
+
let len = api.list_size(py_val);
|
|
66
|
+
let mut items = Vec::with_capacity(len as usize);
|
|
67
|
+
for i in 0..len {
|
|
68
|
+
let item = api.list_get_item(py_val, i);
|
|
69
|
+
items.push(python_to_sendable(item, api)?);
|
|
70
|
+
}
|
|
71
|
+
return Ok(SendableValue::List(items));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if api.dict_check(py_val) {
|
|
75
|
+
let len = api.dict_size(py_val);
|
|
76
|
+
let mut items = Vec::with_capacity(len);
|
|
77
|
+
let mut start = 0;
|
|
78
|
+
let mut key = std::ptr::null_mut();
|
|
79
|
+
let mut value = std::ptr::null_mut();
|
|
80
|
+
while api.dict_next(py_val, &mut start, &mut key, &mut value) {
|
|
81
|
+
let send_key = python_to_sendable(key, api)?;
|
|
82
|
+
let send_value = python_to_sendable(value, api)?;
|
|
83
|
+
items.push((send_key, send_value));
|
|
84
|
+
}
|
|
85
|
+
return Ok(SendableValue::Dict(items));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if py_val == api.py_true {
|
|
89
|
+
return Ok(SendableValue::Bool(true));
|
|
90
|
+
}
|
|
91
|
+
if py_val == api.py_false {
|
|
92
|
+
return Ok(SendableValue::Bool(false));
|
|
93
|
+
}
|
|
94
|
+
Err("Cannot convert Python value to Ruby".to_string())
|
|
95
|
+
}
|
|
96
|
+
pub(crate) fn ruby_to_python(
|
|
97
|
+
value: Value,
|
|
98
|
+
api: &PythonApi,
|
|
99
|
+
) -> Result<*mut PyObject, magnus::Error> {
|
|
100
|
+
let ruby = Ruby::get().map_err(|e| {
|
|
101
|
+
magnus::Error::new(
|
|
102
|
+
ruby_helpers::runtime_error(),
|
|
103
|
+
format!("Ruby VM handle unavailable: {e}"),
|
|
104
|
+
)
|
|
105
|
+
})?;
|
|
106
|
+
if value.is_nil() {
|
|
107
|
+
api.incref(api.py_none);
|
|
108
|
+
return Ok(api.py_none);
|
|
109
|
+
}
|
|
110
|
+
if value.is_kind_of(ruby.class_true_class()) {
|
|
111
|
+
api.incref(api.py_true);
|
|
112
|
+
return Ok(api.py_true);
|
|
113
|
+
}
|
|
114
|
+
if value.is_kind_of(ruby.class_false_class()) {
|
|
115
|
+
api.incref(api.py_false);
|
|
116
|
+
return Ok(api.py_false);
|
|
117
|
+
}
|
|
118
|
+
if value.is_kind_of(ruby.class_integer()) {
|
|
119
|
+
let val = i64::try_convert(value)?;
|
|
120
|
+
return val
|
|
121
|
+
.to_python(api)
|
|
122
|
+
.map_err(|e| magnus::Error::new(ruby_helpers::runtime_error(), e.to_string()));
|
|
123
|
+
}
|
|
124
|
+
if value.is_kind_of(ruby.class_float()) {
|
|
125
|
+
let val = f64::try_convert(value)?;
|
|
126
|
+
return val
|
|
127
|
+
.to_python(api)
|
|
128
|
+
.map_err(|e| magnus::Error::new(ruby_helpers::runtime_error(), e.to_string()));
|
|
129
|
+
}
|
|
130
|
+
if value.is_kind_of(ruby.class_symbol()) {
|
|
131
|
+
let sym = Symbol::try_convert(value)?;
|
|
132
|
+
let name = sym.name().map_err(|e| {
|
|
133
|
+
magnus::Error::new(
|
|
134
|
+
ruby_helpers::runtime_error(),
|
|
135
|
+
format!("Symbol name error: {e}"),
|
|
136
|
+
)
|
|
137
|
+
})?;
|
|
138
|
+
let py_str = api.string_from_str(name.as_ref());
|
|
139
|
+
if py_str.is_null() {
|
|
140
|
+
return Err(magnus::Error::new(
|
|
141
|
+
ruby_helpers::runtime_error(),
|
|
142
|
+
"Failed to create Python string from Symbol",
|
|
143
|
+
));
|
|
144
|
+
}
|
|
145
|
+
return Ok(py_str);
|
|
146
|
+
}
|
|
147
|
+
if value.is_kind_of(ruby.class_string()) {
|
|
148
|
+
let val = String::try_convert(value)?;
|
|
149
|
+
return val
|
|
150
|
+
.to_python(api)
|
|
151
|
+
.map_err(|e| magnus::Error::new(ruby_helpers::runtime_error(), e.to_string()));
|
|
152
|
+
}
|
|
153
|
+
if value.is_kind_of(ruby.class_array()) {
|
|
154
|
+
let arr = magnus::RArray::try_convert(value)?;
|
|
155
|
+
let len = arr.len();
|
|
156
|
+
let py_list = api.list_new(len as isize);
|
|
157
|
+
if py_list.is_null() {
|
|
158
|
+
return Err(magnus::Error::new(
|
|
159
|
+
ruby_helpers::runtime_error(),
|
|
160
|
+
"Failed to create Python list",
|
|
161
|
+
));
|
|
162
|
+
}
|
|
163
|
+
for (i, item) in arr.into_iter().enumerate() {
|
|
164
|
+
let py_item = ruby_to_python(item, api).inspect_err(|_e| {
|
|
165
|
+
api.decref(py_list);
|
|
166
|
+
})?;
|
|
167
|
+
let result = api.list_set_item(py_list, i as isize, py_item);
|
|
168
|
+
if result != 0 {
|
|
169
|
+
api.decref(py_item);
|
|
170
|
+
api.decref(py_list);
|
|
171
|
+
return Err(magnus::Error::new(
|
|
172
|
+
ruby_helpers::runtime_error(),
|
|
173
|
+
"Failed to set Python list item",
|
|
174
|
+
));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return Ok(py_list);
|
|
178
|
+
}
|
|
179
|
+
if value.is_kind_of(ruby.class_hash()) {
|
|
180
|
+
let hash = RHash::try_convert(value)?;
|
|
181
|
+
let dict = api.dict_new();
|
|
182
|
+
if dict.is_null() {
|
|
183
|
+
return Err(magnus::Error::new(
|
|
184
|
+
ruby_helpers::runtime_error(),
|
|
185
|
+
"Failed to create Python dict",
|
|
186
|
+
));
|
|
187
|
+
}
|
|
188
|
+
let mut err: Option<magnus::Error> = None;
|
|
189
|
+
hash.foreach(|k: Value, v: Value| {
|
|
190
|
+
let py_key = match ruby_to_python(k, api) {
|
|
191
|
+
Ok(k) => k,
|
|
192
|
+
Err(e) => {
|
|
193
|
+
err = Some(e);
|
|
194
|
+
return Ok(ForEach::Stop);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
let py_val = match ruby_to_python(v, api) {
|
|
198
|
+
Ok(v) => v,
|
|
199
|
+
Err(e) => {
|
|
200
|
+
api.decref(py_key);
|
|
201
|
+
err = Some(e);
|
|
202
|
+
return Ok(ForEach::Stop);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
let result = api.dict_set_item(dict, py_key, py_val);
|
|
206
|
+
api.decref(py_key);
|
|
207
|
+
api.decref(py_val);
|
|
208
|
+
if result == -1 {
|
|
209
|
+
err = Some(magnus::Error::new(
|
|
210
|
+
ruby_helpers::runtime_error(),
|
|
211
|
+
"Failed to set Python dict item",
|
|
212
|
+
));
|
|
213
|
+
return Ok(ForEach::Stop);
|
|
214
|
+
}
|
|
215
|
+
Ok(ForEach::Continue)
|
|
216
|
+
})?;
|
|
217
|
+
if let Some(e) = err {
|
|
218
|
+
api.decref(dict);
|
|
219
|
+
return Err(e);
|
|
220
|
+
}
|
|
221
|
+
return Ok(dict);
|
|
222
|
+
}
|
|
223
|
+
// Already wrapped Python object
|
|
224
|
+
if let Ok(obj) = Obj::<RubyxObject>::try_convert(value) {
|
|
225
|
+
api.incref(obj.as_ptr());
|
|
226
|
+
return Ok(obj.as_ptr());
|
|
227
|
+
}
|
|
228
|
+
Err(magnus::Error::new(
|
|
229
|
+
ruby_helpers::type_error(),
|
|
230
|
+
format!("Cannot convert {} to Python object", unsafe {
|
|
231
|
+
value.class().name()
|
|
232
|
+
}),
|
|
233
|
+
))
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/// A Ruby object that wraps a Python object.
|
|
237
|
+
/// Handles cross-language GC coordination.
|
|
238
|
+
#[magnus::wrap(class = "RubyxObject", mark, free_immediately, size)]
|
|
239
|
+
pub struct RubyxObject {
|
|
240
|
+
py_obj: *mut PyObject,
|
|
241
|
+
api: &'static PythonApi,
|
|
242
|
+
}
|
|
243
|
+
unsafe impl Send for RubyxObject {}
|
|
244
|
+
unsafe impl Sync for RubyxObject {}
|
|
245
|
+
impl RubyxObject {
|
|
246
|
+
/// Create a new wrapper, incrementing the Python object's reference count.
|
|
247
|
+
pub fn new(py_obj: *mut PyObject, api: &'static PythonApi) -> Option<Self> {
|
|
248
|
+
if py_obj.is_null() {
|
|
249
|
+
return None;
|
|
250
|
+
}
|
|
251
|
+
if !api.is_initialized() {
|
|
252
|
+
return None;
|
|
253
|
+
}
|
|
254
|
+
// ensure_gil is reentrant — safe even if caller already holds GIL
|
|
255
|
+
let gil = api.ensure_gil();
|
|
256
|
+
// Increase refcount
|
|
257
|
+
api.incref(py_obj);
|
|
258
|
+
api.release_gil(gil);
|
|
259
|
+
Some(RubyxObject { py_obj, api })
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
pub fn as_ptr(&self) -> *mut PyObject {
|
|
263
|
+
self.py_obj
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/// This method provides a dynamic dispatch mechanism to resolve and call methods on Python objects
|
|
267
|
+
/// in a Ruby environment using the `magnus` bridge and internal Python C API bindings.
|
|
268
|
+
///
|
|
269
|
+
/// The `method_missing` function is the Ruby equivalent of handling undefined method calls (e.g., `obj.foo`)
|
|
270
|
+
/// on a Ruby object, but it utilizes Python interop to dynamically retrieve, set, or invoke Python attributes
|
|
271
|
+
/// and methods, depending on the method call's context.
|
|
272
|
+
///
|
|
273
|
+
/// # Arguments
|
|
274
|
+
/// - `&self`: Reference to the current object which interacts with a Python object.
|
|
275
|
+
/// - `args`: A slice of `magnus::Value` that represents Ruby arguments. This typically includes:
|
|
276
|
+
/// * The name of the method being called as a Symbol/String.
|
|
277
|
+
/// * Any additional arguments for a method call or value in the case of setters.
|
|
278
|
+
/// # Returns
|
|
279
|
+
/// - `Result<magnus::Value, magnus::Error>`:
|
|
280
|
+
/// * On success, returns a `magnus::Value` object that represents the result of the Python interaction,
|
|
281
|
+
/// whether it's an attribute access, setter operation, or method call.
|
|
282
|
+
/// * On failure, returns a `magnus::Error` containing details about the failure reason.
|
|
283
|
+
///
|
|
284
|
+
/// # Error Handling
|
|
285
|
+
/// - Raises `magnus::Error` for invalid invocation patterns:
|
|
286
|
+
/// * If `args` is empty.
|
|
287
|
+
/// * If the method name is not a valid String or Symbol.
|
|
288
|
+
/// * If the method attempts a setter operation with an incorrect number of arguments.
|
|
289
|
+
/// - Handles Ruby and Python exceptions during API interop by translating them into appropriate `magnus::Error`s.
|
|
290
|
+
/// # Examples
|
|
291
|
+
/// ```ruby
|
|
292
|
+
/// obj.foo # Triggers a Python attribute getter
|
|
293
|
+
/// obj.foo(1, 2) # Triggers a Python method call with positional arguments
|
|
294
|
+
/// obj.foo = value # Triggers a Python attribute setter
|
|
295
|
+
/// ```
|
|
296
|
+
///
|
|
297
|
+
/// ## Ruby Code to `args` Slice Mapping
|
|
298
|
+
///
|
|
299
|
+
/// The `args` parameter is a flat slice where `args[0]` is always the method name
|
|
300
|
+
/// (Symbol or String), and the remaining elements are the call arguments. Ruby's
|
|
301
|
+
/// `method_missing(*args)` (declared with arity `-1` in Magnus) packs everything
|
|
302
|
+
/// into this single slice.
|
|
303
|
+
///
|
|
304
|
+
/// | Ruby Code | `args` Slice | Dispatch Path |
|
|
305
|
+
/// |----------------------------------|-----------------------------------------------------|-------------------|
|
|
306
|
+
/// | `obj.foo` | `[:foo]` | Getter |
|
|
307
|
+
/// | `obj.foo = 42` | `[:"foo=", 42]` | Setter |
|
|
308
|
+
/// | `obj.foo(1, 2)` | `[:foo, 1, 2]` | Call (positional) |
|
|
309
|
+
/// | `obj.foo(a, k: v)` | `[:foo, a, {k: v}]` | Call (pos + kwargs)|
|
|
310
|
+
/// | `obj.dumps(data, indent: 2)` | `[:dumps, data, {indent: 2}]` | Call (pos + kwargs)|
|
|
311
|
+
///
|
|
312
|
+
/// ### Getter (`args.len() == 1`, no `=` suffix)
|
|
313
|
+
/// ```ruby
|
|
314
|
+
/// obj.foo # args = [:foo]
|
|
315
|
+
/// ```
|
|
316
|
+
/// Resolves via `PyObject_GetAttrString`. If the attribute is non-callable, it is
|
|
317
|
+
/// returned directly as a wrapped `RubyxObject`.
|
|
318
|
+
///
|
|
319
|
+
/// ### Setter (`args[0]` ends with `=`, `args.len() == 2`)
|
|
320
|
+
/// ```ruby
|
|
321
|
+
/// obj.foo = value # args = [:"foo=", value]
|
|
322
|
+
/// ```
|
|
323
|
+
/// The trailing `=` is stripped to get the attribute name, then
|
|
324
|
+
/// `PyObject_SetAttrString` is called with the converted Python value.
|
|
325
|
+
///
|
|
326
|
+
/// ### Callable (`args.len() > 1`, or attribute is callable)
|
|
327
|
+
/// ```ruby
|
|
328
|
+
/// obj.foo(1, 2) # args = [:foo, 1, 2] → positional only
|
|
329
|
+
/// obj.foo(1, key: "val") # args = [:foo, 1, {key: "val"}] → positional + kwargs
|
|
330
|
+
/// ```
|
|
331
|
+
/// Positional arguments are `args[1..]` (excluding a trailing Hash). If the last
|
|
332
|
+
/// element in `args[1..]` is a Ruby `Hash`, it is split off and converted to a
|
|
333
|
+
/// Python kwargs dict. A Python tuple is built from the positional arguments, and
|
|
334
|
+
/// the call is dispatched via `PyObject_Call(callable, args_tuple, kwargs_dict)`.
|
|
335
|
+
///
|
|
336
|
+
/// # Limitations
|
|
337
|
+
/// - Currently restricted to single inheritance where the missing Ruby method maps directly to a single Python
|
|
338
|
+
/// object interaction.
|
|
339
|
+
/// - Keyword arguments (kwargs) are only supported if the last Ruby argument is a hash that can be converted to a Python dict.
|
|
340
|
+
pub fn method_missing(&self, args: &[magnus::Value]) -> Result<magnus::Value, magnus::Error> {
|
|
341
|
+
let api = self.api;
|
|
342
|
+
let gil = api.ensure_gil();
|
|
343
|
+
|
|
344
|
+
// Get python attribute if exist
|
|
345
|
+
let result = (|| -> Result<Value, magnus::Error> {
|
|
346
|
+
if args.is_empty() {
|
|
347
|
+
return Err(magnus::Error::new(
|
|
348
|
+
ruby_helpers::arg_error(),
|
|
349
|
+
"No method name given",
|
|
350
|
+
));
|
|
351
|
+
}
|
|
352
|
+
let ruby = Ruby::get().map_err(|e| {
|
|
353
|
+
magnus::Error::new(
|
|
354
|
+
ruby_helpers::runtime_error(),
|
|
355
|
+
format!("Ruby VM handle unavailable: {e}"),
|
|
356
|
+
)
|
|
357
|
+
})?;
|
|
358
|
+
let method_name = if let Ok(s) = String::try_convert(args[0]) {
|
|
359
|
+
s
|
|
360
|
+
} else if let Ok(sym) = Symbol::try_convert(args[0]) {
|
|
361
|
+
sym.name()?.to_string()
|
|
362
|
+
} else {
|
|
363
|
+
return Err(magnus::Error::new(
|
|
364
|
+
ruby_helpers::type_error(),
|
|
365
|
+
"method_missing expects Symbol/String method name",
|
|
366
|
+
));
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
if RUBY_IMPLICIT_CONVERSIONS.contains(&method_name.as_str()) {
|
|
370
|
+
return Err(magnus::Error::new(
|
|
371
|
+
ruby_helpers::no_method_error(),
|
|
372
|
+
format!("undefined method '{}' for RubyxObject", method_name),
|
|
373
|
+
));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Setter - `obj.foo = value`
|
|
377
|
+
if method_name.ends_with("=") {
|
|
378
|
+
if args.len() != 2 {
|
|
379
|
+
return Err(magnus::Error::new(
|
|
380
|
+
ruby_helpers::arg_error(),
|
|
381
|
+
"Setter required exactly one value",
|
|
382
|
+
));
|
|
383
|
+
}
|
|
384
|
+
let attr_name = &method_name[..method_name.len() - 1];
|
|
385
|
+
let py_value = ruby_to_python(args[1], api)?;
|
|
386
|
+
let rc = api.object_set_attr_string(self.py_obj, attr_name, py_value);
|
|
387
|
+
api.decref(py_value); // set_attr_string does not steal reference
|
|
388
|
+
if rc != 0 {
|
|
389
|
+
if let Some(py_err) = PythonApi::extract_exception(api) {
|
|
390
|
+
return Err(magnus::Error::from(py_err));
|
|
391
|
+
}
|
|
392
|
+
return Err(magnus::Error::new(
|
|
393
|
+
ruby_helpers::runtime_error(),
|
|
394
|
+
"Failed to set Python attribute",
|
|
395
|
+
));
|
|
396
|
+
}
|
|
397
|
+
return Ok(args[1]);
|
|
398
|
+
}
|
|
399
|
+
// Getter - `obj.foo`
|
|
400
|
+
let python_attr = api.object_get_attr_string(self.py_obj, &method_name);
|
|
401
|
+
if python_attr.is_null() {
|
|
402
|
+
api.clear_error();
|
|
403
|
+
return Err(magnus::Error::new(
|
|
404
|
+
ruby_helpers::exception(),
|
|
405
|
+
format!("undefined method `{method_name}` for a Python object"),
|
|
406
|
+
));
|
|
407
|
+
}
|
|
408
|
+
let py_attr_guard = PyGuard::new(python_attr, api).ok_or_else(|| {
|
|
409
|
+
magnus::Error::new(ruby_helpers::runtime_error(), "Null Python attribute")
|
|
410
|
+
})?;
|
|
411
|
+
|
|
412
|
+
// Attribute read path (non-callable + no args) - `obj.foo`
|
|
413
|
+
if api.callable_check(py_attr_guard.ptr()) == 0 && args.len() == 1 {
|
|
414
|
+
let wrapper = RubyxObject::new(py_attr_guard.ptr(), api).ok_or_else(|| {
|
|
415
|
+
magnus::Error::new(
|
|
416
|
+
ruby_helpers::runtime_error(),
|
|
417
|
+
"Failed to wrap Python attribute",
|
|
418
|
+
)
|
|
419
|
+
})?;
|
|
420
|
+
return Ok(wrapper.into_value_with(&ruby));
|
|
421
|
+
}
|
|
422
|
+
// Call path - `obj.foo(args)`
|
|
423
|
+
let call_args = &args[1..];
|
|
424
|
+
|
|
425
|
+
// Optional kwargs: last arg hash
|
|
426
|
+
let (positional, kwargs) = if let Some(last) = call_args.last() {
|
|
427
|
+
if last.is_kind_of(ruby.class_hash()) {
|
|
428
|
+
(
|
|
429
|
+
&call_args[..call_args.len() - 1],
|
|
430
|
+
Some(RHash::try_convert(*last)?),
|
|
431
|
+
)
|
|
432
|
+
} else {
|
|
433
|
+
(call_args, None)
|
|
434
|
+
}
|
|
435
|
+
} else {
|
|
436
|
+
(call_args, None)
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// Args Tuple for args
|
|
440
|
+
let py_args = api.tuple_new(positional.len() as isize);
|
|
441
|
+
if py_args.is_null() {
|
|
442
|
+
return Err(magnus::Error::new(
|
|
443
|
+
ruby_helpers::runtime_error(),
|
|
444
|
+
"Failed to allocate Python args tuple",
|
|
445
|
+
));
|
|
446
|
+
}
|
|
447
|
+
let py_args_guard = PyGuard::new(py_args, api).ok_or_else(|| {
|
|
448
|
+
magnus::Error::new(ruby_helpers::runtime_error(), "Null Python args tuple")
|
|
449
|
+
})?;
|
|
450
|
+
for (i, arg) in positional.iter().enumerate() {
|
|
451
|
+
let py_arg = ruby_to_python(*arg, api)?;
|
|
452
|
+
// tuple_set_item steals reference on success
|
|
453
|
+
if api.tuple_set_item(py_args_guard.ptr(), i as isize, py_arg) != 0 {
|
|
454
|
+
api.decref(py_arg); // only decref on failure
|
|
455
|
+
if let Some(py_err) = PythonApi::extract_exception(api) {
|
|
456
|
+
return Err(magnus::Error::from(py_err));
|
|
457
|
+
}
|
|
458
|
+
return Err(magnus::Error::new(
|
|
459
|
+
ruby_helpers::runtime_error(),
|
|
460
|
+
"Failed to set tuple argument",
|
|
461
|
+
));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
// Kwargs Dict for kwargs
|
|
465
|
+
let py_kwargs_guard = if let Some(hash) = kwargs {
|
|
466
|
+
// Convert kwargs to Python dict
|
|
467
|
+
let dict = api.dict_new();
|
|
468
|
+
if dict.is_null() {
|
|
469
|
+
return Err(magnus::Error::new(
|
|
470
|
+
ruby_helpers::runtime_error(),
|
|
471
|
+
"Failed to allocate kwargs dict",
|
|
472
|
+
));
|
|
473
|
+
}
|
|
474
|
+
let guard = PyGuard::new(dict, api).ok_or_else(|| {
|
|
475
|
+
magnus::Error::new(ruby_helpers::runtime_error(), "Null kwargs dict")
|
|
476
|
+
})?;
|
|
477
|
+
// Save the key and value to python dict
|
|
478
|
+
hash.foreach(|k: Value, v: Value| {
|
|
479
|
+
let key = if let Ok(s) = String::try_convert(k) {
|
|
480
|
+
s
|
|
481
|
+
} else if let Ok(sym) = Symbol::try_convert(k) {
|
|
482
|
+
sym.name()?.to_string()
|
|
483
|
+
} else {
|
|
484
|
+
return Err(magnus::Error::new(
|
|
485
|
+
ruby_helpers::type_error(),
|
|
486
|
+
"kwargs keys must be String or Symbol",
|
|
487
|
+
));
|
|
488
|
+
};
|
|
489
|
+
let py_key = key.to_python(api).map_err(|e| {
|
|
490
|
+
magnus::Error::new(ruby_helpers::runtime_error(), format!("{e:?}"))
|
|
491
|
+
})?;
|
|
492
|
+
let py_val = ruby_to_python(v, api)?;
|
|
493
|
+
let rc = api.dict_set_item(guard.ptr(), py_key, py_val);
|
|
494
|
+
// dict_set_item does not steal
|
|
495
|
+
api.decref(py_key);
|
|
496
|
+
api.decref(py_val);
|
|
497
|
+
if rc != 0 {
|
|
498
|
+
if let Some(py_err) = PythonApi::extract_exception(api) {
|
|
499
|
+
return Err(magnus::Error::from(py_err));
|
|
500
|
+
}
|
|
501
|
+
return Err(magnus::Error::new(
|
|
502
|
+
ruby_helpers::runtime_error(),
|
|
503
|
+
"Failed to set kwargs item",
|
|
504
|
+
));
|
|
505
|
+
}
|
|
506
|
+
Ok(ForEach::Continue)
|
|
507
|
+
})?;
|
|
508
|
+
Some(guard)
|
|
509
|
+
} else {
|
|
510
|
+
None
|
|
511
|
+
};
|
|
512
|
+
let py_kwargs_ptr = py_kwargs_guard
|
|
513
|
+
.as_ref()
|
|
514
|
+
.map_or(std::ptr::null_mut(), |g| g.ptr());
|
|
515
|
+
let py_result =
|
|
516
|
+
api.object_call(py_attr_guard.ptr(), py_args_guard.ptr(), py_kwargs_ptr);
|
|
517
|
+
if py_result.is_null() {
|
|
518
|
+
if let Some(py_err) = PythonApi::extract_exception(api) {
|
|
519
|
+
return Err(magnus::Error::from(py_err));
|
|
520
|
+
}
|
|
521
|
+
return Err(magnus::Error::new(
|
|
522
|
+
ruby_helpers::runtime_error(),
|
|
523
|
+
"Python call failed",
|
|
524
|
+
));
|
|
525
|
+
}
|
|
526
|
+
let py_result_guard = PyGuard::new(py_result, api).ok_or_else(|| {
|
|
527
|
+
magnus::Error::new(ruby_helpers::runtime_error(), "Null Python result")
|
|
528
|
+
})?;
|
|
529
|
+
let wrapper = RubyxObject::new(py_result_guard.ptr(), api).ok_or_else(|| {
|
|
530
|
+
magnus::Error::new(
|
|
531
|
+
ruby_helpers::runtime_error(),
|
|
532
|
+
"Failed to wrap a Python result",
|
|
533
|
+
)
|
|
534
|
+
})?;
|
|
535
|
+
Ok(wrapper.into_value_with(&ruby))
|
|
536
|
+
})();
|
|
537
|
+
api.release_gil(gil);
|
|
538
|
+
result
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
pub fn respond_to_missing(&self, args: &[magnus::Value]) -> Result<bool, magnus::Error> {
|
|
542
|
+
if args.is_empty() {
|
|
543
|
+
return Err(magnus::Error::new(
|
|
544
|
+
ruby_helpers::arg_error(),
|
|
545
|
+
"No method name given",
|
|
546
|
+
));
|
|
547
|
+
}
|
|
548
|
+
let name = if let Ok(s) = String::try_convert(args[0]) {
|
|
549
|
+
s
|
|
550
|
+
} else if let Ok(sym) = Symbol::try_convert(args[0]) {
|
|
551
|
+
sym.name()?.to_string()
|
|
552
|
+
} else {
|
|
553
|
+
return Err(magnus::Error::new(
|
|
554
|
+
ruby_helpers::type_error(),
|
|
555
|
+
"method_missing expects Symbol/String method name",
|
|
556
|
+
));
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
let api = self.api;
|
|
560
|
+
let gil = api.ensure_gil();
|
|
561
|
+
let c_name = CString::new(name.as_str())
|
|
562
|
+
.map_err(|_| magnus::Error::new(ruby_helpers::arg_error(), "Invalid method name"))?;
|
|
563
|
+
let result = api.object_has_attr_string(self.as_ptr(), c_name.as_ptr()) != 0;
|
|
564
|
+
api.release_gil(gil);
|
|
565
|
+
Ok(result)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
pub fn to_s(&self) -> Result<String, magnus::Error> {
|
|
569
|
+
let api = self.api;
|
|
570
|
+
let gil = api.ensure_gil();
|
|
571
|
+
let py_str = api.object_str(self.as_ptr());
|
|
572
|
+
let result = if py_str.is_null() {
|
|
573
|
+
api.clear_error();
|
|
574
|
+
format!("#<RubyxObject:{:p}>", self.as_ptr())
|
|
575
|
+
} else {
|
|
576
|
+
let s = api.string_to_string(py_str).unwrap_or_default();
|
|
577
|
+
api.decref(py_str);
|
|
578
|
+
s
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
api.release_gil(gil);
|
|
582
|
+
Ok(result)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
pub fn inspect(&self) -> Result<String, magnus::Error> {
|
|
586
|
+
let api = self.api;
|
|
587
|
+
let gil = api.ensure_gil();
|
|
588
|
+
let result = api.object_repr(self.as_ptr());
|
|
589
|
+
|
|
590
|
+
api.release_gil(gil);
|
|
591
|
+
Ok(result)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
pub fn to_ruby(&self) -> Result<magnus::Value, magnus::Error> {
|
|
595
|
+
let api = self.api;
|
|
596
|
+
let gil = api.ensure_gil();
|
|
597
|
+
|
|
598
|
+
let sendable = python_to_sendable(self.as_ptr(), api)
|
|
599
|
+
.map_err(|e| magnus::Error::new(ruby_helpers::runtime_error(), e));
|
|
600
|
+
|
|
601
|
+
api.release_gil(gil);
|
|
602
|
+
|
|
603
|
+
sendable?.try_into()
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
pub fn getitem(&self, key: Value) -> Result<Value, magnus::Error> {
|
|
607
|
+
let api = self.api;
|
|
608
|
+
let gil = api.ensure_gil();
|
|
609
|
+
let ruby = Ruby::get()
|
|
610
|
+
.map_err(|e| magnus::Error::new(ruby_helpers::runtime_error(), e.to_string()))?;
|
|
611
|
+
|
|
612
|
+
let py_key = ruby_to_python(key, api)?;
|
|
613
|
+
let result = api.object_get_item(self.as_ptr(), py_key);
|
|
614
|
+
api.decref(py_key);
|
|
615
|
+
|
|
616
|
+
if result.is_null() {
|
|
617
|
+
let err = if let Some(exc) = PythonApi::extract_exception(api) {
|
|
618
|
+
magnus::Error::from(exc)
|
|
619
|
+
} else {
|
|
620
|
+
magnus::Error::new(ruby_helpers::runtime_error(), "KeyError or IndexError")
|
|
621
|
+
};
|
|
622
|
+
api.release_gil(gil);
|
|
623
|
+
return Err(err);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
let wrapper = RubyxObject::new(result, api).ok_or_else(|| {
|
|
627
|
+
magnus::Error::new(ruby_helpers::runtime_error(), "Failed to wrap result")
|
|
628
|
+
})?;
|
|
629
|
+
api.release_gil(gil);
|
|
630
|
+
Ok(wrapper.into_value_with(&ruby))
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
pub fn setitem(&self, key: Value, value: Value) -> Result<Value, magnus::Error> {
|
|
634
|
+
let api = self.api;
|
|
635
|
+
let gil = api.ensure_gil();
|
|
636
|
+
|
|
637
|
+
let py_key = ruby_to_python(key, api)?;
|
|
638
|
+
let py_val = ruby_to_python(value, api)?;
|
|
639
|
+
let result = api.object_set_item(self.as_ptr(), py_key, py_val);
|
|
640
|
+
api.decref(py_key);
|
|
641
|
+
api.decref(py_val);
|
|
642
|
+
|
|
643
|
+
if result == -1 {
|
|
644
|
+
let err = if let Some(exc) = PythonApi::extract_exception(api) {
|
|
645
|
+
magnus::Error::from(exc)
|
|
646
|
+
} else {
|
|
647
|
+
magnus::Error::new(ruby_helpers::runtime_error(), "Failed to set item")
|
|
648
|
+
};
|
|
649
|
+
api.release_gil(gil);
|
|
650
|
+
return Err(err);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
api.release_gil(gil);
|
|
654
|
+
Ok(value)
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
pub fn delitem(&self, key: Value) -> Result<Value, magnus::Error> {
|
|
658
|
+
let api = self.api;
|
|
659
|
+
let gil = api.ensure_gil();
|
|
660
|
+
let ruby = Ruby::get()
|
|
661
|
+
.map_err(|e| magnus::Error::new(ruby_helpers::runtime_error(), e.to_string()))?;
|
|
662
|
+
|
|
663
|
+
let py_key = ruby_to_python(key, api)?;
|
|
664
|
+
let result = api.object_del_item(self.as_ptr(), py_key);
|
|
665
|
+
api.decref(py_key);
|
|
666
|
+
|
|
667
|
+
if result == -1 {
|
|
668
|
+
let err = if let Some(exc) = PythonApi::extract_exception(api) {
|
|
669
|
+
magnus::Error::from(exc)
|
|
670
|
+
} else {
|
|
671
|
+
magnus::Error::new(ruby_helpers::runtime_error(), "Failed to delete item")
|
|
672
|
+
};
|
|
673
|
+
api.release_gil(gil);
|
|
674
|
+
return Err(err);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
api.release_gil(gil);
|
|
678
|
+
Ok(ruby.qnil().as_value())
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
pub fn each(&self) -> Result<Value, magnus::Error> {
|
|
682
|
+
let ruby = Ruby::get()
|
|
683
|
+
.map_err(|e| magnus::Error::new(ruby_helpers::runtime_error(), e.to_string()))?;
|
|
684
|
+
|
|
685
|
+
if !ruby.block_given() {
|
|
686
|
+
let receiver: Value = ruby.current_receiver()?;
|
|
687
|
+
return Ok(receiver.enumeratorize("each", ()).as_value());
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
let api = self.api;
|
|
691
|
+
let gil = api.ensure_gil();
|
|
692
|
+
|
|
693
|
+
let py_iter = api.object_get_iter(self.as_ptr());
|
|
694
|
+
if py_iter.is_null() {
|
|
695
|
+
api.clear_error();
|
|
696
|
+
api.release_gil(gil);
|
|
697
|
+
return Err(magnus::Error::new(
|
|
698
|
+
ruby_helpers::type_error(),
|
|
699
|
+
"Python object is not iterable",
|
|
700
|
+
));
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Use closure to ensure cleanup (decref + release_gil) runs on all paths,
|
|
704
|
+
// including early returns from yield_value (Ruby break) or wrap failures.
|
|
705
|
+
let result = (|| -> Result<(), magnus::Error> {
|
|
706
|
+
loop {
|
|
707
|
+
let item = api.iter_next(py_iter);
|
|
708
|
+
if item.is_null() {
|
|
709
|
+
if api.has_error() {
|
|
710
|
+
let exc = PythonApi::extract_exception(api);
|
|
711
|
+
if let Some(e) = exc {
|
|
712
|
+
return Err(magnus::Error::from(e));
|
|
713
|
+
}
|
|
714
|
+
return Err(magnus::Error::new(
|
|
715
|
+
ruby_helpers::runtime_error(),
|
|
716
|
+
"Python iteration error",
|
|
717
|
+
));
|
|
718
|
+
}
|
|
719
|
+
break;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
let wrapper = RubyxObject::new(item, api).ok_or_else(|| {
|
|
723
|
+
magnus::Error::new(ruby_helpers::runtime_error(), "Failed to wrap item")
|
|
724
|
+
})?;
|
|
725
|
+
let val = wrapper.into_value_with(&ruby);
|
|
726
|
+
let _: Value = ruby.yield_value(val)?;
|
|
727
|
+
}
|
|
728
|
+
Ok(())
|
|
729
|
+
})();
|
|
730
|
+
|
|
731
|
+
// Always cleanup — regardless of success or error
|
|
732
|
+
api.decref(py_iter);
|
|
733
|
+
api.release_gil(gil);
|
|
734
|
+
|
|
735
|
+
result?;
|
|
736
|
+
Ok(ruby.qnil().as_value())
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
pub fn is_truthy(&self) -> bool {
|
|
740
|
+
let gil = self.api.ensure_gil();
|
|
741
|
+
let result = self.api.object_is_true(self.py_obj);
|
|
742
|
+
self.api.release_gil(gil);
|
|
743
|
+
result
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
pub fn is_falsy(&self) -> bool {
|
|
747
|
+
!self.is_truthy()
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
pub fn is_callable(&self) -> bool {
|
|
751
|
+
let gil = self.api.ensure_gil();
|
|
752
|
+
let result = self.api.callable_check(self.py_obj) != 0;
|
|
753
|
+
self.api.release_gil(gil);
|
|
754
|
+
result
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
pub fn py_type(&self) -> Result<String, magnus::Error> {
|
|
758
|
+
let gil = self.api.ensure_gil();
|
|
759
|
+
let result = self.api.type_name(self.py_obj);
|
|
760
|
+
self.api.release_gil(gil);
|
|
761
|
+
Ok(result.unwrap_or_default())
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
impl Drop for RubyxObject {
|
|
766
|
+
fn drop(&mut self) {
|
|
767
|
+
// Python object no longer exist
|
|
768
|
+
if self.py_obj.is_null() {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
// Python api does not exist
|
|
772
|
+
if !self.api.is_initialized() {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
// Lock gil
|
|
776
|
+
let gil = self.api.ensure_gil();
|
|
777
|
+
self.api.decref(self.py_obj);
|
|
778
|
+
self.api.release_gil(gil);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
#[cfg(test)]
|
|
783
|
+
mod tests {
|
|
784
|
+
use super::*;
|
|
785
|
+
use crate::test_helpers::with_ruby_python;
|
|
786
|
+
use magnus::{IntoValue, TryConvert};
|
|
787
|
+
use serial_test::serial;
|
|
788
|
+
|
|
789
|
+
#[test]
|
|
790
|
+
#[serial]
|
|
791
|
+
fn test_ruby_to_python_primitives() {
|
|
792
|
+
with_ruby_python(|ruby, api| {
|
|
793
|
+
let py_nil =
|
|
794
|
+
ruby_to_python(ruby.qnil().as_value(), api).expect("nil conversion should succeed");
|
|
795
|
+
assert!(api.is_none(py_nil));
|
|
796
|
+
api.decref(py_nil);
|
|
797
|
+
|
|
798
|
+
let py_true = ruby_to_python(true.into_value_with(ruby), api)
|
|
799
|
+
.expect("true conversion should succeed");
|
|
800
|
+
assert!(api.is_true(py_true));
|
|
801
|
+
api.decref(py_true);
|
|
802
|
+
|
|
803
|
+
let py_int = ruby_to_python(42_i64.into_value_with(ruby), api)
|
|
804
|
+
.expect("int conversion should succeed");
|
|
805
|
+
assert_eq!(api.long_to_i64(py_int), 42);
|
|
806
|
+
api.decref(py_int);
|
|
807
|
+
|
|
808
|
+
let py_float = ruby_to_python(3.5_f64.into_value_with(ruby), api)
|
|
809
|
+
.expect("float conversion should succeed");
|
|
810
|
+
assert!(api.is_float(py_float));
|
|
811
|
+
assert!((api.float_to_f64(py_float) - 3.5).abs() < 1e-9);
|
|
812
|
+
api.decref(py_float);
|
|
813
|
+
|
|
814
|
+
let py_str = ruby_to_python("hello".into_value_with(ruby), api)
|
|
815
|
+
.expect("string conversion should succeed");
|
|
816
|
+
assert_eq!(api.string_to_string(py_str), Some("hello".to_string()));
|
|
817
|
+
api.decref(py_str);
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
#[test]
|
|
822
|
+
#[serial]
|
|
823
|
+
fn test_ruby_to_python_symbol() {
|
|
824
|
+
with_ruby_python(|ruby, api| {
|
|
825
|
+
let sym = ruby.sym_new("hello");
|
|
826
|
+
let py_str =
|
|
827
|
+
ruby_to_python(sym.as_value(), api).expect("symbol conversion should succeed");
|
|
828
|
+
assert!(api.is_string(py_str));
|
|
829
|
+
assert_eq!(api.string_to_string(py_str), Some("hello".to_string()));
|
|
830
|
+
api.decref(py_str);
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
#[test]
|
|
835
|
+
#[serial]
|
|
836
|
+
fn test_ruby_to_python_false() {
|
|
837
|
+
with_ruby_python(|ruby, api| {
|
|
838
|
+
let py_false = ruby_to_python(false.into_value_with(ruby), api)
|
|
839
|
+
.expect("false conversion should succeed");
|
|
840
|
+
assert!(api.is_false(py_false));
|
|
841
|
+
api.decref(py_false);
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
#[test]
|
|
846
|
+
#[serial]
|
|
847
|
+
fn test_ruby_to_python_array() {
|
|
848
|
+
with_ruby_python(|ruby, api| {
|
|
849
|
+
let arr = magnus::RArray::new();
|
|
850
|
+
arr.push(1_i64.into_value_with(ruby)).unwrap();
|
|
851
|
+
arr.push(2_i64.into_value_with(ruby)).unwrap();
|
|
852
|
+
arr.push(3_i64.into_value_with(ruby)).unwrap();
|
|
853
|
+
let py_list = ruby_to_python(arr.into_value_with(ruby), api)
|
|
854
|
+
.expect("array conversion should succeed");
|
|
855
|
+
assert!(api.list_check(py_list));
|
|
856
|
+
assert_eq!(api.list_size(py_list), 3);
|
|
857
|
+
assert_eq!(api.long_to_i64(api.list_get_item(py_list, 0)), 1);
|
|
858
|
+
assert_eq!(api.long_to_i64(api.list_get_item(py_list, 1)), 2);
|
|
859
|
+
assert_eq!(api.long_to_i64(api.list_get_item(py_list, 2)), 3);
|
|
860
|
+
api.decref(py_list);
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
#[test]
|
|
865
|
+
#[serial]
|
|
866
|
+
fn test_ruby_to_python_hash() {
|
|
867
|
+
with_ruby_python(|ruby, api| {
|
|
868
|
+
let hash = RHash::new();
|
|
869
|
+
hash.aset(ruby.sym_new("name"), "Alice".into_value_with(ruby))
|
|
870
|
+
.unwrap();
|
|
871
|
+
hash.aset(ruby.sym_new("age"), 30_i64.into_value_with(ruby))
|
|
872
|
+
.unwrap();
|
|
873
|
+
|
|
874
|
+
let py_dict = ruby_to_python(hash.into_value_with(ruby), api)
|
|
875
|
+
.expect("hash conversion should succeed");
|
|
876
|
+
assert!(api.dict_check(py_dict));
|
|
877
|
+
|
|
878
|
+
let key_name = api.string_from_str("name");
|
|
879
|
+
let val_name = api.dict_get_item(py_dict, key_name);
|
|
880
|
+
assert!(!val_name.is_null());
|
|
881
|
+
assert_eq!(api.string_to_string(val_name), Some("Alice".to_string()));
|
|
882
|
+
api.decref(key_name);
|
|
883
|
+
|
|
884
|
+
let key_age = api.string_from_str("age");
|
|
885
|
+
let val_age = api.dict_get_item(py_dict, key_age);
|
|
886
|
+
assert!(!val_age.is_null());
|
|
887
|
+
assert_eq!(api.long_to_i64(val_age), 30);
|
|
888
|
+
api.decref(key_age);
|
|
889
|
+
|
|
890
|
+
api.decref(py_dict);
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
#[test]
|
|
895
|
+
#[serial]
|
|
896
|
+
fn test_ruby_to_python_nested_array_in_hash() {
|
|
897
|
+
with_ruby_python(|ruby, api| {
|
|
898
|
+
let inner = magnus::RArray::new();
|
|
899
|
+
inner.push(10_i64.into_value_with(ruby)).unwrap();
|
|
900
|
+
inner.push(20_i64.into_value_with(ruby)).unwrap();
|
|
901
|
+
let hash = RHash::new();
|
|
902
|
+
hash.aset(ruby.sym_new("items"), inner.into_value_with(ruby))
|
|
903
|
+
.unwrap();
|
|
904
|
+
|
|
905
|
+
let py_dict = ruby_to_python(hash.into_value_with(ruby), api)
|
|
906
|
+
.expect("nested conversion should succeed");
|
|
907
|
+
assert!(api.dict_check(py_dict));
|
|
908
|
+
|
|
909
|
+
let key = api.string_from_str("items");
|
|
910
|
+
let py_list = api.dict_get_item(py_dict, key);
|
|
911
|
+
assert!(!py_list.is_null());
|
|
912
|
+
assert!(api.list_check(py_list));
|
|
913
|
+
assert_eq!(api.list_size(py_list), 2);
|
|
914
|
+
assert_eq!(api.long_to_i64(api.list_get_item(py_list, 0)), 10);
|
|
915
|
+
assert_eq!(api.long_to_i64(api.list_get_item(py_list, 1)), 20);
|
|
916
|
+
|
|
917
|
+
api.decref(key);
|
|
918
|
+
api.decref(py_dict);
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
#[test]
|
|
923
|
+
#[serial]
|
|
924
|
+
fn test_ruby_to_python_empty_array() {
|
|
925
|
+
with_ruby_python(|ruby, api| {
|
|
926
|
+
let arr = magnus::RArray::new();
|
|
927
|
+
let py_list =
|
|
928
|
+
ruby_to_python(arr.into_value_with(ruby), api).expect("empty array should convert");
|
|
929
|
+
assert!(api.list_check(py_list));
|
|
930
|
+
assert_eq!(api.list_size(py_list), 0);
|
|
931
|
+
api.decref(py_list);
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
#[test]
|
|
936
|
+
#[serial]
|
|
937
|
+
fn test_ruby_to_python_empty_hash() {
|
|
938
|
+
with_ruby_python(|ruby, api| {
|
|
939
|
+
let hash = RHash::new();
|
|
940
|
+
let py_dict =
|
|
941
|
+
ruby_to_python(hash.into_value_with(ruby), api).expect("empty hash should convert");
|
|
942
|
+
assert!(api.dict_check(py_dict));
|
|
943
|
+
api.decref(py_dict);
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
#[test]
|
|
948
|
+
#[serial]
|
|
949
|
+
fn test_ruby_to_python_rubyx_object_passthrough() {
|
|
950
|
+
with_ruby_python(|ruby, api| {
|
|
951
|
+
// Create a Python object via eval
|
|
952
|
+
let globals = crate::eval::make_globals(api);
|
|
953
|
+
let py_obj = api
|
|
954
|
+
.run_string("42", 258, globals.ptr(), globals.ptr())
|
|
955
|
+
.expect("eval should succeed");
|
|
956
|
+
|
|
957
|
+
let wrapper = RubyxObject::new(py_obj, api).expect("wrapper should be created");
|
|
958
|
+
let ruby_val = wrapper.into_value_with(ruby);
|
|
959
|
+
|
|
960
|
+
let py_result =
|
|
961
|
+
ruby_to_python(ruby_val, api).expect("RubyxObject passthrough should succeed");
|
|
962
|
+
assert_eq!(api.long_to_i64(py_result), 42);
|
|
963
|
+
api.decref(py_result);
|
|
964
|
+
api.decref(py_obj);
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
#[test]
|
|
969
|
+
#[serial]
|
|
970
|
+
fn test_method_missing_calls_python_callable() {
|
|
971
|
+
with_ruby_python(|ruby, api| {
|
|
972
|
+
let json = api.import_module("json").expect("json module must import");
|
|
973
|
+
let wrapper = RubyxObject::new(json, api).expect("wrapper should be created");
|
|
974
|
+
|
|
975
|
+
let args = vec![
|
|
976
|
+
"loads".into_value_with(ruby),
|
|
977
|
+
"[1, 2, 3]".into_value_with(ruby),
|
|
978
|
+
];
|
|
979
|
+
let result = wrapper
|
|
980
|
+
.method_missing(&args)
|
|
981
|
+
.expect("loads call should succeed");
|
|
982
|
+
let py_result = Obj::<RubyxObject>::try_convert(result)
|
|
983
|
+
.expect("result should be wrapped Python object");
|
|
984
|
+
assert!(api.list_check(py_result.as_ptr()));
|
|
985
|
+
assert_eq!(api.list_size(py_result.as_ptr()), 3);
|
|
986
|
+
|
|
987
|
+
drop(wrapper);
|
|
988
|
+
api.decref(json);
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
#[test]
|
|
993
|
+
#[serial]
|
|
994
|
+
fn test_method_missing_reads_non_callable_attribute() {
|
|
995
|
+
with_ruby_python(|ruby, api| {
|
|
996
|
+
let sys = api.import_module("sys").expect("sys module must import");
|
|
997
|
+
let wrapper = RubyxObject::new(sys, api).expect("wrapper should be created");
|
|
998
|
+
|
|
999
|
+
let args = vec!["version".into_value_with(ruby)];
|
|
1000
|
+
let result = wrapper
|
|
1001
|
+
.method_missing(&args)
|
|
1002
|
+
.expect("attribute read should succeed");
|
|
1003
|
+
let py_result = Obj::<RubyxObject>::try_convert(result)
|
|
1004
|
+
.expect("result should be wrapped Python object");
|
|
1005
|
+
assert!(api.is_string(py_result.as_ptr()));
|
|
1006
|
+
let version = api
|
|
1007
|
+
.string_to_string(py_result.as_ptr())
|
|
1008
|
+
.expect("version should decode as string");
|
|
1009
|
+
assert!(!version.is_empty());
|
|
1010
|
+
println!("Python version: {}", version);
|
|
1011
|
+
|
|
1012
|
+
drop(wrapper);
|
|
1013
|
+
api.decref(sys);
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
#[test]
|
|
1018
|
+
#[serial]
|
|
1019
|
+
fn test_method_missing_returns_error_for_unknown_member() {
|
|
1020
|
+
with_ruby_python(|ruby, api| {
|
|
1021
|
+
let sys = api.import_module("sys").expect("sys module must import");
|
|
1022
|
+
let wrapper = RubyxObject::new(sys, api).expect("wrapper should be created");
|
|
1023
|
+
|
|
1024
|
+
let args = vec!["this_member_should_not_exist_abc123".into_value_with(ruby)];
|
|
1025
|
+
let result = wrapper.method_missing(&args);
|
|
1026
|
+
assert!(result.is_err());
|
|
1027
|
+
|
|
1028
|
+
drop(wrapper);
|
|
1029
|
+
api.decref(sys);
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// ========== to_s tests ==========
|
|
1034
|
+
|
|
1035
|
+
#[test]
|
|
1036
|
+
#[serial]
|
|
1037
|
+
fn test_to_s_returns_python_str_for_int() {
|
|
1038
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1039
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1040
|
+
return;
|
|
1041
|
+
};
|
|
1042
|
+
let api = guard.api();
|
|
1043
|
+
|
|
1044
|
+
let py_int = api.long_from_i64(99);
|
|
1045
|
+
let wrapper = RubyxObject::new(py_int, api).unwrap();
|
|
1046
|
+
assert_eq!(wrapper.to_s().unwrap(), "99");
|
|
1047
|
+
drop(wrapper);
|
|
1048
|
+
api.decref(py_int);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
#[test]
|
|
1052
|
+
#[serial]
|
|
1053
|
+
fn test_to_s_returns_python_str_for_string() {
|
|
1054
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1055
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1056
|
+
return;
|
|
1057
|
+
};
|
|
1058
|
+
let api = guard.api();
|
|
1059
|
+
|
|
1060
|
+
let py_str = api.string_from_str("world");
|
|
1061
|
+
let wrapper = RubyxObject::new(py_str, api).unwrap();
|
|
1062
|
+
assert_eq!(wrapper.to_s().unwrap(), "world");
|
|
1063
|
+
drop(wrapper);
|
|
1064
|
+
api.decref(py_str);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
#[test]
|
|
1068
|
+
#[serial]
|
|
1069
|
+
fn test_to_s_returns_python_str_for_none() {
|
|
1070
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1071
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1072
|
+
return;
|
|
1073
|
+
};
|
|
1074
|
+
let api = guard.api();
|
|
1075
|
+
|
|
1076
|
+
api.incref(api.py_none);
|
|
1077
|
+
let wrapper = RubyxObject::new(api.py_none, api).unwrap();
|
|
1078
|
+
assert_eq!(wrapper.to_s().unwrap(), "None");
|
|
1079
|
+
drop(wrapper);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// ========== inspect tests ==========
|
|
1083
|
+
|
|
1084
|
+
#[test]
|
|
1085
|
+
#[serial]
|
|
1086
|
+
fn test_inspect_returns_repr_for_int() {
|
|
1087
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1088
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1089
|
+
return;
|
|
1090
|
+
};
|
|
1091
|
+
let api = guard.api();
|
|
1092
|
+
|
|
1093
|
+
let py_int = api.long_from_i64(7);
|
|
1094
|
+
let wrapper = RubyxObject::new(py_int, api).unwrap();
|
|
1095
|
+
assert_eq!(wrapper.inspect().unwrap(), "7");
|
|
1096
|
+
drop(wrapper);
|
|
1097
|
+
api.decref(py_int);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
#[test]
|
|
1101
|
+
#[serial]
|
|
1102
|
+
fn test_inspect_returns_repr_for_string_with_quotes() {
|
|
1103
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1104
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1105
|
+
return;
|
|
1106
|
+
};
|
|
1107
|
+
let api = guard.api();
|
|
1108
|
+
|
|
1109
|
+
let py_str = api.string_from_str("test");
|
|
1110
|
+
let wrapper = RubyxObject::new(py_str, api).unwrap();
|
|
1111
|
+
// Python repr of string includes quotes
|
|
1112
|
+
assert_eq!(wrapper.inspect().unwrap(), "'test'");
|
|
1113
|
+
drop(wrapper);
|
|
1114
|
+
api.decref(py_str);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// ========== to_ruby tests ==========
|
|
1118
|
+
|
|
1119
|
+
#[test]
|
|
1120
|
+
#[serial]
|
|
1121
|
+
fn test_to_ruby_converts_int() {
|
|
1122
|
+
with_ruby_python(|_ruby, api| {
|
|
1123
|
+
let py_int = api.long_from_i64(123);
|
|
1124
|
+
let wrapper = RubyxObject::new(py_int, api).unwrap();
|
|
1125
|
+
let ruby_val = wrapper.to_ruby().expect("to_ruby should succeed");
|
|
1126
|
+
assert_eq!(i64::try_convert(ruby_val).unwrap(), 123);
|
|
1127
|
+
drop(wrapper);
|
|
1128
|
+
api.decref(py_int);
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
#[test]
|
|
1133
|
+
#[serial]
|
|
1134
|
+
fn test_to_ruby_converts_string() {
|
|
1135
|
+
with_ruby_python(|_ruby, api| {
|
|
1136
|
+
let py_str = api.string_from_str("rubyx");
|
|
1137
|
+
let wrapper = RubyxObject::new(py_str, api).unwrap();
|
|
1138
|
+
let ruby_val = wrapper.to_ruby().expect("to_ruby should succeed");
|
|
1139
|
+
assert_eq!(String::try_convert(ruby_val).unwrap(), "rubyx");
|
|
1140
|
+
drop(wrapper);
|
|
1141
|
+
api.decref(py_str);
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
#[test]
|
|
1146
|
+
#[serial]
|
|
1147
|
+
fn test_to_ruby_converts_float() {
|
|
1148
|
+
with_ruby_python(|_ruby, api| {
|
|
1149
|
+
let py_float = api.float_from_f64(2.718);
|
|
1150
|
+
let wrapper = RubyxObject::new(py_float, api).unwrap();
|
|
1151
|
+
let ruby_val = wrapper.to_ruby().expect("to_ruby should succeed");
|
|
1152
|
+
let f = f64::try_convert(ruby_val).unwrap();
|
|
1153
|
+
assert!((f - 2.718).abs() < 0.001);
|
|
1154
|
+
drop(wrapper);
|
|
1155
|
+
api.decref(py_float);
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
#[test]
|
|
1160
|
+
#[serial]
|
|
1161
|
+
fn test_to_ruby_converts_bool() {
|
|
1162
|
+
with_ruby_python(|_ruby, api| {
|
|
1163
|
+
let py_true = api.bool_from_i64(1);
|
|
1164
|
+
let wrapper = RubyxObject::new(py_true, api).unwrap();
|
|
1165
|
+
let ruby_val = wrapper.to_ruby().expect("to_ruby should succeed");
|
|
1166
|
+
assert!(bool::try_convert(ruby_val).unwrap());
|
|
1167
|
+
drop(wrapper);
|
|
1168
|
+
api.decref(py_true);
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
#[test]
|
|
1173
|
+
#[serial]
|
|
1174
|
+
fn test_to_ruby_converts_none_to_nil() {
|
|
1175
|
+
with_ruby_python(|_ruby, api| {
|
|
1176
|
+
api.incref(api.py_none);
|
|
1177
|
+
let wrapper = RubyxObject::new(api.py_none, api).unwrap();
|
|
1178
|
+
let ruby_val = wrapper.to_ruby().expect("to_ruby should succeed");
|
|
1179
|
+
assert!(magnus::value::ReprValue::is_nil(ruby_val));
|
|
1180
|
+
drop(wrapper);
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
#[test]
|
|
1185
|
+
#[serial]
|
|
1186
|
+
fn test_to_ruby_errors_for_module() {
|
|
1187
|
+
with_ruby_python(|_ruby, api| {
|
|
1188
|
+
let module = api.import_module("os").expect("os should import");
|
|
1189
|
+
let wrapper = RubyxObject::new(module, api).unwrap();
|
|
1190
|
+
assert!(
|
|
1191
|
+
wrapper.to_ruby().is_err(),
|
|
1192
|
+
"module should not convert to Ruby"
|
|
1193
|
+
);
|
|
1194
|
+
drop(wrapper);
|
|
1195
|
+
api.decref(module);
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// ========== method_missing with args ==========
|
|
1200
|
+
|
|
1201
|
+
#[test]
|
|
1202
|
+
#[serial]
|
|
1203
|
+
fn test_method_missing_with_no_args_returns_error() {
|
|
1204
|
+
with_ruby_python(|_ruby, api| {
|
|
1205
|
+
let sys = api.import_module("sys").expect("sys should import");
|
|
1206
|
+
let wrapper = RubyxObject::new(sys, api).unwrap();
|
|
1207
|
+
|
|
1208
|
+
let result = wrapper.method_missing(&[]);
|
|
1209
|
+
assert!(result.is_err(), "empty args should error");
|
|
1210
|
+
|
|
1211
|
+
drop(wrapper);
|
|
1212
|
+
api.decref(sys);
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
#[test]
|
|
1217
|
+
#[serial]
|
|
1218
|
+
fn test_method_missing_chained_calls() {
|
|
1219
|
+
with_ruby_python(|ruby, api| {
|
|
1220
|
+
let json = api.import_module("json").expect("json should import");
|
|
1221
|
+
let wrapper = RubyxObject::new(json, api).unwrap();
|
|
1222
|
+
|
|
1223
|
+
// json.dumps(json.loads("[1,2]"))
|
|
1224
|
+
let loads_args = vec![
|
|
1225
|
+
"loads".into_value_with(ruby),
|
|
1226
|
+
"[1, 2, 3]".into_value_with(ruby),
|
|
1227
|
+
];
|
|
1228
|
+
let list_result = wrapper
|
|
1229
|
+
.method_missing(&loads_args)
|
|
1230
|
+
.expect("loads should succeed");
|
|
1231
|
+
|
|
1232
|
+
let list_wrapper =
|
|
1233
|
+
Obj::<RubyxObject>::try_convert(list_result).expect("should be RubyxObject");
|
|
1234
|
+
assert!(api.list_check(list_wrapper.as_ptr()));
|
|
1235
|
+
|
|
1236
|
+
drop(wrapper);
|
|
1237
|
+
api.decref(json);
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// ========== respond_to_missing? tests ==========
|
|
1242
|
+
|
|
1243
|
+
#[test]
|
|
1244
|
+
#[serial]
|
|
1245
|
+
fn test_respond_to_missing_existing_attr() {
|
|
1246
|
+
with_ruby_python(|ruby, api| {
|
|
1247
|
+
let sys = api.import_module("sys").expect("sys should import");
|
|
1248
|
+
let wrapper = RubyxObject::new(sys, api).unwrap();
|
|
1249
|
+
|
|
1250
|
+
// sys.version exists
|
|
1251
|
+
let args = vec!["version".into_value_with(ruby)];
|
|
1252
|
+
let result = wrapper.respond_to_missing(&args).expect("should not error");
|
|
1253
|
+
assert!(result, "sys.version should exist");
|
|
1254
|
+
|
|
1255
|
+
drop(wrapper);
|
|
1256
|
+
api.decref(sys);
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
#[test]
|
|
1261
|
+
#[serial]
|
|
1262
|
+
fn test_respond_to_missing_nonexistent_attr() {
|
|
1263
|
+
with_ruby_python(|ruby, api| {
|
|
1264
|
+
let sys = api.import_module("sys").expect("sys should import");
|
|
1265
|
+
let wrapper = RubyxObject::new(sys, api).unwrap();
|
|
1266
|
+
|
|
1267
|
+
let args = vec!["nonexistent_xyz_123".into_value_with(ruby)];
|
|
1268
|
+
let result = wrapper.respond_to_missing(&args).expect("should not error");
|
|
1269
|
+
assert!(!result, "nonexistent attr should return false");
|
|
1270
|
+
|
|
1271
|
+
drop(wrapper);
|
|
1272
|
+
api.decref(sys);
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
#[test]
|
|
1277
|
+
#[serial]
|
|
1278
|
+
fn test_respond_to_missing_callable_method() {
|
|
1279
|
+
with_ruby_python(|ruby, api| {
|
|
1280
|
+
let json = api.import_module("json").expect("json should import");
|
|
1281
|
+
let wrapper = RubyxObject::new(json, api).unwrap();
|
|
1282
|
+
|
|
1283
|
+
let args = vec!["loads".into_value_with(ruby)];
|
|
1284
|
+
let result = wrapper.respond_to_missing(&args).expect("should not error");
|
|
1285
|
+
assert!(result, "json.loads should exist");
|
|
1286
|
+
|
|
1287
|
+
drop(wrapper);
|
|
1288
|
+
api.decref(json);
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
#[test]
|
|
1293
|
+
#[serial]
|
|
1294
|
+
fn test_respond_to_missing_with_string_arg() {
|
|
1295
|
+
with_ruby_python(|ruby, api| {
|
|
1296
|
+
let sys = api.import_module("sys").expect("sys should import");
|
|
1297
|
+
let wrapper = RubyxObject::new(sys, api).unwrap();
|
|
1298
|
+
|
|
1299
|
+
// Pass string instead of symbol
|
|
1300
|
+
let args = vec!["version".into_value_with(ruby)];
|
|
1301
|
+
let result = wrapper.respond_to_missing(&args).expect("should not error");
|
|
1302
|
+
assert!(result, "should accept string arg too");
|
|
1303
|
+
|
|
1304
|
+
drop(wrapper);
|
|
1305
|
+
api.decref(sys);
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
#[test]
|
|
1310
|
+
#[serial]
|
|
1311
|
+
fn test_respond_to_missing_empty_args_errors() {
|
|
1312
|
+
with_ruby_python(|_ruby, api| {
|
|
1313
|
+
let sys = api.import_module("sys").expect("sys should import");
|
|
1314
|
+
let wrapper = RubyxObject::new(sys, api).unwrap();
|
|
1315
|
+
|
|
1316
|
+
let result = wrapper.respond_to_missing(&[]);
|
|
1317
|
+
assert!(result.is_err(), "empty args should error");
|
|
1318
|
+
|
|
1319
|
+
drop(wrapper);
|
|
1320
|
+
api.decref(sys);
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// ========== implicit conversion guards ==========
|
|
1325
|
+
|
|
1326
|
+
#[test]
|
|
1327
|
+
#[serial]
|
|
1328
|
+
fn test_method_missing_guards_to_ary() {
|
|
1329
|
+
with_ruby_python(|ruby, api| {
|
|
1330
|
+
let py_int = api.long_from_i64(42);
|
|
1331
|
+
let wrapper = RubyxObject::new(py_int, api).unwrap();
|
|
1332
|
+
|
|
1333
|
+
let args = vec!["to_ary".into_value_with(ruby)];
|
|
1334
|
+
let result = wrapper.method_missing(&args);
|
|
1335
|
+
assert!(result.is_err(), "to_ary should be guarded");
|
|
1336
|
+
|
|
1337
|
+
drop(wrapper);
|
|
1338
|
+
api.decref(py_int);
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
#[test]
|
|
1343
|
+
#[serial]
|
|
1344
|
+
fn test_method_missing_guards_to_str() {
|
|
1345
|
+
with_ruby_python(|ruby, api| {
|
|
1346
|
+
let py_int = api.long_from_i64(42);
|
|
1347
|
+
let wrapper = RubyxObject::new(py_int, api).unwrap();
|
|
1348
|
+
|
|
1349
|
+
let args = vec!["to_str".into_value_with(ruby)];
|
|
1350
|
+
let result = wrapper.method_missing(&args);
|
|
1351
|
+
assert!(result.is_err(), "to_str should be guarded");
|
|
1352
|
+
|
|
1353
|
+
drop(wrapper);
|
|
1354
|
+
api.decref(py_int);
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
#[test]
|
|
1359
|
+
#[serial]
|
|
1360
|
+
fn test_method_missing_guards_to_hash() {
|
|
1361
|
+
with_ruby_python(|ruby, api| {
|
|
1362
|
+
let py_int = api.long_from_i64(42);
|
|
1363
|
+
let wrapper = RubyxObject::new(py_int, api).unwrap();
|
|
1364
|
+
|
|
1365
|
+
let args = vec!["to_hash".into_value_with(ruby)];
|
|
1366
|
+
let result = wrapper.method_missing(&args);
|
|
1367
|
+
assert!(result.is_err(), "to_hash should be guarded");
|
|
1368
|
+
|
|
1369
|
+
drop(wrapper);
|
|
1370
|
+
api.decref(py_int);
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
#[test]
|
|
1375
|
+
#[serial]
|
|
1376
|
+
fn test_method_missing_guards_to_int() {
|
|
1377
|
+
with_ruby_python(|ruby, api| {
|
|
1378
|
+
let py_int = api.long_from_i64(42);
|
|
1379
|
+
let wrapper = RubyxObject::new(py_int, api).unwrap();
|
|
1380
|
+
|
|
1381
|
+
let args = vec!["to_int".into_value_with(ruby)];
|
|
1382
|
+
let result = wrapper.method_missing(&args);
|
|
1383
|
+
assert!(result.is_err(), "to_int should be guarded");
|
|
1384
|
+
|
|
1385
|
+
drop(wrapper);
|
|
1386
|
+
api.decref(py_int);
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
#[test]
|
|
1391
|
+
#[serial]
|
|
1392
|
+
fn test_method_missing_allows_regular_methods() {
|
|
1393
|
+
with_ruby_python(|ruby, api| {
|
|
1394
|
+
let sys = api.import_module("sys").expect("sys should import");
|
|
1395
|
+
let wrapper = RubyxObject::new(sys, api).unwrap();
|
|
1396
|
+
|
|
1397
|
+
// "version" is not guarded — should delegate to Python
|
|
1398
|
+
let args = vec!["version".into_value_with(ruby)];
|
|
1399
|
+
let result = wrapper.method_missing(&args);
|
|
1400
|
+
assert!(result.is_ok(), "regular attributes should not be guarded");
|
|
1401
|
+
|
|
1402
|
+
drop(wrapper);
|
|
1403
|
+
api.decref(sys);
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// ========== getitem / setitem / delitem tests ==========
|
|
1408
|
+
|
|
1409
|
+
#[test]
|
|
1410
|
+
#[serial]
|
|
1411
|
+
fn test_getitem_dict_string_key() {
|
|
1412
|
+
with_ruby_python(|ruby, api| {
|
|
1413
|
+
let globals = crate::eval::make_globals(api);
|
|
1414
|
+
let py_dict = api
|
|
1415
|
+
.run_string(
|
|
1416
|
+
"{'name': 'Alice', 'age': 30}",
|
|
1417
|
+
258,
|
|
1418
|
+
globals.ptr(),
|
|
1419
|
+
globals.ptr(),
|
|
1420
|
+
)
|
|
1421
|
+
.expect("should create dict");
|
|
1422
|
+
let wrapper = RubyxObject::new(py_dict, api).unwrap();
|
|
1423
|
+
|
|
1424
|
+
let key: magnus::Value = "name".into_value_with(ruby);
|
|
1425
|
+
let result = wrapper.getitem(key).expect("getitem should succeed");
|
|
1426
|
+
let obj = Obj::<RubyxObject>::try_convert(result).expect("should be RubyxObject");
|
|
1427
|
+
assert_eq!(
|
|
1428
|
+
api.string_to_string(obj.as_ptr()),
|
|
1429
|
+
Some("Alice".to_string())
|
|
1430
|
+
);
|
|
1431
|
+
|
|
1432
|
+
drop(wrapper);
|
|
1433
|
+
api.decref(py_dict);
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
#[test]
|
|
1438
|
+
#[serial]
|
|
1439
|
+
fn test_getitem_dict_integer_key() {
|
|
1440
|
+
with_ruby_python(|ruby, api| {
|
|
1441
|
+
let globals = crate::eval::make_globals(api);
|
|
1442
|
+
let py_dict = api
|
|
1443
|
+
.run_string("{1: 'one', 2: 'two'}", 258, globals.ptr(), globals.ptr())
|
|
1444
|
+
.expect("should create dict");
|
|
1445
|
+
let wrapper = RubyxObject::new(py_dict, api).unwrap();
|
|
1446
|
+
|
|
1447
|
+
let key: magnus::Value = 1_i64.into_value_with(ruby);
|
|
1448
|
+
let result = wrapper.getitem(key).expect("getitem should succeed");
|
|
1449
|
+
let obj = Obj::<RubyxObject>::try_convert(result).expect("should be RubyxObject");
|
|
1450
|
+
assert_eq!(api.string_to_string(obj.as_ptr()), Some("one".to_string()));
|
|
1451
|
+
|
|
1452
|
+
drop(wrapper);
|
|
1453
|
+
api.decref(py_dict);
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
#[test]
|
|
1458
|
+
#[serial]
|
|
1459
|
+
fn test_getitem_list_by_index() {
|
|
1460
|
+
with_ruby_python(|ruby, api| {
|
|
1461
|
+
let globals = crate::eval::make_globals(api);
|
|
1462
|
+
let py_list = api
|
|
1463
|
+
.run_string("[10, 20, 30]", 258, globals.ptr(), globals.ptr())
|
|
1464
|
+
.expect("should create list");
|
|
1465
|
+
let wrapper = RubyxObject::new(py_list, api).unwrap();
|
|
1466
|
+
|
|
1467
|
+
let key: magnus::Value = 1_i64.into_value_with(ruby);
|
|
1468
|
+
let result = wrapper.getitem(key).expect("getitem should succeed");
|
|
1469
|
+
let obj = Obj::<RubyxObject>::try_convert(result).expect("should be RubyxObject");
|
|
1470
|
+
assert_eq!(api.long_to_i64(obj.as_ptr()), 20);
|
|
1471
|
+
|
|
1472
|
+
drop(wrapper);
|
|
1473
|
+
api.decref(py_list);
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
#[test]
|
|
1478
|
+
#[serial]
|
|
1479
|
+
fn test_getitem_list_negative_index() {
|
|
1480
|
+
with_ruby_python(|ruby, api| {
|
|
1481
|
+
let globals = crate::eval::make_globals(api);
|
|
1482
|
+
let py_list = api
|
|
1483
|
+
.run_string("[10, 20, 30]", 258, globals.ptr(), globals.ptr())
|
|
1484
|
+
.expect("should create list");
|
|
1485
|
+
let wrapper = RubyxObject::new(py_list, api).unwrap();
|
|
1486
|
+
|
|
1487
|
+
// Python supports negative indexing
|
|
1488
|
+
let key: magnus::Value = (-1_i64).into_value_with(ruby);
|
|
1489
|
+
let result = wrapper.getitem(key).expect("getitem should succeed");
|
|
1490
|
+
let obj = Obj::<RubyxObject>::try_convert(result).expect("should be RubyxObject");
|
|
1491
|
+
assert_eq!(api.long_to_i64(obj.as_ptr()), 30);
|
|
1492
|
+
|
|
1493
|
+
drop(wrapper);
|
|
1494
|
+
api.decref(py_list);
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
#[test]
|
|
1499
|
+
#[serial]
|
|
1500
|
+
fn test_getitem_missing_key_raises_error() {
|
|
1501
|
+
with_ruby_python(|ruby, api| {
|
|
1502
|
+
let globals = crate::eval::make_globals(api);
|
|
1503
|
+
let py_dict = api
|
|
1504
|
+
.run_string("{}", 258, globals.ptr(), globals.ptr())
|
|
1505
|
+
.expect("should create empty dict");
|
|
1506
|
+
let wrapper = RubyxObject::new(py_dict, api).unwrap();
|
|
1507
|
+
|
|
1508
|
+
let key: magnus::Value = "nope".into_value_with(ruby);
|
|
1509
|
+
let result = wrapper.getitem(key);
|
|
1510
|
+
assert!(result.is_err(), "missing key should raise error");
|
|
1511
|
+
|
|
1512
|
+
drop(wrapper);
|
|
1513
|
+
api.decref(py_dict);
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
#[test]
|
|
1518
|
+
#[serial]
|
|
1519
|
+
fn test_getitem_index_out_of_range() {
|
|
1520
|
+
with_ruby_python(|ruby, api| {
|
|
1521
|
+
let globals = crate::eval::make_globals(api);
|
|
1522
|
+
let py_list = api
|
|
1523
|
+
.run_string("[1, 2]", 258, globals.ptr(), globals.ptr())
|
|
1524
|
+
.expect("should create list");
|
|
1525
|
+
let wrapper = RubyxObject::new(py_list, api).unwrap();
|
|
1526
|
+
|
|
1527
|
+
let key: magnus::Value = 99_i64.into_value_with(ruby);
|
|
1528
|
+
let result = wrapper.getitem(key);
|
|
1529
|
+
assert!(result.is_err(), "out of range index should raise error");
|
|
1530
|
+
|
|
1531
|
+
drop(wrapper);
|
|
1532
|
+
api.decref(py_list);
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
#[test]
|
|
1537
|
+
#[serial]
|
|
1538
|
+
fn test_setitem_dict() {
|
|
1539
|
+
with_ruby_python(|ruby, api| {
|
|
1540
|
+
let globals = crate::eval::make_globals(api);
|
|
1541
|
+
let py_dict = api
|
|
1542
|
+
.run_string("{}", 258, globals.ptr(), globals.ptr())
|
|
1543
|
+
.expect("should create empty dict");
|
|
1544
|
+
let wrapper = RubyxObject::new(py_dict, api).unwrap();
|
|
1545
|
+
|
|
1546
|
+
let key: magnus::Value = "role".into_value_with(ruby);
|
|
1547
|
+
let val: magnus::Value = "admin".into_value_with(ruby);
|
|
1548
|
+
wrapper.setitem(key, val).expect("setitem should succeed");
|
|
1549
|
+
|
|
1550
|
+
// Verify the value was set
|
|
1551
|
+
let check_key: magnus::Value = "role".into_value_with(ruby);
|
|
1552
|
+
let result = wrapper.getitem(check_key).expect("should find new key");
|
|
1553
|
+
let obj = Obj::<RubyxObject>::try_convert(result).expect("should be RubyxObject");
|
|
1554
|
+
assert_eq!(
|
|
1555
|
+
api.string_to_string(obj.as_ptr()),
|
|
1556
|
+
Some("admin".to_string())
|
|
1557
|
+
);
|
|
1558
|
+
|
|
1559
|
+
drop(wrapper);
|
|
1560
|
+
api.decref(py_dict);
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
#[test]
|
|
1565
|
+
#[serial]
|
|
1566
|
+
fn test_setitem_list() {
|
|
1567
|
+
with_ruby_python(|ruby, api| {
|
|
1568
|
+
let globals = crate::eval::make_globals(api);
|
|
1569
|
+
let py_list = api
|
|
1570
|
+
.run_string("[1, 2, 3]", 258, globals.ptr(), globals.ptr())
|
|
1571
|
+
.expect("should create list");
|
|
1572
|
+
let wrapper = RubyxObject::new(py_list, api).unwrap();
|
|
1573
|
+
|
|
1574
|
+
let key: magnus::Value = 1_i64.into_value_with(ruby);
|
|
1575
|
+
let val: magnus::Value = 99_i64.into_value_with(ruby);
|
|
1576
|
+
wrapper.setitem(key, val).expect("setitem should succeed");
|
|
1577
|
+
|
|
1578
|
+
// Verify
|
|
1579
|
+
let check_key: magnus::Value = 1_i64.into_value_with(ruby);
|
|
1580
|
+
let result = wrapper.getitem(check_key).expect("should read index 1");
|
|
1581
|
+
let obj = Obj::<RubyxObject>::try_convert(result).expect("should be RubyxObject");
|
|
1582
|
+
assert_eq!(api.long_to_i64(obj.as_ptr()), 99);
|
|
1583
|
+
|
|
1584
|
+
drop(wrapper);
|
|
1585
|
+
api.decref(py_list);
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
#[test]
|
|
1590
|
+
#[serial]
|
|
1591
|
+
fn test_setitem_overwrite_existing() {
|
|
1592
|
+
with_ruby_python(|ruby, api| {
|
|
1593
|
+
let globals = crate::eval::make_globals(api);
|
|
1594
|
+
let py_dict = api
|
|
1595
|
+
.run_string("{'x': 1}", 258, globals.ptr(), globals.ptr())
|
|
1596
|
+
.expect("should create dict");
|
|
1597
|
+
let wrapper = RubyxObject::new(py_dict, api).unwrap();
|
|
1598
|
+
|
|
1599
|
+
let key: magnus::Value = "x".into_value_with(ruby);
|
|
1600
|
+
let val: magnus::Value = 42_i64.into_value_with(ruby);
|
|
1601
|
+
wrapper.setitem(key, val).expect("setitem should succeed");
|
|
1602
|
+
|
|
1603
|
+
let check_key: magnus::Value = "x".into_value_with(ruby);
|
|
1604
|
+
let result = wrapper.getitem(check_key).expect("should read key");
|
|
1605
|
+
let obj = Obj::<RubyxObject>::try_convert(result).expect("should be RubyxObject");
|
|
1606
|
+
assert_eq!(api.long_to_i64(obj.as_ptr()), 42);
|
|
1607
|
+
|
|
1608
|
+
drop(wrapper);
|
|
1609
|
+
api.decref(py_dict);
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
#[test]
|
|
1614
|
+
#[serial]
|
|
1615
|
+
fn test_delitem_dict() {
|
|
1616
|
+
with_ruby_python(|ruby, api| {
|
|
1617
|
+
let globals = crate::eval::make_globals(api);
|
|
1618
|
+
let py_dict = api
|
|
1619
|
+
.run_string("{'a': 1, 'b': 2}", 258, globals.ptr(), globals.ptr())
|
|
1620
|
+
.expect("should create dict");
|
|
1621
|
+
let wrapper = RubyxObject::new(py_dict, api).unwrap();
|
|
1622
|
+
|
|
1623
|
+
let key: magnus::Value = "a".into_value_with(ruby);
|
|
1624
|
+
wrapper.delitem(key).expect("delitem should succeed");
|
|
1625
|
+
|
|
1626
|
+
// Verify 'a' is gone
|
|
1627
|
+
let check_key: magnus::Value = "a".into_value_with(ruby);
|
|
1628
|
+
let result = wrapper.getitem(check_key);
|
|
1629
|
+
assert!(result.is_err(), "'a' should be deleted");
|
|
1630
|
+
|
|
1631
|
+
// Verify 'b' still exists
|
|
1632
|
+
let check_key_b: magnus::Value = "b".into_value_with(ruby);
|
|
1633
|
+
let result_b = wrapper
|
|
1634
|
+
.getitem(check_key_b)
|
|
1635
|
+
.expect("'b' should still exist");
|
|
1636
|
+
let obj = Obj::<RubyxObject>::try_convert(result_b).expect("should be RubyxObject");
|
|
1637
|
+
assert_eq!(api.long_to_i64(obj.as_ptr()), 2);
|
|
1638
|
+
|
|
1639
|
+
drop(wrapper);
|
|
1640
|
+
api.decref(py_dict);
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
#[test]
|
|
1645
|
+
#[serial]
|
|
1646
|
+
fn test_delitem_missing_key_raises_error() {
|
|
1647
|
+
with_ruby_python(|ruby, api| {
|
|
1648
|
+
let globals = crate::eval::make_globals(api);
|
|
1649
|
+
let py_dict = api
|
|
1650
|
+
.run_string("{}", 258, globals.ptr(), globals.ptr())
|
|
1651
|
+
.expect("should create empty dict");
|
|
1652
|
+
let wrapper = RubyxObject::new(py_dict, api).unwrap();
|
|
1653
|
+
|
|
1654
|
+
let key: magnus::Value = "nope".into_value_with(ruby);
|
|
1655
|
+
let result = wrapper.delitem(key);
|
|
1656
|
+
assert!(result.is_err(), "deleting missing key should error");
|
|
1657
|
+
|
|
1658
|
+
drop(wrapper);
|
|
1659
|
+
api.decref(py_dict);
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// ========== is_truthy / is_falsy tests ==========
|
|
1664
|
+
|
|
1665
|
+
#[test]
|
|
1666
|
+
#[serial]
|
|
1667
|
+
fn test_truthy_nonzero_int() {
|
|
1668
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1669
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1670
|
+
return;
|
|
1671
|
+
};
|
|
1672
|
+
let api = guard.api();
|
|
1673
|
+
let w = RubyxObject::new(api.long_from_i64(42), api).unwrap();
|
|
1674
|
+
assert!(w.is_truthy());
|
|
1675
|
+
assert!(!w.is_falsy());
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
#[test]
|
|
1679
|
+
#[serial]
|
|
1680
|
+
fn test_truthy_zero_int() {
|
|
1681
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1682
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1683
|
+
return;
|
|
1684
|
+
};
|
|
1685
|
+
let api = guard.api();
|
|
1686
|
+
let w = RubyxObject::new(api.long_from_i64(0), api).unwrap();
|
|
1687
|
+
assert!(!w.is_truthy());
|
|
1688
|
+
assert!(w.is_falsy());
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
#[test]
|
|
1692
|
+
#[serial]
|
|
1693
|
+
fn test_truthy_none() {
|
|
1694
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1695
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1696
|
+
return;
|
|
1697
|
+
};
|
|
1698
|
+
let api = guard.api();
|
|
1699
|
+
api.incref(api.py_none);
|
|
1700
|
+
let w = RubyxObject::new(api.py_none, api).unwrap();
|
|
1701
|
+
assert!(!w.is_truthy());
|
|
1702
|
+
assert!(w.is_falsy());
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
#[test]
|
|
1706
|
+
#[serial]
|
|
1707
|
+
fn test_truthy_bool() {
|
|
1708
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1709
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1710
|
+
return;
|
|
1711
|
+
};
|
|
1712
|
+
let api = guard.api();
|
|
1713
|
+
let t = RubyxObject::new(api.bool_from_i64(1), api).unwrap();
|
|
1714
|
+
assert!(t.is_truthy());
|
|
1715
|
+
let f = RubyxObject::new(api.bool_from_i64(0), api).unwrap();
|
|
1716
|
+
assert!(f.is_falsy());
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
#[test]
|
|
1720
|
+
#[serial]
|
|
1721
|
+
fn test_truthy_empty_string() {
|
|
1722
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1723
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1724
|
+
return;
|
|
1725
|
+
};
|
|
1726
|
+
let api = guard.api();
|
|
1727
|
+
let w = RubyxObject::new(api.string_from_str(""), api).unwrap();
|
|
1728
|
+
assert!(w.is_falsy());
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
#[test]
|
|
1732
|
+
#[serial]
|
|
1733
|
+
fn test_truthy_nonempty_string() {
|
|
1734
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1735
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1736
|
+
return;
|
|
1737
|
+
};
|
|
1738
|
+
let api = guard.api();
|
|
1739
|
+
let w = RubyxObject::new(api.string_from_str("hello"), api).unwrap();
|
|
1740
|
+
assert!(w.is_truthy());
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
#[test]
|
|
1744
|
+
#[serial]
|
|
1745
|
+
fn test_truthy_empty_list() {
|
|
1746
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1747
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1748
|
+
return;
|
|
1749
|
+
};
|
|
1750
|
+
let api = guard.api();
|
|
1751
|
+
let list = unsafe { (api.py_list_new)(0) };
|
|
1752
|
+
let w = RubyxObject::new(list, api).unwrap();
|
|
1753
|
+
assert!(w.is_falsy());
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
#[test]
|
|
1757
|
+
#[serial]
|
|
1758
|
+
fn test_truthy_nonempty_list() {
|
|
1759
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1760
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1761
|
+
return;
|
|
1762
|
+
};
|
|
1763
|
+
let api = guard.api();
|
|
1764
|
+
let list = unsafe { (api.py_list_new)(1) };
|
|
1765
|
+
unsafe { (api.py_list_set_item)(list, 0, api.long_from_i64(1)) };
|
|
1766
|
+
let w = RubyxObject::new(list, api).unwrap();
|
|
1767
|
+
assert!(w.is_truthy());
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// ========== is_callable tests ==========
|
|
1771
|
+
|
|
1772
|
+
#[test]
|
|
1773
|
+
#[serial]
|
|
1774
|
+
fn test_callable_function() {
|
|
1775
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1776
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1777
|
+
return;
|
|
1778
|
+
};
|
|
1779
|
+
let api = guard.api();
|
|
1780
|
+
let json = api.import_module("json").expect("json should import");
|
|
1781
|
+
let loads = api.object_get_attr_string(json, "loads");
|
|
1782
|
+
let w = RubyxObject::new(loads, api).unwrap();
|
|
1783
|
+
assert!(w.is_callable());
|
|
1784
|
+
drop(w);
|
|
1785
|
+
api.decref(loads);
|
|
1786
|
+
api.decref(json);
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
#[test]
|
|
1790
|
+
#[serial]
|
|
1791
|
+
fn test_not_callable_int() {
|
|
1792
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1793
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1794
|
+
return;
|
|
1795
|
+
};
|
|
1796
|
+
let api = guard.api();
|
|
1797
|
+
let w = RubyxObject::new(api.long_from_i64(42), api).unwrap();
|
|
1798
|
+
assert!(!w.is_callable());
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
#[test]
|
|
1802
|
+
#[serial]
|
|
1803
|
+
fn test_not_callable_string() {
|
|
1804
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1805
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1806
|
+
return;
|
|
1807
|
+
};
|
|
1808
|
+
let api = guard.api();
|
|
1809
|
+
let w = RubyxObject::new(api.string_from_str("hi"), api).unwrap();
|
|
1810
|
+
assert!(!w.is_callable());
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
#[test]
|
|
1814
|
+
#[serial]
|
|
1815
|
+
fn test_not_callable_module() {
|
|
1816
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1817
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1818
|
+
return;
|
|
1819
|
+
};
|
|
1820
|
+
let api = guard.api();
|
|
1821
|
+
let os = api.import_module("os").expect("os should import");
|
|
1822
|
+
let w = RubyxObject::new(os, api).unwrap();
|
|
1823
|
+
assert!(!w.is_callable());
|
|
1824
|
+
drop(w);
|
|
1825
|
+
api.decref(os);
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
// ========== py_type tests ==========
|
|
1829
|
+
|
|
1830
|
+
#[test]
|
|
1831
|
+
#[serial]
|
|
1832
|
+
fn test_py_type_int() {
|
|
1833
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1834
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1835
|
+
return;
|
|
1836
|
+
};
|
|
1837
|
+
let api = guard.api();
|
|
1838
|
+
let w = RubyxObject::new(api.long_from_i64(1), api).unwrap();
|
|
1839
|
+
assert_eq!(w.py_type().unwrap(), "int");
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
#[test]
|
|
1843
|
+
#[serial]
|
|
1844
|
+
fn test_py_type_str() {
|
|
1845
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1846
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1847
|
+
return;
|
|
1848
|
+
};
|
|
1849
|
+
let api = guard.api();
|
|
1850
|
+
let w = RubyxObject::new(api.string_from_str("x"), api).unwrap();
|
|
1851
|
+
assert_eq!(w.py_type().unwrap(), "str");
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
#[test]
|
|
1855
|
+
#[serial]
|
|
1856
|
+
fn test_py_type_float() {
|
|
1857
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1858
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1859
|
+
return;
|
|
1860
|
+
};
|
|
1861
|
+
let api = guard.api();
|
|
1862
|
+
let w = RubyxObject::new(api.float_from_f64(1.0), api).unwrap();
|
|
1863
|
+
assert_eq!(w.py_type().unwrap(), "float");
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
#[test]
|
|
1867
|
+
#[serial]
|
|
1868
|
+
fn test_py_type_bool() {
|
|
1869
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1870
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1871
|
+
return;
|
|
1872
|
+
};
|
|
1873
|
+
let api = guard.api();
|
|
1874
|
+
let w = RubyxObject::new(api.bool_from_i64(1), api).unwrap();
|
|
1875
|
+
assert_eq!(w.py_type().unwrap(), "bool");
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
#[test]
|
|
1879
|
+
#[serial]
|
|
1880
|
+
fn test_py_type_list() {
|
|
1881
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1882
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1883
|
+
return;
|
|
1884
|
+
};
|
|
1885
|
+
let api = guard.api();
|
|
1886
|
+
let list = unsafe { (api.py_list_new)(0) };
|
|
1887
|
+
let w = RubyxObject::new(list, api).unwrap();
|
|
1888
|
+
assert_eq!(w.py_type().unwrap(), "list");
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
#[test]
|
|
1892
|
+
#[serial]
|
|
1893
|
+
fn test_py_type_dict() {
|
|
1894
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1895
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1896
|
+
return;
|
|
1897
|
+
};
|
|
1898
|
+
let api = guard.api();
|
|
1899
|
+
let dict = api.dict_new();
|
|
1900
|
+
let w = RubyxObject::new(dict, api).unwrap();
|
|
1901
|
+
assert_eq!(w.py_type().unwrap(), "dict");
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
#[test]
|
|
1905
|
+
#[serial]
|
|
1906
|
+
fn test_py_type_none() {
|
|
1907
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1908
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1909
|
+
return;
|
|
1910
|
+
};
|
|
1911
|
+
let api = guard.api();
|
|
1912
|
+
api.incref(api.py_none);
|
|
1913
|
+
let w = RubyxObject::new(api.py_none, api).unwrap();
|
|
1914
|
+
assert_eq!(w.py_type().unwrap(), "NoneType");
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
#[test]
|
|
1918
|
+
#[serial]
|
|
1919
|
+
fn test_py_type_module() {
|
|
1920
|
+
use crate::test_helpers::skip_if_no_python;
|
|
1921
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1922
|
+
return;
|
|
1923
|
+
};
|
|
1924
|
+
let api = guard.api();
|
|
1925
|
+
let os = api.import_module("os").expect("os should import");
|
|
1926
|
+
let w = RubyxObject::new(os, api).unwrap();
|
|
1927
|
+
assert_eq!(w.py_type().unwrap(), "module");
|
|
1928
|
+
drop(w);
|
|
1929
|
+
api.decref(os);
|
|
1930
|
+
}
|
|
1931
|
+
}
|