rubyx-py 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +97 -28
- data/docs/assets/logo.png +0 -0
- data/ext/rubyx/src/context.rs +45 -14
- data/ext/rubyx/src/eval.rs +14 -60
- data/ext/rubyx/src/future.rs +534 -14
- data/ext/rubyx/src/gvl.rs +70 -0
- data/ext/rubyx/src/lib.rs +83 -32
- data/ext/rubyx/src/nonblocking_stream.rs +5 -78
- data/ext/rubyx/src/python_api.rs +1027 -0
- data/ext/rubyx/src/rubyx_object.rs +1159 -4
- data/ext/rubyx/src/rubyx_stream.rs +168 -0
- data/ext/rubyx/src/stream.rs +225 -3
- data/lib/rubyx/railtie.rb +2 -1
- data/lib/rubyx/tasks/rubyx.rake +127 -0
- data/lib/rubyx/version.rb +2 -2
- metadata +4 -1
|
@@ -73,6 +73,7 @@ mod tests {
|
|
|
73
73
|
use crate::stream::SendableValue;
|
|
74
74
|
use crate::test_helpers::{skip_if_no_python, with_ruby_python};
|
|
75
75
|
use crossbeam_channel::{bounded, unbounded};
|
|
76
|
+
use magnus::encoding::EncodingCapable;
|
|
76
77
|
use magnus::value::ReprValue;
|
|
77
78
|
use magnus::TryConvert;
|
|
78
79
|
use serial_test::serial;
|
|
@@ -947,4 +948,171 @@ mod tests {
|
|
|
947
948
|
assert_eq!(v, 99);
|
|
948
949
|
});
|
|
949
950
|
}
|
|
951
|
+
|
|
952
|
+
// ========== SendableValue::Bytes → Ruby String (ASCII-8BIT) ==========
|
|
953
|
+
|
|
954
|
+
#[test]
|
|
955
|
+
#[serial]
|
|
956
|
+
fn test_sendable_bytes_converts_to_ruby_string() {
|
|
957
|
+
with_ruby_python(|ruby, _api| {
|
|
958
|
+
let val: magnus::Value = SendableValue::Bytes(b"hello".to_vec())
|
|
959
|
+
.try_into()
|
|
960
|
+
.expect("Bytes conversion should succeed");
|
|
961
|
+
|
|
962
|
+
// Must be a Ruby String, not an Array of integers
|
|
963
|
+
let rstr = magnus::RString::try_convert(val).expect("should be a String");
|
|
964
|
+
let content = unsafe { rstr.as_slice() };
|
|
965
|
+
assert_eq!(content, b"hello");
|
|
966
|
+
|
|
967
|
+
// Verify encoding is ASCII-8BIT
|
|
968
|
+
let enc = rstr.enc_get();
|
|
969
|
+
let ascii_8bit = ruby
|
|
970
|
+
.find_encindex("ASCII-8BIT")
|
|
971
|
+
.expect("ASCII-8BIT must exist");
|
|
972
|
+
assert!(
|
|
973
|
+
enc == ascii_8bit,
|
|
974
|
+
"Bytes should produce ASCII-8BIT encoded String"
|
|
975
|
+
);
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
#[test]
|
|
980
|
+
#[serial]
|
|
981
|
+
fn test_sendable_bytes_empty() {
|
|
982
|
+
with_ruby_python(|ruby, _api| {
|
|
983
|
+
let val: magnus::Value = SendableValue::Bytes(Vec::new())
|
|
984
|
+
.try_into()
|
|
985
|
+
.expect("empty Bytes conversion should succeed");
|
|
986
|
+
|
|
987
|
+
let rstr = magnus::RString::try_convert(val).expect("should be a String");
|
|
988
|
+
let content = unsafe { rstr.as_slice() };
|
|
989
|
+
assert!(content.is_empty());
|
|
990
|
+
|
|
991
|
+
let enc = rstr.enc_get();
|
|
992
|
+
let ascii_8bit = ruby
|
|
993
|
+
.find_encindex("ASCII-8BIT")
|
|
994
|
+
.expect("ASCII-8BIT must exist");
|
|
995
|
+
assert!(enc == ascii_8bit, "encoding should be ASCII-8BIT");
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
#[test]
|
|
1000
|
+
#[serial]
|
|
1001
|
+
fn test_sendable_bytes_with_null_bytes() {
|
|
1002
|
+
with_ruby_python(|ruby, _api| {
|
|
1003
|
+
let data = vec![0x00, 0x01, 0xff, 0x00, 0xfe];
|
|
1004
|
+
let val: magnus::Value = SendableValue::Bytes(data.clone())
|
|
1005
|
+
.try_into()
|
|
1006
|
+
.expect("Bytes with NULs should convert");
|
|
1007
|
+
|
|
1008
|
+
let rstr = magnus::RString::try_convert(val).expect("should be a String");
|
|
1009
|
+
let content = unsafe { rstr.as_slice() };
|
|
1010
|
+
assert_eq!(content, data.as_slice());
|
|
1011
|
+
|
|
1012
|
+
let enc = rstr.enc_get();
|
|
1013
|
+
let ascii_8bit = ruby
|
|
1014
|
+
.find_encindex("ASCII-8BIT")
|
|
1015
|
+
.expect("ASCII-8BIT must exist");
|
|
1016
|
+
assert!(enc == ascii_8bit, "encoding should be ASCII-8BIT");
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
#[test]
|
|
1021
|
+
#[serial]
|
|
1022
|
+
fn test_sendable_bytes_all_256_values() {
|
|
1023
|
+
with_ruby_python(|ruby, _api| {
|
|
1024
|
+
let data: Vec<u8> = (0..=255).collect();
|
|
1025
|
+
let val: magnus::Value = SendableValue::Bytes(data.clone())
|
|
1026
|
+
.try_into()
|
|
1027
|
+
.expect("all byte values should convert");
|
|
1028
|
+
|
|
1029
|
+
let rstr = magnus::RString::try_convert(val).expect("should be a String");
|
|
1030
|
+
let content = unsafe { rstr.as_slice() };
|
|
1031
|
+
assert_eq!(content, data.as_slice());
|
|
1032
|
+
|
|
1033
|
+
let enc = rstr.enc_get();
|
|
1034
|
+
let ascii_8bit = ruby
|
|
1035
|
+
.find_encindex("ASCII-8BIT")
|
|
1036
|
+
.expect("ASCII-8BIT must exist");
|
|
1037
|
+
assert!(enc == ascii_8bit, "encoding should be ASCII-8BIT");
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
#[test]
|
|
1042
|
+
#[serial]
|
|
1043
|
+
fn test_sendable_bytes_is_not_array() {
|
|
1044
|
+
with_ruby_python(|_ruby, _api| {
|
|
1045
|
+
let val: magnus::Value = SendableValue::Bytes(b"test".to_vec())
|
|
1046
|
+
.try_into()
|
|
1047
|
+
.expect("Bytes conversion should succeed");
|
|
1048
|
+
|
|
1049
|
+
// Must NOT be an Array (Vec<u8>.into_value would produce Array<Integer>)
|
|
1050
|
+
assert!(
|
|
1051
|
+
magnus::RArray::try_convert(val).is_err(),
|
|
1052
|
+
"Bytes must not convert to Ruby Array"
|
|
1053
|
+
);
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
#[test]
|
|
1058
|
+
#[serial]
|
|
1059
|
+
fn test_sendable_bytes_in_stream_delivery() {
|
|
1060
|
+
with_ruby_python(|ruby, _api| {
|
|
1061
|
+
let (tx, rx) = unbounded();
|
|
1062
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
1063
|
+
|
|
1064
|
+
thread::spawn(move || {
|
|
1065
|
+
tx.send(Some(SendableValue::Integer(1))).ok();
|
|
1066
|
+
tx.send(Some(SendableValue::Bytes(b"\xde\xad".to_vec())))
|
|
1067
|
+
.ok();
|
|
1068
|
+
tx.send(Some(SendableValue::Str("after".to_string()))).ok();
|
|
1069
|
+
tx.send(None).ok();
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
let mut stream = crate::stream::AsyncStream::from_channel(rx, cancel_tx);
|
|
1073
|
+
|
|
1074
|
+
// First: integer
|
|
1075
|
+
let val = stream.next().unwrap().unwrap();
|
|
1076
|
+
assert_eq!(i64::try_convert(val).unwrap(), 1);
|
|
1077
|
+
|
|
1078
|
+
// Second: bytes → ASCII-8BIT String
|
|
1079
|
+
let val = stream.next().unwrap().unwrap();
|
|
1080
|
+
let rstr = magnus::RString::try_convert(val).expect("should be String");
|
|
1081
|
+
let content = unsafe { rstr.as_slice() };
|
|
1082
|
+
assert_eq!(content, b"\xde\xad");
|
|
1083
|
+
let enc = rstr.enc_get();
|
|
1084
|
+
let ascii_8bit = ruby
|
|
1085
|
+
.find_encindex("ASCII-8BIT")
|
|
1086
|
+
.expect("ASCII-8BIT must exist");
|
|
1087
|
+
assert!(enc == ascii_8bit, "encoding should be ASCII-8BIT");
|
|
1088
|
+
|
|
1089
|
+
// Third: regular string
|
|
1090
|
+
let val = stream.next().unwrap().unwrap();
|
|
1091
|
+
assert_eq!(String::try_convert(val).unwrap(), "after");
|
|
1092
|
+
|
|
1093
|
+
assert!(stream.next().is_none());
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
#[test]
|
|
1098
|
+
#[serial]
|
|
1099
|
+
fn test_sendable_bytes_large_payload() {
|
|
1100
|
+
with_ruby_python(|ruby, _api| {
|
|
1101
|
+
let data: Vec<u8> = (0..10_000).map(|i| (i % 256) as u8).collect();
|
|
1102
|
+
let val: magnus::Value = SendableValue::Bytes(data.clone())
|
|
1103
|
+
.try_into()
|
|
1104
|
+
.expect("large Bytes should convert");
|
|
1105
|
+
|
|
1106
|
+
let rstr = magnus::RString::try_convert(val).expect("should be a String");
|
|
1107
|
+
let content = unsafe { rstr.as_slice() };
|
|
1108
|
+
assert_eq!(content.len(), 10_000);
|
|
1109
|
+
assert_eq!(content, data.as_slice());
|
|
1110
|
+
|
|
1111
|
+
let enc = rstr.enc_get();
|
|
1112
|
+
let ascii_8bit = ruby
|
|
1113
|
+
.find_encindex("ASCII-8BIT")
|
|
1114
|
+
.expect("ASCII-8BIT must exist");
|
|
1115
|
+
assert!(enc == ascii_8bit, "encoding should be ASCII-8BIT");
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
950
1118
|
}
|
data/ext/rubyx/src/stream.rs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
use crate::api;
|
|
2
1
|
use crate::python_ffi::PyObject;
|
|
3
2
|
use crate::ruby_helpers::runtime_error;
|
|
4
|
-
use crate::rubyx_object::python_to_sendable;
|
|
3
|
+
use crate::rubyx_object::{python_to_sendable, RubyxObject};
|
|
4
|
+
use crate::{api, ruby_helpers};
|
|
5
5
|
use crossbeam_channel::{bounded, unbounded, Receiver, Sender};
|
|
6
|
+
use magnus::encoding::EncodingCapable;
|
|
6
7
|
use magnus::value::ReprValue;
|
|
7
8
|
use magnus::{IntoValue, Value};
|
|
8
9
|
use std::thread;
|
|
@@ -21,8 +22,11 @@ pub(crate) enum SendableValue {
|
|
|
21
22
|
Float(f64),
|
|
22
23
|
Str(String),
|
|
23
24
|
Bool(bool),
|
|
25
|
+
Bytes(Vec<u8>),
|
|
26
|
+
Set(Vec<SendableValue>),
|
|
24
27
|
List(Vec<SendableValue>),
|
|
25
28
|
Dict(Vec<(SendableValue, SendableValue)>),
|
|
29
|
+
PyObjectRef(usize),
|
|
26
30
|
}
|
|
27
31
|
impl TryInto<magnus::Value> for SendableValue {
|
|
28
32
|
type Error = magnus::Error;
|
|
@@ -37,7 +41,15 @@ impl TryInto<magnus::Value> for SendableValue {
|
|
|
37
41
|
SendableValue::Float(f) => f.into_value_with(&ruby),
|
|
38
42
|
SendableValue::Str(s) => s.as_str().into_value_with(&ruby),
|
|
39
43
|
SendableValue::Bool(b) => b.into_value_with(&ruby),
|
|
40
|
-
SendableValue::
|
|
44
|
+
SendableValue::Bytes(b) => {
|
|
45
|
+
let s = ruby.str_from_slice(&b);
|
|
46
|
+
s.enc_associate(ruby.find_encoding("ASCII-8BIT").ok_or(magnus::Error::new(
|
|
47
|
+
ruby_helpers::no_method_error(),
|
|
48
|
+
"No ASCII-8BIT",
|
|
49
|
+
))?)?;
|
|
50
|
+
s.as_value()
|
|
51
|
+
}
|
|
52
|
+
SendableValue::List(l) | SendableValue::Set(l) => {
|
|
41
53
|
let ruby_array = ruby.ary_new_capa(l.len());
|
|
42
54
|
for item in l {
|
|
43
55
|
let val: Value = item.try_into()?;
|
|
@@ -54,6 +66,19 @@ impl TryInto<magnus::Value> for SendableValue {
|
|
|
54
66
|
}
|
|
55
67
|
hash.as_value()
|
|
56
68
|
}
|
|
69
|
+
SendableValue::PyObjectRef(addr) => {
|
|
70
|
+
let py_obj = addr as *mut PyObject;
|
|
71
|
+
let api = crate::API.get().ok_or_else(|| {
|
|
72
|
+
magnus::Error::new(runtime_error(), "Python API not initialized")
|
|
73
|
+
})?;
|
|
74
|
+
// RubyxObject::new increfs internally and python_to_sendable also incref
|
|
75
|
+
let wrapper = RubyxObject::new(py_obj, api).ok_or_else(|| {
|
|
76
|
+
magnus::Error::new(runtime_error(), "Failed to wrap Python object")
|
|
77
|
+
})?;
|
|
78
|
+
// Balance the extra incref from python_to_sendable
|
|
79
|
+
api.decref(py_obj);
|
|
80
|
+
wrapper.into_value_with(&ruby)
|
|
81
|
+
}
|
|
57
82
|
};
|
|
58
83
|
Ok(result)
|
|
59
84
|
}
|
|
@@ -710,4 +735,201 @@ mod tests {
|
|
|
710
735
|
assert_eq!(sum, (0..1000i64).sum::<i64>());
|
|
711
736
|
});
|
|
712
737
|
}
|
|
738
|
+
|
|
739
|
+
// ========== PyObjectRef: SendableValue → RubyxObject ==========
|
|
740
|
+
|
|
741
|
+
#[test]
|
|
742
|
+
#[serial]
|
|
743
|
+
fn test_py_object_ref_converts_to_rubyx_object() {
|
|
744
|
+
with_ruby_python(|_ruby, api| {
|
|
745
|
+
let os = api.import_module("os").expect("os should import");
|
|
746
|
+
api.incref(os); // incref for PyObjectRef (simulates what python_to_sendable does)
|
|
747
|
+
let sendable = SendableValue::PyObjectRef(os as usize);
|
|
748
|
+
|
|
749
|
+
let val: Value = sendable.try_into().expect("PyObjectRef should convert");
|
|
750
|
+
// The result should be a RubyxObject, not a primitive
|
|
751
|
+
assert!(!val.is_nil());
|
|
752
|
+
// Verify it's not a primitive type
|
|
753
|
+
assert!(i64::try_convert(val).is_err(), "should not be an Integer");
|
|
754
|
+
assert!(String::try_convert(val).is_err(), "should not be a String");
|
|
755
|
+
|
|
756
|
+
api.decref(os); // balance the import_module refcount
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
#[test]
|
|
761
|
+
#[serial]
|
|
762
|
+
fn test_py_object_ref_in_stream() {
|
|
763
|
+
with_ruby_python(|_ruby, api| {
|
|
764
|
+
let os = api.import_module("os").expect("os should import");
|
|
765
|
+
api.incref(os);
|
|
766
|
+
|
|
767
|
+
let (tx, rx) = unbounded();
|
|
768
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
769
|
+
let addr = os as usize;
|
|
770
|
+
|
|
771
|
+
thread::spawn(move || {
|
|
772
|
+
tx.send(Some(SendableValue::Integer(1))).ok();
|
|
773
|
+
tx.send(Some(SendableValue::PyObjectRef(addr))).ok();
|
|
774
|
+
tx.send(Some(SendableValue::Str("after".to_string()))).ok();
|
|
775
|
+
tx.send(None).ok();
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
let mut stream = AsyncStream::from_channel(rx, cancel_tx);
|
|
779
|
+
|
|
780
|
+
// First: integer
|
|
781
|
+
let val = stream.next().unwrap().unwrap();
|
|
782
|
+
assert_eq!(i64::try_convert(val).unwrap(), 1);
|
|
783
|
+
|
|
784
|
+
// Second: PyObjectRef → RubyxObject
|
|
785
|
+
let val = stream.next().unwrap().unwrap();
|
|
786
|
+
assert!(!val.is_nil());
|
|
787
|
+
assert!(
|
|
788
|
+
i64::try_convert(val).is_err(),
|
|
789
|
+
"should be RubyxObject, not Integer"
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
// Third: string
|
|
793
|
+
let val = stream.next().unwrap().unwrap();
|
|
794
|
+
assert_eq!(String::try_convert(val).unwrap(), "after");
|
|
795
|
+
|
|
796
|
+
assert!(stream.next().is_none());
|
|
797
|
+
api.decref(os);
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// ========== Set: SendableValue::Set → Ruby Array ==========
|
|
802
|
+
|
|
803
|
+
#[test]
|
|
804
|
+
#[serial]
|
|
805
|
+
fn test_set_converts_to_ruby_array() {
|
|
806
|
+
with_ruby_python(|_ruby, _api| {
|
|
807
|
+
let (tx, rx) = unbounded();
|
|
808
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
809
|
+
|
|
810
|
+
thread::spawn(move || {
|
|
811
|
+
tx.send(Some(SendableValue::Set(vec![
|
|
812
|
+
SendableValue::Integer(1),
|
|
813
|
+
SendableValue::Integer(2),
|
|
814
|
+
SendableValue::Integer(3),
|
|
815
|
+
])))
|
|
816
|
+
.ok();
|
|
817
|
+
tx.send(None).ok();
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
let mut stream = AsyncStream::from_channel(rx, cancel_tx);
|
|
821
|
+
let val = stream.next().unwrap().unwrap();
|
|
822
|
+
|
|
823
|
+
let arr = magnus::RArray::try_convert(val).unwrap();
|
|
824
|
+
assert_eq!(arr.len(), 3);
|
|
825
|
+
|
|
826
|
+
let mut items: Vec<i64> = (0..arr.len())
|
|
827
|
+
.map(|i| i64::try_convert(arr.entry::<Value>(i as isize).unwrap()).unwrap())
|
|
828
|
+
.collect();
|
|
829
|
+
items.sort();
|
|
830
|
+
assert_eq!(items, vec![1, 2, 3]);
|
|
831
|
+
|
|
832
|
+
assert!(stream.next().is_none());
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
#[test]
|
|
837
|
+
#[serial]
|
|
838
|
+
fn test_empty_set_converts_to_empty_ruby_array() {
|
|
839
|
+
with_ruby_python(|_ruby, _api| {
|
|
840
|
+
let (tx, rx) = unbounded();
|
|
841
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
842
|
+
|
|
843
|
+
thread::spawn(move || {
|
|
844
|
+
tx.send(Some(SendableValue::Set(vec![]))).ok();
|
|
845
|
+
tx.send(None).ok();
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
let mut stream = AsyncStream::from_channel(rx, cancel_tx);
|
|
849
|
+
let val = stream.next().unwrap().unwrap();
|
|
850
|
+
|
|
851
|
+
let arr = magnus::RArray::try_convert(val).unwrap();
|
|
852
|
+
assert_eq!(arr.len(), 0);
|
|
853
|
+
|
|
854
|
+
assert!(stream.next().is_none());
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
#[test]
|
|
859
|
+
#[serial]
|
|
860
|
+
fn test_set_with_mixed_types_converts_to_ruby_array() {
|
|
861
|
+
with_ruby_python(|_ruby, _api| {
|
|
862
|
+
let (tx, rx) = unbounded();
|
|
863
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
864
|
+
|
|
865
|
+
thread::spawn(move || {
|
|
866
|
+
tx.send(Some(SendableValue::Set(vec![
|
|
867
|
+
SendableValue::Integer(42),
|
|
868
|
+
SendableValue::Str("hello".to_string()),
|
|
869
|
+
SendableValue::Float(3.14),
|
|
870
|
+
SendableValue::Bool(true),
|
|
871
|
+
])))
|
|
872
|
+
.ok();
|
|
873
|
+
tx.send(None).ok();
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
let mut stream = AsyncStream::from_channel(rx, cancel_tx);
|
|
877
|
+
let val = stream.next().unwrap().unwrap();
|
|
878
|
+
|
|
879
|
+
let arr = magnus::RArray::try_convert(val).unwrap();
|
|
880
|
+
assert_eq!(arr.len(), 4);
|
|
881
|
+
|
|
882
|
+
assert_eq!(
|
|
883
|
+
i64::try_convert(arr.entry::<Value>(0).unwrap()).unwrap(),
|
|
884
|
+
42
|
|
885
|
+
);
|
|
886
|
+
assert_eq!(
|
|
887
|
+
String::try_convert(arr.entry::<Value>(1).unwrap()).unwrap(),
|
|
888
|
+
"hello"
|
|
889
|
+
);
|
|
890
|
+
assert!(
|
|
891
|
+
(f64::try_convert(arr.entry::<Value>(2).unwrap()).unwrap() - 3.14).abs() < 1e-9
|
|
892
|
+
);
|
|
893
|
+
assert!(bool::try_convert(arr.entry::<Value>(3).unwrap()).unwrap());
|
|
894
|
+
|
|
895
|
+
assert!(stream.next().is_none());
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
#[test]
|
|
900
|
+
#[serial]
|
|
901
|
+
fn test_set_in_stream_with_other_types() {
|
|
902
|
+
with_ruby_python(|_ruby, _api| {
|
|
903
|
+
let (tx, rx) = unbounded();
|
|
904
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
905
|
+
|
|
906
|
+
thread::spawn(move || {
|
|
907
|
+
tx.send(Some(SendableValue::Integer(1))).ok();
|
|
908
|
+
tx.send(Some(SendableValue::Set(vec![
|
|
909
|
+
SendableValue::Integer(10),
|
|
910
|
+
SendableValue::Integer(20),
|
|
911
|
+
])))
|
|
912
|
+
.ok();
|
|
913
|
+
tx.send(Some(SendableValue::Str("after".to_string()))).ok();
|
|
914
|
+
tx.send(None).ok();
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
let mut stream = AsyncStream::from_channel(rx, cancel_tx);
|
|
918
|
+
|
|
919
|
+
// First: integer
|
|
920
|
+
let val = stream.next().unwrap().unwrap();
|
|
921
|
+
assert_eq!(i64::try_convert(val).unwrap(), 1);
|
|
922
|
+
|
|
923
|
+
// Second: set → array
|
|
924
|
+
let val = stream.next().unwrap().unwrap();
|
|
925
|
+
let arr = magnus::RArray::try_convert(val).unwrap();
|
|
926
|
+
assert_eq!(arr.len(), 2);
|
|
927
|
+
|
|
928
|
+
// Third: string
|
|
929
|
+
let val = stream.next().unwrap().unwrap();
|
|
930
|
+
assert_eq!(String::try_convert(val).unwrap(), "after");
|
|
931
|
+
|
|
932
|
+
assert!(stream.next().is_none());
|
|
933
|
+
});
|
|
934
|
+
}
|
|
713
935
|
}
|
data/lib/rubyx/railtie.rb
CHANGED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
namespace :rubyx do
|
|
2
|
+
desc 'Initialize Python environment (downloads uv and Python if needed)'
|
|
3
|
+
task init: :environment do
|
|
4
|
+
Rubyx::Rails.init!
|
|
5
|
+
puts '[Rubyx] Python environment initialized successfully.'
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
desc 'Check Python environment health'
|
|
9
|
+
task check: :environment do
|
|
10
|
+
puts 'Checking Python environment...'
|
|
11
|
+
puts
|
|
12
|
+
|
|
13
|
+
# Check uv
|
|
14
|
+
system_uv = `which uv 2>/dev/null`.strip
|
|
15
|
+
uv_available = !system_uv.empty? && File.exist?(system_uv)
|
|
16
|
+
puts "uv available: #{uv_available ? "Yes (#{system_uv})" : 'No (will auto-download)'}"
|
|
17
|
+
|
|
18
|
+
begin
|
|
19
|
+
Rubyx::Rails.ensure_initialized!
|
|
20
|
+
puts 'Python initialized: Yes'
|
|
21
|
+
rescue => e
|
|
22
|
+
puts "Python initialized: No (#{e.message})"
|
|
23
|
+
puts
|
|
24
|
+
puts 'Run `rake rubyx:init` to initialize.'
|
|
25
|
+
exit 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
Rubyx.eval('1 + 1')
|
|
30
|
+
puts 'Basic eval: OK'
|
|
31
|
+
rescue => e
|
|
32
|
+
puts "Basic eval: FAILED (#{e.message})"
|
|
33
|
+
exit 1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
begin
|
|
37
|
+
gen = Rubyx.eval("import sys\niter([sys.version.split()[0]])")
|
|
38
|
+
version = Rubyx.stream(gen).first
|
|
39
|
+
puts "Import sys: OK (Python #{version})"
|
|
40
|
+
rescue => e
|
|
41
|
+
puts "Import sys: FAILED (#{e.message})"
|
|
42
|
+
exit 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
puts
|
|
46
|
+
puts 'All checks passed!'
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
desc 'Show Rubyx configuration and status'
|
|
50
|
+
task status: :environment do
|
|
51
|
+
config = Rubyx::Rails.configuration
|
|
52
|
+
|
|
53
|
+
puts 'Rubyx Status'
|
|
54
|
+
puts '=' * 40
|
|
55
|
+
|
|
56
|
+
puts "Initialized: #{Rubyx::Rails.initialized?}"
|
|
57
|
+
puts
|
|
58
|
+
|
|
59
|
+
puts 'Configuration:'
|
|
60
|
+
puts " pyproject_path: #{config.pyproject_path || '(not set)'}"
|
|
61
|
+
puts " pyproject_content: #{config.pyproject_content ? '(inline, %d bytes)' % config.pyproject_content.length : '(not set)'}"
|
|
62
|
+
puts " auto_init: #{config.auto_init}"
|
|
63
|
+
puts " force_reinit: #{config.force_reinit}"
|
|
64
|
+
puts " uv_version: #{config.uv_version}"
|
|
65
|
+
puts " debug: #{config.debug}"
|
|
66
|
+
puts " python_paths: #{config.python_paths.inspect}"
|
|
67
|
+
puts " uv_path: #{config.uv_path || '(auto-download)'}"
|
|
68
|
+
puts " uv_args: #{config.uv_args.inspect}"
|
|
69
|
+
puts
|
|
70
|
+
|
|
71
|
+
if config.pyproject_path
|
|
72
|
+
exists = File.exist?(config.pyproject_path.to_s)
|
|
73
|
+
puts "pyproject.toml exists: #{exists}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
if config.pyproject_path
|
|
77
|
+
venv_dir = File.join(File.dirname(config.pyproject_path.to_s), '.venv')
|
|
78
|
+
puts ".venv exists: #{Dir.exist?(venv_dir)}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
system_uv = `which uv 2>/dev/null`.strip
|
|
82
|
+
puts "System uv: #{!system_uv.empty? ? system_uv : '(not found)'}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
desc 'List installed Python packages'
|
|
86
|
+
task packages: :environment do
|
|
87
|
+
Rubyx::Rails.ensure_initialized!
|
|
88
|
+
|
|
89
|
+
gen = Rubyx.eval(<<~PY)
|
|
90
|
+
import pkg_resources
|
|
91
|
+
packages = sorted([f"{d.project_name}=={d.version}" for d in pkg_resources.working_set])
|
|
92
|
+
iter(packages)
|
|
93
|
+
PY
|
|
94
|
+
|
|
95
|
+
puts 'Installed Python packages:'
|
|
96
|
+
Rubyx.stream(gen).each { |pkg| puts " #{pkg}" }
|
|
97
|
+
rescue => e
|
|
98
|
+
begin
|
|
99
|
+
gen = Rubyx.eval(<<~PY)
|
|
100
|
+
from importlib.metadata import distributions
|
|
101
|
+
packages = sorted([f"{d.metadata['Name']}=={d.metadata['Version']}" for d in distributions()])
|
|
102
|
+
iter(packages)
|
|
103
|
+
PY
|
|
104
|
+
|
|
105
|
+
puts 'Installed Python packages:'
|
|
106
|
+
Rubyx.stream(gen).each { |pkg| puts " #{pkg}" }
|
|
107
|
+
rescue => e2
|
|
108
|
+
puts "Could not list packages: #{e2.message}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
desc 'Clear the Rubyx cache (re-download uv + Python on next init)'
|
|
113
|
+
task clear_cache: :environment do
|
|
114
|
+
cache_dir = File.join(
|
|
115
|
+
ENV.fetch('XDG_CACHE_HOME', File.join(Dir.home, '.cache')),
|
|
116
|
+
'rubyx'
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if Dir.exist?(cache_dir)
|
|
120
|
+
require 'fileutils'
|
|
121
|
+
FileUtils.rm_rf(cache_dir)
|
|
122
|
+
puts "[Rubyx] Cache cleared: #{cache_dir}"
|
|
123
|
+
else
|
|
124
|
+
puts '[Rubyx] No cache directory found.'
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
data/lib/rubyx/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.2.1
|
|
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
|
|
@@ -94,6 +96,7 @@ files:
|
|
|
94
96
|
- lib/rubyx/error.rb
|
|
95
97
|
- lib/rubyx/rails.rb
|
|
96
98
|
- lib/rubyx/railtie.rb
|
|
99
|
+
- lib/rubyx/tasks/rubyx.rake
|
|
97
100
|
- lib/rubyx/uv.rb
|
|
98
101
|
- lib/rubyx/version.rb
|
|
99
102
|
homepage: https://github.com/yinho999/rubyx
|