nautilus_core/python/
version.rs1#![expect(
19 clippy::manual_let_else,
20 reason = "Prefer explicit control flow for error handling"
21)]
22use pyo3::{Bound, prelude::*, types::PyTuple};
23
24#[must_use]
27pub fn get_python_version() -> String {
28 Python::attach(|py| {
29 let sys = match py.import("sys") {
30 Ok(mod_sys) => mod_sys,
31 Err(_) => return "Unavailable (failed to import sys)".to_string(),
32 };
33
34 let version_info = match sys.getattr("version_info") {
35 Ok(info) => info,
36 Err(_) => return "Unavailable (version_info not found)".to_string(),
37 };
38
39 let version_tuple: &Bound<'_, PyTuple> = match version_info.cast::<PyTuple>() {
40 Ok(tuple) => tuple,
41 Err(_) => return "Unavailable (failed to extract version_info)".to_string(),
42 };
43
44 let major = version_tuple
45 .get_item(0)
46 .ok()
47 .and_then(|item| item.extract::<i32>().ok())
48 .unwrap_or(-1);
49 let minor = version_tuple
50 .get_item(1)
51 .ok()
52 .and_then(|item| item.extract::<i32>().ok())
53 .unwrap_or(-1);
54 let micro = version_tuple
55 .get_item(2)
56 .ok()
57 .and_then(|item| item.extract::<i32>().ok())
58 .unwrap_or(-1);
59
60 if major == -1 || minor == -1 || micro == -1 {
61 "Unavailable (failed to extract version components)".to_string()
62 } else {
63 format!("{major}.{minor}.{micro}")
64 }
65 })
66}
67
68#[must_use]
69pub fn get_python_package_version(package_name: &str) -> String {
79 Python::attach(|py| match py.import(package_name) {
80 Ok(package) => match package.getattr("__version__") {
81 Ok(version_attr) => match version_attr.extract::<String>() {
82 Ok(version) => version,
83 Err(_) => "Unavailable (failed to extract version)".to_string(),
84 },
85 Err(_) => "Unavailable (__version__ attribute not found)".to_string(),
86 },
87 Err(_) => "Unavailable (failed to import package)".to_string(),
88 })
89}
90
91#[must_use]
97pub fn get_python_package_version_opt(package_name: &str) -> Option<String> {
98 Python::attach(|py| {
99 let package = py.import(package_name).ok()?;
100 let version_attr = package.getattr("__version__").ok()?;
101 version_attr.extract::<String>().ok()
102 })
103}
104
105#[cfg(test)]
106mod tests {
107 use rstest::rstest;
108
109 use super::*;
110
111 #[rstest]
112 fn test_get_python_version_handles_malformed_version_info() {
113 Python::initialize();
114 let version = Python::attach(|py| -> PyResult<String> {
115 let sys = py.import("sys")?;
116 let original = sys.getattr("version_info")?;
117 sys.setattr("version_info", "malformed")?;
118 let version = get_python_version();
119 sys.setattr("version_info", original)?;
120 Ok(version)
121 })
122 .expect("test Python setup should succeed");
123
124 assert_eq!(version, "Unavailable (failed to extract version_info)");
125 }
126
127 #[rstest]
128 fn test_get_python_package_version_opt_missing_returns_none() {
129 Python::initialize();
130 assert!(get_python_package_version_opt("nautilus_definitely_absent_pkg").is_none());
131 }
132
133 #[rstest]
134 fn test_get_python_package_version_opt_present_returns_some() {
135 Python::initialize();
136 let version = Python::attach(|py| {
137 let types = py.import("types").expect("import types");
138 let module = types
139 .call_method1("ModuleType", ("fake_pkg_xyz",))
140 .expect("create module");
141 module
142 .setattr("__version__", "9.9.9")
143 .expect("set __version__");
144 let modules = py
145 .import("sys")
146 .expect("import sys")
147 .getattr("modules")
148 .expect("sys.modules");
149 modules
150 .set_item("fake_pkg_xyz", &module)
151 .expect("register module");
152 get_python_package_version_opt("fake_pkg_xyz")
153 });
154
155 assert_eq!(version, Some("9.9.9".to_string()));
156 }
157}