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.
@@ -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
+ }