diff --git a/crates/djls-python/src/lib.rs b/crates/djls-python/src/lib.rs index 47fb78e..48cc62b 100644 --- a/crates/djls-python/src/lib.rs +++ b/crates/djls-python/src/lib.rs @@ -1,4 +1,5 @@ mod environment; +mod packaging; mod python; pub use environment::PythonEnvironment; diff --git a/crates/djls-python/src/packaging.rs b/crates/djls-python/src/packaging.rs new file mode 100644 index 0000000..8e8d284 --- /dev/null +++ b/crates/djls-python/src/packaging.rs @@ -0,0 +1,166 @@ +use pyo3::prelude::*; +use std::collections::HashMap; +use std::fmt; +use std::path::{Path, PathBuf}; +use std::process::Command; + +#[derive(Debug)] +pub struct Package { + name: String, + version: String, + location: Option, +} + +impl Package { + fn new(name: String, version: String, location: Option) -> Self { + Self { + name, + version, + location, + } + } + + pub fn name(&self) -> &String { + &self.name + } + + pub fn version(&self) -> &String { + &self.version + } + + pub fn location(&self) -> &Option { + &self.location + } +} + +impl fmt::Display for Package { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {}", self.name, self.version)?; + if let Some(location) = &self.location { + write!(f, " ({})", location.display())?; + } + Ok(()) + } +} + +#[derive(Debug)] +pub struct Packages(HashMap); + +impl Packages { + fn new() -> Self { + Self(HashMap::new()) + } + + pub fn from_python(py: Python) -> PyResult { + let importlib_metadata = py.import("importlib.metadata")?; + let distributions = importlib_metadata.call_method0("distributions")?; + + let mut packages = Packages::new(); + + for dist in (distributions.try_iter()?).flatten() { + if let Ok(metadata) = dist.getattr("metadata") { + if let (Ok(name), Ok(version)) = ( + metadata.get_item("Name")?.extract::(), + dist.getattr("version")?.extract::(), + ) { + let location = match dist.call_method1("locate_file", ("",)) { + Ok(path) => path + .getattr("parent")? + .call_method0("as_posix")? + .extract::() + .ok() + .map(PathBuf::from), + Err(_) => None, + }; + + packages + .0 + .insert(name.clone(), Package::new(name, version, location)); + } + } + } + + Ok(packages) + } + + pub fn from_executable(executable: &Path) -> Result { + let output = Command::new(executable) + .args([ + "-c", + r#" +import json +import importlib.metadata + +packages = {} +for dist in importlib.metadata.distributions(): + try: + packages[dist.metadata["Name"]] = { + "name": dist.metadata["Name"], + "version": dist.version, + "location": dist.locate_file("").parent.as_posix() if dist.locate_file("") else None + } + except Exception: + continue + +print(json.dumps(packages)) +"#, + ]) + .output()?; + + let output_str = String::from_utf8(output.stdout)?; + let packages_info: serde_json::Value = serde_json::from_str(&output_str)?; + + Ok(packages_info + .as_object() + .unwrap() + .iter() + .map(|(name, info)| { + ( + name.clone(), + Package { + name: name.clone(), + version: info["version"].as_str().unwrap().to_string(), + location: info["location"].as_str().map(PathBuf::from), + }, + ) + }) + .collect()) + } +} + +impl FromIterator<(String, Package)> for Packages { + fn from_iter>(iter: T) -> Self { + Self(HashMap::from_iter(iter)) + } +} + +impl fmt::Display for Packages { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut packages: Vec<_> = self.0.values().collect(); + packages.sort_by(|a, b| a.name.cmp(&b.name)); + + if packages.is_empty() { + writeln!(f, " (no packages installed)")?; + } else { + for package in packages { + writeln!(f, "{}", package)?; + } + } + Ok(()) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum PackagingError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("JSON parsing error: {0}")] + Json(#[from] serde_json::Error), + + #[error("Python error: {0}")] + Python(#[from] PyErr), + + #[error("UTF-8 conversion error: {0}")] + Utf8(#[from] std::string::FromUtf8Error), +} diff --git a/crates/djls-python/src/python.rs b/crates/djls-python/src/python.rs index dc1a15d..351bf1a 100644 --- a/crates/djls-python/src/python.rs +++ b/crates/djls-python/src/python.rs @@ -1,3 +1,4 @@ +use crate::packaging::{Packages, PackagingError}; use pyo3::prelude::*; use serde::Deserialize; use std::fmt; @@ -135,6 +136,7 @@ pub struct Interpreter { sys_base_prefix: PathBuf, sys_executable: PathBuf, sys_path: Vec, + packages: Packages, } impl Interpreter { @@ -145,6 +147,7 @@ impl Interpreter { sys_base_prefix: PathBuf, sys_executable: PathBuf, sys_path: Vec, + packages: Packages, ) -> Self { Self { version_info, @@ -153,6 +156,7 @@ impl Interpreter { sys_base_prefix, sys_executable, sys_path, + packages, } } @@ -180,6 +184,10 @@ impl Interpreter { &self.sys_path } + pub fn packages(&self) -> &Packages { + &self.packages + } + pub fn for_build(py: Python) -> PyResult { let sys = py.import("sys")?; @@ -194,6 +202,7 @@ impl Interpreter { .into_iter() .map(PathBuf::from) .collect(), + Packages::from_python(py)?, )) } @@ -228,6 +237,7 @@ print(json.dumps({ .iter() .map(|p| PathBuf::from(p.as_str().unwrap())) .collect(), + Packages::from_executable(executable)?, )) } } @@ -243,7 +253,9 @@ impl fmt::Display for Interpreter { writeln!(f, "{}", path.display())?; } writeln!(f, "Sysconfig Paths:")?; - write!(f, "{}", self.sysconfig_paths) + write!(f, "{}", self.sysconfig_paths)?; + writeln!(f, "\nInstalled Packages:")?; + write!(f, "{}", self.packages) } } @@ -258,6 +270,9 @@ pub enum PythonError { #[error("JSON parsing error: {0}")] Json(#[from] serde_json::Error), + #[error("Packaging error: {0}")] + Packaging(#[from] PackagingError), + #[error("Integer parsing error: {0}")] Parse(#[from] std::num::ParseIntError), }