rubyx-py 0.1.0 → 0.2.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.
data/ext/rubyx/src/lib.rs CHANGED
@@ -18,6 +18,7 @@ mod convert;
18
18
  mod eval;
19
19
  mod exception;
20
20
  mod future;
21
+ mod gvl;
21
22
  mod import;
22
23
  mod nonblocking_stream;
23
24
  mod pipe_notify;
@@ -155,7 +156,8 @@ fn init(ruby: &magnus::Ruby) -> Result<(), magnus::Error> {
155
156
 
156
157
  // Rubyx::Future class
157
158
  let future_class = rubyx_module.define_class("Future", ruby.class_object())?;
158
- future_class.define_method("value", method!(crate::future::RubyxFuture::value, 0))?;
159
+ // value() instead of await since await is a reserved keyword
160
+ future_class.define_method("await", method!(crate::future::RubyxFuture::value, 0))?;
159
161
  future_class.define_method("ready?", method!(crate::future::RubyxFuture::is_ready, 0))?;
160
162
 
161
163
  Ok(())
@@ -2968,13 +2970,17 @@ mod tests {
2968
2970
 
2969
2971
  #[test]
2970
2972
  #[serial]
2971
- fn test_to_ruby_unconvertible_returns_error() {
2973
+ fn test_to_ruby_wraps_module_via_pyobjectref() {
2972
2974
  with_ruby_python(|_ruby, api| {
2973
- // Python module objects can't be converted to Ruby primitives
2975
+ // Modules have __dict__ and are not callable, so python_to_sendable
2976
+ // returns PyObjectRef → wrapped as RubyxObject.
2974
2977
  let module = api.import_module("sys").expect("sys should import");
2975
2978
  let wrapper = RubyxObject::new(module, api).unwrap();
2976
2979
  let result = wrapper.to_ruby();
2977
- assert!(result.is_err(), "module should not be convertible to Ruby");
2980
+ assert!(
2981
+ result.is_ok(),
2982
+ "module should wrap as RubyxObject via PyObjectRef"
2983
+ );
2978
2984
 
2979
2985
  drop(wrapper);
2980
2986
  api.decref(module);
@@ -3008,6 +3014,17 @@ mod tests {
3008
3014
 
3009
3015
  // ========== Rubyx::Future / async_await tests ==========
3010
3016
 
3017
+ /// Spin-wait then call value() — ensures the fast path is hit in tests,
3018
+ /// avoiding rb_thread_call_without_gvl which deadlocks in embedded Ruby.
3019
+ fn test_future_value(
3020
+ future: &crate::future::RubyxFuture,
3021
+ ) -> Result<magnus::Value, magnus::Error> {
3022
+ while !future.is_ready() {
3023
+ std::thread::sleep(std::time::Duration::from_millis(1));
3024
+ }
3025
+ future.value()
3026
+ }
3027
+
3011
3028
  /// Helper: define an async function in globals, create coroutine,
3012
3029
  /// release GIL, run future, restore GIL, return result.
3013
3030
  fn run_future_test(
@@ -3025,7 +3042,7 @@ mod tests {
3025
3042
 
3026
3043
  let tstate = api.save_thread();
3027
3044
  let future = crate::future::RubyxFuture::from_coroutine(coroutine, api);
3028
- let result = future.value();
3045
+ let result = test_future_value(&future);
3029
3046
  drop(future);
3030
3047
  api.restore_thread(tstate);
3031
3048
 
@@ -3197,14 +3214,18 @@ mod tests {
3197
3214
  .run_string("get_val()", PY_EVAL_INPUT, globals, globals)
3198
3215
  .expect("should create coroutine");
3199
3216
 
3200
- let wrapper = RubyxObject::new(coroutine, api).expect("should wrap coroutine");
3201
- let coro_value: magnus::Value = magnus::IntoValue::into_value_with(wrapper, ruby);
3202
- let result =
3203
- crate::eval::rubyx_await(coro_value).expect("blocking await should succeed");
3217
+ // Manually do what rubyx_await does, but with proper GIL management
3218
+ // for the test environment (can't call rubyx_await directly because
3219
+ // its ensure_gil/release_gil nests incorrectly with with_ruby_python's GIL)
3220
+ let future = crate::future::RubyxFuture::from_coroutine(coroutine, api);
3204
3221
 
3205
- let obj = magnus::typed_data::Obj::<RubyxObject>::try_convert(result)
3206
- .expect("should be RubyxObject");
3207
- assert_eq!(api.long_to_i64(obj.as_ptr()), 99);
3222
+ // Release GIL so the background thread can acquire it
3223
+ let tstate = api.save_thread();
3224
+ let result = test_future_value(&future).expect("await should succeed");
3225
+ drop(future);
3226
+ api.restore_thread(tstate);
3227
+
3228
+ assert_eq!(i64::try_convert(result).unwrap(), 99);
3208
3229
 
3209
3230
  api.decref(globals);
3210
3231
  });
@@ -3223,12 +3244,16 @@ mod tests {
3223
3244
  )
3224
3245
  .expect("should define async function");
3225
3246
 
3226
- let result = crate::eval::await_eval_with_globals("double(21)", globals, api)
3247
+ let future = crate::eval::await_eval_with_globals("double(21)", globals, api)
3227
3248
  .expect("await_eval should succeed");
3228
3249
 
3229
- let obj = magnus::typed_data::Obj::<RubyxObject>::try_convert(result)
3230
- .expect("should be RubyxObject");
3231
- assert_eq!(api.long_to_i64(obj.as_ptr()), 42);
3250
+ // Release GIL so background thread can run asyncio
3251
+ let tstate = api.save_thread();
3252
+ let result = test_future_value(&future).expect("future should resolve");
3253
+ drop(future);
3254
+ api.restore_thread(tstate);
3255
+
3256
+ assert_eq!(i64::try_convert(result).unwrap(), 42);
3232
3257
 
3233
3258
  api.decref(globals);
3234
3259
  });
@@ -3247,8 +3272,17 @@ mod tests {
3247
3272
  )
3248
3273
  .expect("should define async function");
3249
3274
 
3250
- let result = crate::eval::await_eval_with_globals("fail()", globals, api);
3251
- assert!(result.is_err(), "should propagate error");
3275
+ let future_result = crate::eval::await_eval_with_globals("fail()", globals, api);
3276
+ match future_result {
3277
+ Err(_) => {} // eval itself failed — error propagated
3278
+ Ok(future) => {
3279
+ // Eval succeeded but asyncio.run should fail
3280
+ let tstate = api.save_thread();
3281
+ let result = test_future_value(&future);
3282
+ api.restore_thread(tstate);
3283
+ assert!(result.is_err(), "should propagate error");
3284
+ }
3285
+ }
3252
3286
 
3253
3287
  api.decref(globals);
3254
3288
  });
@@ -3430,10 +3464,15 @@ mod tests {
3430
3464
  api.decref(py_a);
3431
3465
  api.decref(py_b);
3432
3466
 
3433
- let result = crate::eval::await_eval_with_globals("multiply(a, b)", globals, api)
3467
+ let future = crate::eval::await_eval_with_globals("multiply(a, b)", globals, api)
3434
3468
  .expect("await should succeed");
3435
- let obj = Obj::<RubyxObject>::try_convert(result).expect("should be RubyxObject");
3436
- assert_eq!(api.long_to_i64(obj.as_ptr()), 42);
3469
+
3470
+ let tstate = api.save_thread();
3471
+ let result = test_future_value(&future).expect("future should resolve");
3472
+ drop(future);
3473
+ api.restore_thread(tstate);
3474
+
3475
+ assert_eq!(i64::try_convert(result).unwrap(), 42);
3437
3476
 
3438
3477
  api.decref(globals);
3439
3478
  });
@@ -3460,13 +3499,16 @@ mod tests {
3460
3499
  api.decref(key);
3461
3500
  api.decref(py_who);
3462
3501
 
3463
- let result = crate::eval::await_eval_with_globals("greet(who)", globals, api)
3502
+ let future = crate::eval::await_eval_with_globals("greet(who)", globals, api)
3464
3503
  .expect("await should succeed");
3465
- let obj = Obj::<RubyxObject>::try_convert(result).expect("should be RubyxObject");
3466
- assert_eq!(
3467
- api.string_to_string(obj.as_ptr()),
3468
- Some("hi world".to_string())
3469
- );
3504
+
3505
+ let tstate = api.save_thread();
3506
+ let result = test_future_value(&future).expect("future should resolve");
3507
+ drop(future);
3508
+ api.restore_thread(tstate);
3509
+
3510
+ let s: String = TryConvert::try_convert(result).unwrap();
3511
+ assert_eq!(s, "hi world");
3470
3512
 
3471
3513
  api.decref(globals);
3472
3514
  });
@@ -3493,8 +3535,16 @@ mod tests {
3493
3535
  api.decref(key);
3494
3536
  api.decref(py_val);
3495
3537
 
3496
- let result = crate::eval::await_eval_with_globals("check(val)", globals, api);
3497
- assert!(result.is_err(), "should propagate ValueError");
3538
+ let future_result = crate::eval::await_eval_with_globals("check(val)", globals, api);
3539
+ match future_result {
3540
+ Err(_) => {} // eval itself failed
3541
+ Ok(future) => {
3542
+ let tstate = api.save_thread();
3543
+ let result = test_future_value(&future);
3544
+ api.restore_thread(tstate);
3545
+ assert!(result.is_err(), "should propagate ValueError");
3546
+ }
3547
+ }
3498
3548
 
3499
3549
  api.decref(globals);
3500
3550
  });
@@ -3537,7 +3587,7 @@ mod tests {
3537
3587
  // Release GIL so the background thread can acquire it
3538
3588
  let tstate = api.save_thread();
3539
3589
  let future = crate::future::RubyxFuture::from_coroutine(coroutine, api);
3540
- let result = future.value().expect("future should resolve");
3590
+ let result = test_future_value(&future).expect("future should resolve");
3541
3591
  drop(future);
3542
3592
  api.restore_thread(tstate);
3543
3593
 
@@ -3582,7 +3632,7 @@ mod tests {
3582
3632
 
3583
3633
  let tstate = api.save_thread();
3584
3634
  let future = crate::future::RubyxFuture::from_coroutine(coroutine, api);
3585
- let result = future.value().expect("future should resolve");
3635
+ let result = test_future_value(&future).expect("future should resolve");
3586
3636
  drop(future);
3587
3637
  api.restore_thread(tstate);
3588
3638
 
@@ -3626,7 +3676,7 @@ mod tests {
3626
3676
 
3627
3677
  let tstate = api.save_thread();
3628
3678
  let future = crate::future::RubyxFuture::from_coroutine(coroutine, api);
3629
- let result = future.value();
3679
+ let result = test_future_value(&future);
3630
3680
  drop(future);
3631
3681
  api.restore_thread(tstate);
3632
3682
 
@@ -1,3 +1,4 @@
1
+ use crate::gvl::{rb_thread_call_without_gvl, rb_thread_check_ints, recv_loop, ubf_cancel};
1
2
  use crate::pipe_notify::PipeNotify;
2
3
  use crate::ruby_helpers::runtime_error;
3
4
  use crate::stream::StreamItem;
@@ -9,37 +10,6 @@ use std::os::fd::RawFd;
9
10
  use std::sync::atomic::AtomicBool;
10
11
  use std::sync::Arc;
11
12
 
12
- extern "C" {
13
- /// Release the GVL, call `func(data1)`, then reacquire the GVL.
14
- ///
15
- /// While `func` runs, other Ruby threads can execute. Inside `func`,
16
- /// you must NOT call any Ruby C API or access any Ruby VALUE.
17
- ///
18
- /// If `ubf` is provided, Ruby may call it from another thread to
19
- /// interrupt `func` (e.g., on Thread#kill or signal delivery).
20
- ///
21
- /// # Safety
22
- ///
23
- /// - `func` must not touch Ruby objects (GVL is not held).
24
- /// - `data1` must remain valid for the duration of `func`.
25
- /// - `data2` must remain valid for the duration of `ubf` (if provided).
26
- fn rb_thread_call_without_gvl(
27
- func: unsafe extern "C" fn(*mut c_void) -> *mut c_void,
28
- data1: *mut c_void,
29
- ubf: Option<unsafe extern "C" fn(*mut c_void)>,
30
- data2: *mut c_void,
31
- ) -> *mut c_void;
32
-
33
- /// Check for pending Ruby interrupts (Thread#kill, signals, etc.).
34
- ///
35
- /// Must be called WITH the GVL held. If an interrupt is pending,
36
- /// this raises a Ruby exception (longjmp). Call this immediately
37
- /// after `rb_thread_call_without_gvl` returns to deliver any
38
- /// interrupts that arrived while the GVL was released.
39
- fn rb_thread_check_ints();
40
-
41
- }
42
-
43
13
  fn create_ruby_io(ruby: &Ruby, fd: RawFd) -> Result<Value, Error> {
44
14
  let io_class: Value = ruby.class_object().const_get("IO")?;
45
15
  io_class.funcall("for_fd", (fd, magnus::kwargs!("autoclose" => false)))
@@ -55,54 +25,11 @@ fn has_fiber_scheduler(ruby: &Ruby) -> bool {
55
25
  .unwrap_or_else(|_| ruby.qnil().as_value());
56
26
  !scheduler.is_nil()
57
27
  }
58
-
59
- unsafe extern "C" fn ubf_cancel(data: *mut c_void) {
60
- let cancel = &*(data as *const AtomicBool);
61
- cancel.store(true, std::sync::atomic::Ordering::Relaxed);
62
- }
63
-
64
- unsafe extern "C" fn recv_without_gvl_cb(args: *mut c_void) -> *mut c_void {
65
- let args = &mut *(args as *mut RecvArgs);
66
- loop {
67
- match args.receiver.try_recv() {
68
- Ok(item) => {
69
- args.result = Some(Ok(item));
70
- return std::ptr::null_mut();
71
- }
72
- Err(crossbeam_channel::TryRecvError::Empty) => {
73
- // Check the cancel flag relaxed already
74
- if args.cancel.load(std::sync::atomic::Ordering::Relaxed) {
75
- args.result = None;
76
- return std::ptr::null_mut();
77
- }
78
-
79
- // Wait for timeout
80
- match args
81
- .receiver
82
- .recv_timeout(std::time::Duration::from_millis(50))
83
- {
84
- Ok(item) => {
85
- args.result = Some(Ok(item));
86
- return std::ptr::null_mut();
87
- }
88
- Err(crossbeam_channel::RecvTimeoutError::Timeout) => {
89
- // continue to check cancel
90
- continue;
91
- }
92
- Err(crossbeam_channel::RecvTimeoutError::Disconnected) => {
93
- args.result = Some(Err(crossbeam_channel::RecvError));
94
- return std::ptr::null_mut();
95
- }
96
- }
97
- }
98
- Err(crossbeam_channel::TryRecvError::Disconnected) => {
99
- args.result = Some(Err(crossbeam_channel::RecvError));
100
- return std::ptr::null_mut();
101
- }
102
- }
103
- }
28
+ pub(crate) unsafe extern "C" fn recv_without_gvl_cb(args: *mut c_void) -> *mut c_void {
29
+ let args = &mut *(args as *mut crate::nonblocking_stream::RecvArgs);
30
+ args.result = recv_loop(&args.receiver, &args.cancel);
31
+ std::ptr::null_mut()
104
32
  }
105
-
106
33
  struct RecvArgs {
107
34
  receiver: Receiver<StreamItem>,
108
35
  result: Option<Result<StreamItem, crossbeam_channel::RecvError>>,
@@ -91,6 +91,14 @@ pub(crate) fn python_to_sendable(
91
91
  if py_val == api.py_false {
92
92
  return Ok(SendableValue::Bool(false));
93
93
  }
94
+ let has_dict = {
95
+ let name = std::ffi::CString::new("__dict__").unwrap();
96
+ api.object_has_attr_string(py_val, name.as_ptr()) != 0
97
+ };
98
+ if api.callable_check(py_val) == 0 && has_dict {
99
+ api.incref(py_val);
100
+ return Ok(SendableValue::PyObjectRef(py_val as usize));
101
+ }
94
102
  Err("Cannot convert Python value to Ruby".to_string())
95
103
  }
96
104
  pub(crate) fn ruby_to_python(
@@ -1183,13 +1191,13 @@ mod tests {
1183
1191
 
1184
1192
  #[test]
1185
1193
  #[serial]
1186
- fn test_to_ruby_errors_for_module() {
1194
+ fn test_to_ruby_wraps_module_as_rubyx_object() {
1187
1195
  with_ruby_python(|_ruby, api| {
1188
1196
  let module = api.import_module("os").expect("os should import");
1189
1197
  let wrapper = RubyxObject::new(module, api).unwrap();
1190
1198
  assert!(
1191
- wrapper.to_ruby().is_err(),
1192
- "module should not convert to Ruby"
1199
+ wrapper.to_ruby().is_ok(),
1200
+ "module should convert to RubyxObject via PyObjectRef"
1193
1201
  );
1194
1202
  drop(wrapper);
1195
1203
  api.decref(module);
@@ -1928,4 +1936,84 @@ mod tests {
1928
1936
  drop(w);
1929
1937
  api.decref(os);
1930
1938
  }
1939
+
1940
+ // ========== python_to_sendable: PyObjectRef fallback ==========
1941
+
1942
+ #[test]
1943
+ #[serial]
1944
+ fn test_python_to_sendable_module_returns_py_object_ref() {
1945
+ use crate::test_helpers::skip_if_no_python;
1946
+ let Some(guard) = skip_if_no_python() else {
1947
+ return;
1948
+ };
1949
+ let api = guard.api();
1950
+ let os = api.import_module("os").expect("os should import");
1951
+ let sendable = python_to_sendable(os, api).unwrap();
1952
+ match &sendable {
1953
+ SendableValue::PyObjectRef(addr) => {
1954
+ assert_eq!(*addr, os as usize);
1955
+ }
1956
+ other => panic!("expected PyObjectRef, got {other:?}"),
1957
+ }
1958
+ // Clean up: decref once for the sendable's incref, once for import_module
1959
+ api.decref(os);
1960
+ api.decref(os);
1961
+ }
1962
+
1963
+ #[test]
1964
+ #[serial]
1965
+ fn test_python_to_sendable_set_returns_err() {
1966
+ use crate::test_helpers::skip_if_no_python;
1967
+ let Some(guard) = skip_if_no_python() else {
1968
+ return;
1969
+ };
1970
+ let api = guard.api();
1971
+ // Sets don't have __dict__, so they return Err (not PyObjectRef)
1972
+ let globals = api.dict_new();
1973
+ let builtins = api
1974
+ .import_module("builtins")
1975
+ .expect("builtins should import");
1976
+ let key = api.string_from_str("__builtins__");
1977
+ api.dict_set_item(globals, key, builtins);
1978
+ api.decref(key);
1979
+ let result = api.run_string("{1, 2, 3}", 258, globals, globals);
1980
+ let py_set = result.expect("set eval should succeed");
1981
+ assert!(!py_set.is_null());
1982
+
1983
+ let sendable = python_to_sendable(py_set, api);
1984
+ assert!(sendable.is_err(), "set should return Err (no __dict__)");
1985
+ api.decref(py_set);
1986
+ api.decref(builtins);
1987
+ api.decref(globals);
1988
+ }
1989
+
1990
+ #[test]
1991
+ #[serial]
1992
+ fn test_python_to_sendable_primitives_not_py_object_ref() {
1993
+ use crate::test_helpers::skip_if_no_python;
1994
+ let Some(guard) = skip_if_no_python() else {
1995
+ return;
1996
+ };
1997
+ let api = guard.api();
1998
+
1999
+ // int → Integer, not PyObjectRef
2000
+ let py_int = api.long_from_i64(42);
2001
+ assert!(matches!(
2002
+ python_to_sendable(py_int, api),
2003
+ Ok(SendableValue::Integer(42))
2004
+ ));
2005
+
2006
+ // str → Str, not PyObjectRef
2007
+ let py_str = api.string_from_str("hello");
2008
+ assert!(matches!(
2009
+ python_to_sendable(py_str, api),
2010
+ Ok(SendableValue::Str(_))
2011
+ ));
2012
+
2013
+ // None → Nil, not PyObjectRef
2014
+ assert!(matches!(
2015
+ python_to_sendable(api.py_none, api),
2016
+ Ok(SendableValue::Nil)
2017
+ ));
2018
+ }
1931
2019
  }
@@ -1,7 +1,7 @@
1
1
  use crate::api;
2
2
  use crate::python_ffi::PyObject;
3
3
  use crate::ruby_helpers::runtime_error;
4
- use crate::rubyx_object::python_to_sendable;
4
+ use crate::rubyx_object::{python_to_sendable, RubyxObject};
5
5
  use crossbeam_channel::{bounded, unbounded, Receiver, Sender};
6
6
  use magnus::value::ReprValue;
7
7
  use magnus::{IntoValue, Value};
@@ -23,6 +23,7 @@ pub(crate) enum SendableValue {
23
23
  Bool(bool),
24
24
  List(Vec<SendableValue>),
25
25
  Dict(Vec<(SendableValue, SendableValue)>),
26
+ PyObjectRef(usize),
26
27
  }
27
28
  impl TryInto<magnus::Value> for SendableValue {
28
29
  type Error = magnus::Error;
@@ -54,6 +55,19 @@ impl TryInto<magnus::Value> for SendableValue {
54
55
  }
55
56
  hash.as_value()
56
57
  }
58
+ SendableValue::PyObjectRef(addr) => {
59
+ let py_obj = addr as *mut PyObject;
60
+ let api = crate::API.get().ok_or_else(|| {
61
+ magnus::Error::new(runtime_error(), "Python API not initialized")
62
+ })?;
63
+ // RubyxObject::new increfs internally and python_to_sendable also incref
64
+ let wrapper = RubyxObject::new(py_obj, api).ok_or_else(|| {
65
+ magnus::Error::new(runtime_error(), "Failed to wrap Python object")
66
+ })?;
67
+ // Balance the extra incref from python_to_sendable
68
+ api.decref(py_obj);
69
+ wrapper.into_value_with(&ruby)
70
+ }
57
71
  };
58
72
  Ok(result)
59
73
  }
@@ -710,4 +724,66 @@ mod tests {
710
724
  assert_eq!(sum, (0..1000i64).sum::<i64>());
711
725
  });
712
726
  }
727
+
728
+ // ========== PyObjectRef: SendableValue → RubyxObject ==========
729
+
730
+ #[test]
731
+ #[serial]
732
+ fn test_py_object_ref_converts_to_rubyx_object() {
733
+ with_ruby_python(|_ruby, api| {
734
+ let os = api.import_module("os").expect("os should import");
735
+ api.incref(os); // incref for PyObjectRef (simulates what python_to_sendable does)
736
+ let sendable = SendableValue::PyObjectRef(os as usize);
737
+
738
+ let val: Value = sendable.try_into().expect("PyObjectRef should convert");
739
+ // The result should be a RubyxObject, not a primitive
740
+ assert!(!val.is_nil());
741
+ // Verify it's not a primitive type
742
+ assert!(i64::try_convert(val).is_err(), "should not be an Integer");
743
+ assert!(String::try_convert(val).is_err(), "should not be a String");
744
+
745
+ api.decref(os); // balance the import_module refcount
746
+ });
747
+ }
748
+
749
+ #[test]
750
+ #[serial]
751
+ fn test_py_object_ref_in_stream() {
752
+ with_ruby_python(|_ruby, api| {
753
+ let os = api.import_module("os").expect("os should import");
754
+ api.incref(os);
755
+
756
+ let (tx, rx) = unbounded();
757
+ let (cancel_tx, _cancel_rx) = bounded(1);
758
+ let addr = os as usize;
759
+
760
+ thread::spawn(move || {
761
+ tx.send(Some(SendableValue::Integer(1))).ok();
762
+ tx.send(Some(SendableValue::PyObjectRef(addr))).ok();
763
+ tx.send(Some(SendableValue::Str("after".to_string()))).ok();
764
+ tx.send(None).ok();
765
+ });
766
+
767
+ let mut stream = AsyncStream::from_channel(rx, cancel_tx);
768
+
769
+ // First: integer
770
+ let val = stream.next().unwrap().unwrap();
771
+ assert_eq!(i64::try_convert(val).unwrap(), 1);
772
+
773
+ // Second: PyObjectRef → RubyxObject
774
+ let val = stream.next().unwrap().unwrap();
775
+ assert!(!val.is_nil());
776
+ assert!(
777
+ i64::try_convert(val).is_err(),
778
+ "should be RubyxObject, not Integer"
779
+ );
780
+
781
+ // Third: string
782
+ let val = stream.next().unwrap().unwrap();
783
+ assert_eq!(String::try_convert(val).unwrap(), "after");
784
+
785
+ assert!(stream.next().is_none());
786
+ api.decref(os);
787
+ });
788
+ }
713
789
  }
@@ -0,0 +1,5 @@
1
+ """Example Python module. Import with: Rubyx.import('example')"""
2
+
3
+
4
+ def hello(name="World"):
5
+ return f"Hello, {name}! From Python."
@@ -0,0 +1,5 @@
1
+ [project]
2
+ name = "my-rails-app"
3
+ version = "0.1.0"
4
+ requires-python = ">=3.12"
5
+ dependencies = []
data/lib/rubyx/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Rubyx
3
- VERSION = "0.1.0".freeze
3
+ VERSION = "0.2.0".freeze
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubyx-py
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Naiker
@@ -62,6 +62,7 @@ extra_rdoc_files: []
62
62
  files:
63
63
  - Cargo.toml
64
64
  - README.md
65
+ - docs/assets/logo.png
65
66
  - ext/rubyx/Cargo.toml
66
67
  - ext/rubyx/extconf.rb
67
68
  - ext/rubyx/src/async_gen.rs
@@ -70,6 +71,7 @@ files:
70
71
  - ext/rubyx/src/eval.rs
71
72
  - ext/rubyx/src/exception.rs
72
73
  - ext/rubyx/src/future.rs
74
+ - ext/rubyx/src/gvl.rs
73
75
  - ext/rubyx/src/import.rs
74
76
  - ext/rubyx/src/lib.rs
75
77
  - ext/rubyx/src/nonblocking_stream.rs
@@ -85,6 +87,8 @@ files:
85
87
  - ext/rubyx/src/stream.rs
86
88
  - ext/rubyx/src/test_helpers.rs
87
89
  - lib/generators/rubyx/install_generator.rb
90
+ - lib/generators/rubyx/templates/example.py
91
+ - lib/generators/rubyx/templates/pyproject.toml
88
92
  - lib/generators/rubyx/templates/rubyx_initializer.rb
89
93
  - lib/rubyx-py.rb
90
94
  - lib/rubyx.rb