mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 19:08:04 +00:00
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:
parent
9d35128840
commit
1c0e03f807
19 changed files with 201 additions and 163 deletions
|
@ -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()
|
||||
))
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue