puffin_interpreter cleanup ahead of #235 (#492)

Preparing for #235, some refactoring to `puffin_interpreter`.

* Added a dedicated error type instead of anyhow
* `InterpreterInfo` -> `Interpreter`
* `detect_virtual_env` now returns an option so it can be chained for
#235
This commit is contained in:
konsti 2023-11-23 09:57:33 +01:00 committed by GitHub
parent 9d35128840
commit 1c0e03f807
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 201 additions and 163 deletions

View file

@ -1,10 +1,8 @@
use std::io;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::UNIX_EPOCH;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::debug;
use pep440_rs::Version;
@ -12,10 +10,11 @@ use pep508_rs::MarkerEnvironment;
use platform_host::Platform;
use crate::python_platform::PythonPlatform;
use crate::Error;
/// A Python executable and its associated platform markers.
#[derive(Debug, Clone)]
pub struct InterpreterInfo {
pub struct Interpreter {
pub(crate) platform: PythonPlatform,
pub(crate) markers: MarkerEnvironment,
pub(crate) base_exec_prefix: PathBuf,
@ -23,9 +22,13 @@ pub struct InterpreterInfo {
pub(crate) sys_executable: PathBuf,
}
impl InterpreterInfo {
impl Interpreter {
/// Detect the interpreter info for the given Python executable.
pub fn query(executable: &Path, platform: Platform, cache: Option<&Path>) -> Result<Self> {
pub fn query(
executable: &Path,
platform: Platform,
cache: Option<&Path>,
) -> Result<Self, Error> {
let info = if let Some(cache) = cache {
InterpreterQueryResult::query_cached(executable, cache)?
} else {
@ -65,7 +68,7 @@ impl InterpreterInfo {
}
}
impl InterpreterInfo {
impl Interpreter {
/// Returns the path to the Python virtual environment.
pub fn platform(&self) -> &Platform {
&self.platform
@ -106,18 +109,6 @@ impl InterpreterInfo {
}
}
#[derive(Debug, Error)]
pub(crate) enum InterpreterQueryError {
#[error(transparent)]
IO(#[from] io::Error),
#[error("Failed to query python interpreter at {interpreter}")]
PythonSubcommand {
interpreter: PathBuf,
#[source]
err: io::Error,
},
}
#[derive(Deserialize, Serialize)]
pub(crate) struct InterpreterQueryResult {
pub(crate) markers: MarkerEnvironment,
@ -128,11 +119,11 @@ pub(crate) struct InterpreterQueryResult {
impl InterpreterQueryResult {
/// Return the resolved [`InterpreterQueryResult`] for the given Python executable.
pub(crate) fn query(interpreter: &Path) -> Result<Self, InterpreterQueryError> {
pub(crate) fn query(interpreter: &Path) -> Result<Self, Error> {
let output = Command::new(interpreter)
.args(["-c", include_str!("get_interpreter_info.py")])
.output()
.map_err(|err| InterpreterQueryError::PythonSubcommand {
.map_err(|err| Error::PythonSubcommandLaunch {
interpreter: interpreter.to_path_buf(),
err,
})?;
@ -140,35 +131,27 @@ impl InterpreterQueryResult {
// stderr isn't technically a criterion for success, but i don't know of any cases where there
// should be stderr output and if there is, we want to know
if !output.status.success() || !output.stderr.is_empty() {
return Err(InterpreterQueryError::PythonSubcommand {
interpreter: interpreter.to_path_buf(),
err: io::Error::new(
io::ErrorKind::Other,
format!(
"Querying python at {} failed with status {}:\n--- stdout:\n{}\n--- stderr:\n{}",
interpreter.display(),
output.status,
String::from_utf8_lossy(&output.stdout).trim(),
String::from_utf8_lossy(&output.stderr).trim()
),
return Err(Error::PythonSubcommandOutput {
message: format!(
"Querying python at {} failed with status {}",
interpreter.display(),
output.status,
),
stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(),
});
}
let data = serde_json::from_slice::<Self>(&output.stdout).map_err(|err|
InterpreterQueryError::PythonSubcommand {
interpreter: interpreter.to_path_buf(),
err: io::Error::new(
io::ErrorKind::Other,
format!(
"Querying python at {} did not return the expected data ({}):\n--- stdout:\n{}\n--- stderr:\n{}",
interpreter.display(),
err,
String::from_utf8_lossy(&output.stdout).trim(),
String::from_utf8_lossy(&output.stderr).trim()
),
let data = serde_json::from_slice::<Self>(&output.stdout).map_err(|err| {
Error::PythonSubcommandOutput {
message: format!(
"Querying python at {} did not return the expected data: {}",
interpreter.display(),
err,
),
stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(),
}
)?;
})?;
Ok(data)
}
@ -178,7 +161,7 @@ impl InterpreterQueryResult {
/// Running a Python script is (relatively) expensive, and the markers won't change
/// unless the Python executable changes, so we use the executable's last modified
/// time as a cache key.
pub(crate) fn query_cached(executable: &Path, cache: &Path) -> Result<Self> {
pub(crate) fn query_cached(executable: &Path, cache: &Path) -> Result<Self, Error> {
// Read from the cache.
let key = if let Ok(key) = cache_key(executable) {
if let Ok(data) = cacache::read_sync(cache, &key) {
@ -211,11 +194,15 @@ impl InterpreterQueryResult {
/// Create a cache key for the Python executable, consisting of the executable's
/// last modified time and the executable's path.
fn cache_key(executable: &Path) -> Result<String> {
let modified = executable
.metadata()?
fn cache_key(executable: &Path) -> Result<String, Error> {
let modified = fs_err::metadata(executable)?
// Note: This is infallible on windows and unix (i.e. all platforms we support)
.modified()?
.duration_since(std::time::UNIX_EPOCH)?
.as_millis();
Ok(format!("puffin:v0:{}:{}", executable.display(), modified))
.duration_since(UNIX_EPOCH)
.map_err(|err| Error::SystemTime(executable.to_path_buf(), err))?;
Ok(format!(
"puffin:v0:{}:{}",
executable.display(),
modified.as_millis()
))
}

View file

@ -1,6 +1,44 @@
pub use crate::interpreter_info::InterpreterInfo;
use std::io;
use std::path::PathBuf;
use std::time::SystemTimeError;
use thiserror::Error;
pub use crate::interpreter::Interpreter;
pub use crate::virtual_env::Virtualenv;
mod interpreter_info;
mod interpreter;
mod python_platform;
mod virtual_env;
#[derive(Debug, Error)]
pub enum Error {
#[error("Expected {0} to be a virtual environment, but pyvenv.cfg is missing")]
MissingPyVenvCfg(PathBuf),
#[error("Your virtualenv at {0} is broken. It contains a pyvenv.cfg but no python at {1}")]
BrokenVenv(PathBuf, PathBuf),
#[error("Both VIRTUAL_ENV and CONDA_PREFIX are set. Please unset one of them.")]
Conflict,
#[error("Couldn't find a virtualenv or conda environment (Looked for VIRTUAL_ENV, CONDA_PREFIX and .venv)")]
NotFound,
#[error(transparent)]
Io(#[from] io::Error),
#[error("Invalid modified date on {0}")]
SystemTime(PathBuf, #[source] SystemTimeError),
#[error("Failed to query python interpreter at {interpreter}")]
PythonSubcommandLaunch {
interpreter: PathBuf,
#[source]
err: io::Error,
},
#[error("{message}:\n--- stdout:\n{stdout}\n--- stderr:\n{stderr}\n---")]
PythonSubcommandOutput {
message: String,
stdout: String,
stderr: String,
},
#[error("Failed to write to cache")]
Cacache(#[from] cacache::Error),
#[error("Failed to write to cache")]
Serde(#[from] serde_json::Error),
}

View file

@ -1,52 +1,58 @@
use std::env;
use std::path::{Path, PathBuf};
use crate::InterpreterInfo;
use anyhow::{bail, Result};
use platform_host::Platform;
use tracing::debug;
use platform_host::Platform;
use crate::python_platform::PythonPlatform;
use crate::{Error, Interpreter};
/// A Python executable and its associated platform markers.
#[derive(Debug, Clone)]
pub struct Virtualenv {
root: PathBuf,
interpreter_info: InterpreterInfo,
interpreter: Interpreter,
}
impl Virtualenv {
/// Venv the current Python executable from the host environment.
pub fn from_env(platform: Platform, cache: Option<&Path>) -> Result<Self> {
pub fn from_env(platform: Platform, cache: Option<&Path>) -> Result<Self, Error> {
let platform = PythonPlatform::from(platform);
let venv = detect_virtual_env(&platform)?;
let Some(venv) = detect_virtual_env(&platform)? else {
return Err(Error::NotFound);
};
let executable = platform.venv_python(&venv);
let interpreter_info = InterpreterInfo::query(&executable, platform.0, cache)?;
let interpreter = Interpreter::query(&executable, platform.0, cache)?;
Ok(Self {
root: venv,
interpreter_info,
interpreter,
})
}
pub fn from_virtualenv(platform: Platform, root: &Path, cache: Option<&Path>) -> Result<Self> {
pub fn from_virtualenv(
platform: Platform,
root: &Path,
cache: Option<&Path>,
) -> Result<Self, Error> {
let platform = PythonPlatform::from(platform);
let executable = platform.venv_python(root);
let interpreter_info = InterpreterInfo::query(&executable, platform.0, cache)?;
let interpreter = Interpreter::query(&executable, platform.0, cache)?;
Ok(Self {
root: root.to_path_buf(),
interpreter_info,
interpreter,
})
}
/// Creating a new venv from a python interpreter changes this
pub fn new_prefix(venv: &Path, interpreter_info: &InterpreterInfo) -> Self {
pub fn new_prefix(venv: &Path, interpreter: &Interpreter) -> Self {
Self {
root: venv.to_path_buf(),
interpreter_info: InterpreterInfo {
interpreter: Interpreter {
base_prefix: venv.to_path_buf(),
..interpreter_info.clone()
..interpreter.clone()
},
}
}
@ -74,25 +80,37 @@ impl Virtualenv {
&self.root
}
pub fn interpreter_info(&self) -> &InterpreterInfo {
&self.interpreter_info
pub fn interpreter(&self) -> &Interpreter {
&self.interpreter
}
/// Returns the path to the `site-packages` directory inside a virtual environment.
pub fn site_packages(&self) -> PathBuf {
self.interpreter_info
self.interpreter
.platform
.venv_site_packages(&self.root, self.interpreter_info().simple_version())
.venv_site_packages(&self.root, self.interpreter().simple_version())
}
}
/// Locate the current virtual environment.
pub(crate) fn detect_virtual_env(target: &PythonPlatform) -> Result<PathBuf> {
pub(crate) fn detect_virtual_env(target: &PythonPlatform) -> Result<Option<PathBuf>, Error> {
match (env::var_os("VIRTUAL_ENV"), env::var_os("CONDA_PREFIX")) {
(Some(dir), None) => return Ok(PathBuf::from(dir)),
(None, Some(dir)) => return Ok(PathBuf::from(dir)),
(Some(dir), None) => {
debug!(
"Found a virtualenv through VIRTUAL_ENV at {}",
Path::new(&dir).display()
);
return Ok(Some(PathBuf::from(dir)));
}
(None, Some(dir)) => {
debug!(
"Found a virtualenv through CONDA_PREFIX at {}",
Path::new(&dir).display()
);
return Ok(Some(PathBuf::from(dir)));
}
(Some(_), Some(_)) => {
bail!("Both VIRTUAL_ENV and CONDA_PREFIX are set. Please unset one of them.")
return Err(Error::Conflict);
}
(None, None) => {
// No environment variables set. Try to find a virtualenv in the current directory.
@ -105,23 +123,16 @@ pub(crate) fn detect_virtual_env(target: &PythonPlatform) -> Result<PathBuf> {
let dot_venv = dir.join(".venv");
if dot_venv.is_dir() {
if !dot_venv.join("pyvenv.cfg").is_file() {
bail!(
"Expected {} to be a virtual environment, but pyvenv.cfg is missing",
dot_venv.display()
);
return Err(Error::MissingPyVenvCfg(dot_venv));
}
let python = target.venv_python(&dot_venv);
if !python.is_file() {
bail!(
"Your virtualenv at {} is broken. It contains a pyvenv.cfg but no python at {}",
dot_venv.display(),
python.display()
);
return Err(Error::BrokenVenv(dot_venv, python));
}
debug!("Found a virtualenv named .venv at {}", dot_venv.display());
return Ok(dot_venv);
return Ok(Some(dot_venv));
}
}
bail!("Couldn't find a virtualenv or conda environment.")
Ok(None)
}