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