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,18 @@
1
+ use std::ffi::c_void;
2
+
3
+ pub type PyObject = c_void;
4
+ #[allow(non_camel_case_types)]
5
+ pub type Py_ssize_t = isize;
6
+
7
+ #[repr(C)]
8
+ pub struct PyThreadState {
9
+ _private: [u8; 0],
10
+ }
11
+
12
+ #[repr(C)]
13
+ #[derive(Clone, Copy, Debug, PartialEq)]
14
+ #[allow(dead_code)]
15
+ pub enum PyGILState {
16
+ Locked = 0,
17
+ Unlocked = 1,
18
+ }
@@ -0,0 +1,119 @@
1
+ use std::path::PathBuf;
2
+ use std::process::Command;
3
+
4
+ pub fn find_libpython() -> Option<PathBuf> {
5
+ if let Some(path) = find_via_python_config() {
6
+ if path.exists() {
7
+ return Some(path);
8
+ }
9
+ }
10
+
11
+ common_paths().into_iter().find(|path| path.exists())
12
+ }
13
+
14
+ fn find_via_python_config() -> Option<PathBuf> {
15
+ let output = Command::new("python3")
16
+ .args([
17
+ "-c",
18
+ "import sysconfig; print(sysconfig.get_config_var('LIBDIR'))",
19
+ ])
20
+ .output()
21
+ .ok()?;
22
+
23
+ if !output.status.success() {
24
+ return None;
25
+ }
26
+
27
+ let libdir = String::from_utf8_lossy(&output.stdout).trim().to_string();
28
+ if libdir.is_empty() || libdir == "None" {
29
+ return None;
30
+ }
31
+
32
+ let version_output = Command::new("python3")
33
+ .args([
34
+ "-c",
35
+ "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')",
36
+ ])
37
+ .output()
38
+ .ok()?;
39
+
40
+ let version = String::from_utf8_lossy(&version_output.stdout)
41
+ .trim()
42
+ .to_string();
43
+
44
+ #[cfg(target_os = "macos")]
45
+ let lib_name = format!("libpython{}.dylib", version);
46
+ #[cfg(target_os = "linux")]
47
+ let lib_name = format!("libpython{}.so", version);
48
+ #[cfg(target_os = "windows")]
49
+ let lib_name = format!("python{}.dll", version.replace(".", ""));
50
+ #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
51
+ let lib_name = format!("libpython{}.so", version);
52
+
53
+ let path = PathBuf::from(libdir).join(&lib_name);
54
+ if path.exists() {
55
+ return Some(path);
56
+ }
57
+
58
+ #[cfg(target_os = "macos")]
59
+ {
60
+ if let Some(p) = find_macos_framework(&version) {
61
+ if p.exists() {
62
+ return Some(p);
63
+ }
64
+ }
65
+ }
66
+
67
+ Some(path)
68
+ }
69
+
70
+ #[cfg(target_os = "macos")]
71
+ fn find_macos_framework(version: &str) -> Option<PathBuf> {
72
+ let output = Command::new("python3")
73
+ .args(["-c", "import sys; print(sys.prefix)"])
74
+ .output()
75
+ .ok()?;
76
+
77
+ let prefix = String::from_utf8_lossy(&output.stdout).trim().to_string();
78
+
79
+ let framework_lib = PathBuf::from(&prefix)
80
+ .join("lib")
81
+ .join(format!("libpython{}.dylib", version));
82
+ if framework_lib.exists() {
83
+ return Some(framework_lib);
84
+ }
85
+
86
+ let python_dylib = PathBuf::from(&prefix).join("Python");
87
+ if python_dylib.exists() {
88
+ return Some(python_dylib);
89
+ }
90
+
91
+ None
92
+ }
93
+
94
+ fn common_paths() -> Vec<PathBuf> {
95
+ let mut paths = Vec::new();
96
+
97
+ #[cfg(target_os = "macos")]
98
+ for minor in (8..=15).rev() {
99
+ paths.push(PathBuf::from(format!(
100
+ "/opt/homebrew/opt/python@3.{}/Frameworks/Python.framework/Versions/3.{}/lib/libpython3.{}.dylib",
101
+ minor, minor, minor
102
+ )));
103
+ paths.push(PathBuf::from(format!(
104
+ "/opt/homebrew/opt/python@3.{}/Frameworks/Python.framework/Versions/3.{}/Python",
105
+ minor, minor
106
+ )));
107
+ }
108
+
109
+ #[cfg(target_os = "linux")]
110
+ for minor in (8..=15).rev() {
111
+ paths.push(PathBuf::from(format!(
112
+ "/usr/lib/x86_64-linux-gnu/libpython3.{}.so",
113
+ minor
114
+ )));
115
+ paths.push(PathBuf::from(format!("/usr/lib/libpython3.{}.so", minor)));
116
+ }
117
+
118
+ paths
119
+ }
@@ -0,0 +1,25 @@
1
+ use crate::python_api::PythonApi;
2
+ use crate::python_ffi::PyObject;
3
+
4
+ /// RAII guard that decrefs a PyObject when dropped.
5
+ pub(crate) struct PyGuard<'a> {
6
+ obj: *mut PyObject,
7
+ api: &'a PythonApi,
8
+ }
9
+ impl<'a> PyGuard<'a> {
10
+ pub(crate) fn new(obj: *mut PyObject, api: &'a PythonApi) -> Option<Self> {
11
+ if obj.is_null() {
12
+ None
13
+ } else {
14
+ Some(Self { obj, api })
15
+ }
16
+ }
17
+ pub(crate) fn ptr(&self) -> *mut PyObject {
18
+ self.obj
19
+ }
20
+ }
21
+ impl Drop for PyGuard<'_> {
22
+ fn drop(&mut self) {
23
+ self.api.decref(self.obj);
24
+ }
25
+ }
@@ -0,0 +1,74 @@
1
+ //! Many call sites (trait impls like `From<PythonException>`, deeply nested
2
+ //! helpers) don't have a `&Ruby` in scope and cannot easily thread one through.
3
+ //! These helpers centralise the `Ruby::get()` call so each caller can simply
4
+ //! write `runtime_error()` instead of repeating the boilerplate.
5
+ //!
6
+ //! # Panics
7
+ //!
8
+ //! Every function in this module calls `Ruby::get().expect(…)` and will panic
9
+ //! if invoked from a non-Ruby thread. This is intentional — it mirrors the
10
+ //! behaviour of the deprecated `magnus::exception::*()` functions, and all
11
+ //! code paths that reach these helpers originate from Ruby callbacks registered
12
+ //! via magnus, where `Ruby::get()` is guaranteed to succeed.
13
+ use magnus::{ExceptionClass, Module};
14
+
15
+ /// Returns Ruby's `RuntimeError` exception class.
16
+ pub(crate) fn runtime_error() -> ExceptionClass {
17
+ let ruby = magnus::Ruby::get().expect("must be called from Ruby thread");
18
+ ruby.exception_runtime_error()
19
+ }
20
+
21
+ /// Returns Ruby's `TypeError` exception class.
22
+ pub(crate) fn type_error() -> ExceptionClass {
23
+ let ruby = magnus::Ruby::get().expect("must be called from Ruby thread");
24
+ ruby.exception_type_error()
25
+ }
26
+
27
+ /// Returns Ruby's `ArgumentError` exception class.
28
+ pub(crate) fn arg_error() -> ExceptionClass {
29
+ let ruby = magnus::Ruby::get().expect("must be called from Ruby thread");
30
+ ruby.exception_arg_error()
31
+ }
32
+
33
+ /// Returns Ruby's `SyntaxError` exception class.
34
+ pub(crate) fn syntax_error() -> ExceptionClass {
35
+ let ruby = magnus::Ruby::get().expect("must be called from Ruby thread");
36
+ ruby.exception_syntax_error()
37
+ }
38
+
39
+ /// Returns Ruby's base `Exception` exception class.
40
+ pub(crate) fn exception() -> ExceptionClass {
41
+ let ruby = magnus::Ruby::get().expect("must be called from Ruby thread");
42
+ ruby.exception_exception()
43
+ }
44
+
45
+ /// Returns Ruby's `NoMethodError` exception class.
46
+ pub(crate) fn no_method_error() -> ExceptionClass {
47
+ let ruby = magnus::Ruby::get().expect("must be called from Ruby thread");
48
+ ruby.exception_no_method_error()
49
+ }
50
+
51
+ /// Look up a Rubyx error class by Python exception kind.
52
+ /// Falls back to `Rubyx::PythonError` for unrecognized kinds,
53
+ /// and `RuntimeError` if the Rubyx module isn't available.
54
+ pub(crate) fn rubyx_exception_class(kind: &str) -> ExceptionClass {
55
+ let ruby = magnus::Ruby::get().expect("must be called from Ruby thread");
56
+ let rubyx = match ruby.define_module("Rubyx") {
57
+ Ok(m) => m,
58
+ Err(_) => return ruby.exception_runtime_error(),
59
+ };
60
+
61
+ let class_name = match kind {
62
+ "KeyError" => "KeyError",
63
+ "IndexError" => "IndexError",
64
+ "ValueError" => "ValueError",
65
+ "AttributeError" => "AttributeError",
66
+ "TypeError" => "TypeError",
67
+ "ImportError" | "ModuleNotFoundError" => "ImportError",
68
+ _ => "PythonError",
69
+ };
70
+
71
+ rubyx
72
+ .const_get::<_, ExceptionClass>(class_name)
73
+ .unwrap_or_else(|_| ruby.exception_runtime_error())
74
+ }