add packaging module to djls-python (#2)

This commit is contained in:
Josh Thomas 2024-12-05 12:17:40 -06:00 committed by GitHub
parent 931c0bc9fb
commit 4c67f6a90d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 183 additions and 1 deletions

View file

@ -1,4 +1,5 @@
mod environment;
mod packaging;
mod python;
pub use environment::PythonEnvironment;

View file

@ -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<PathBuf>,
}
impl Package {
fn new(name: String, version: String, location: Option<PathBuf>) -> Self {
Self {
name,
version,
location,
}
}
pub fn name(&self) -> &String {
&self.name
}
pub fn version(&self) -> &String {
&self.version
}
pub fn location(&self) -> &Option<PathBuf> {
&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<String, Package>);
impl Packages {
fn new() -> Self {
Self(HashMap::new())
}
pub fn from_python(py: Python) -> PyResult<Self> {
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::<String>(),
dist.getattr("version")?.extract::<String>(),
) {
let location = match dist.call_method1("locate_file", ("",)) {
Ok(path) => path
.getattr("parent")?
.call_method0("as_posix")?
.extract::<String>()
.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<Self, PackagingError> {
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<T: IntoIterator<Item = (String, Package)>>(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),
}

View file

@ -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<PathBuf>,
packages: Packages,
}
impl Interpreter {
@ -145,6 +147,7 @@ impl Interpreter {
sys_base_prefix: PathBuf,
sys_executable: PathBuf,
sys_path: Vec<PathBuf>,
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<Self> {
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),
}