mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-01 04:17:37 +00:00
Split virtual environment detection into a dedicated module (#3331)
Split out of https://github.com/astral-sh/uv/pull/3266
This commit is contained in:
parent
c28a2806b3
commit
49675558eb
9 changed files with 171 additions and 152 deletions
|
|
@ -3,12 +3,11 @@ use std::env;
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use same_file::is_same_file;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use uv_cache::Cache;
|
||||
use uv_fs::{LockedFile, Simplified};
|
||||
|
||||
use crate::environment::cfg::PyVenvConfiguration;
|
||||
use crate::virtualenv::{detect_virtualenv, virtualenv_python_executable, PyVenvConfiguration};
|
||||
use crate::{find_default_python, find_requested_python, Error, Interpreter, Target};
|
||||
|
||||
/// A Python environment, consisting of a Python [`Interpreter`] and its associated paths.
|
||||
|
|
@ -21,11 +20,11 @@ pub struct PythonEnvironment {
|
|||
impl PythonEnvironment {
|
||||
/// Create a [`PythonEnvironment`] for an existing virtual environment.
|
||||
pub fn from_virtualenv(cache: &Cache) -> Result<Self, Error> {
|
||||
let Some(venv) = detect_virtual_env()? else {
|
||||
let Some(venv) = detect_virtualenv()? else {
|
||||
return Err(Error::VenvNotFound);
|
||||
};
|
||||
let venv = fs_err::canonicalize(venv)?;
|
||||
let executable = detect_python_executable(&venv);
|
||||
let executable = virtualenv_python_executable(&venv);
|
||||
let interpreter = Interpreter::query(&executable, cache)?;
|
||||
|
||||
debug_assert!(
|
||||
|
|
@ -152,61 +151,3 @@ impl PythonEnvironment {
|
|||
self.interpreter
|
||||
}
|
||||
}
|
||||
|
||||
/// Locate the current virtual environment.
|
||||
pub(crate) fn detect_virtual_env() -> Result<Option<PathBuf>, Error> {
|
||||
if let Some(dir) = env::var_os("VIRTUAL_ENV").filter(|value| !value.is_empty()) {
|
||||
info!(
|
||||
"Found a virtualenv through VIRTUAL_ENV at: {}",
|
||||
Path::new(&dir).display()
|
||||
);
|
||||
return Ok(Some(PathBuf::from(dir)));
|
||||
}
|
||||
if let Some(dir) = env::var_os("CONDA_PREFIX").filter(|value| !value.is_empty()) {
|
||||
info!(
|
||||
"Found a virtualenv through CONDA_PREFIX at: {}",
|
||||
Path::new(&dir).display()
|
||||
);
|
||||
return Ok(Some(PathBuf::from(dir)));
|
||||
}
|
||||
|
||||
// Search for a `.venv` directory in the current or any parent directory.
|
||||
let current_dir = env::current_dir().expect("Failed to detect current directory");
|
||||
for dir in current_dir.ancestors() {
|
||||
let dot_venv = dir.join(".venv");
|
||||
if dot_venv.is_dir() {
|
||||
if !dot_venv.join("pyvenv.cfg").is_file() {
|
||||
return Err(Error::MissingPyVenvCfg(dot_venv));
|
||||
}
|
||||
debug!("Found a virtualenv named .venv at: {}", dot_venv.display());
|
||||
return Ok(Some(dot_venv));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Returns the path to the `python` executable inside a virtual environment.
|
||||
pub(crate) fn detect_python_executable(venv: impl AsRef<Path>) -> PathBuf {
|
||||
let venv = venv.as_ref();
|
||||
if cfg!(windows) {
|
||||
// Search for `python.exe` in the `Scripts` directory.
|
||||
let executable = venv.join("Scripts").join("python.exe");
|
||||
if executable.exists() {
|
||||
return executable;
|
||||
}
|
||||
|
||||
// Apparently, Python installed via msys2 on Windows _might_ produce a POSIX-like layout.
|
||||
// See: https://github.com/PyO3/maturin/issues/1108
|
||||
let executable = venv.join("bin").join("python.exe");
|
||||
if executable.exists() {
|
||||
return executable;
|
||||
}
|
||||
|
||||
// Fallback for Conda environments.
|
||||
venv.join("python.exe")
|
||||
} else {
|
||||
// Search for `python` in the `bin` directory.
|
||||
venv.join("bin").join("python")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
use std::path::Path;
|
||||
|
||||
use fs_err as fs;
|
||||
use thiserror::Error;
|
||||
|
||||
/// A parsed `pyvenv.cfg`
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PyVenvConfiguration {
|
||||
/// The version of the `virtualenv` package used to create the virtual environment, if any.
|
||||
pub(crate) virtualenv: bool,
|
||||
/// The version of the `uv` package used to create the virtual environment, if any.
|
||||
pub(crate) uv: bool,
|
||||
}
|
||||
|
||||
impl PyVenvConfiguration {
|
||||
/// Parse a `pyvenv.cfg` file into a [`PyVenvConfiguration`].
|
||||
pub fn parse(cfg: impl AsRef<Path>) -> Result<Self, Error> {
|
||||
let mut virtualenv = false;
|
||||
let mut uv = false;
|
||||
|
||||
// Per https://snarky.ca/how-virtual-environments-work/, the `pyvenv.cfg` file is not a
|
||||
// valid INI file, and is instead expected to be parsed by partitioning each line on the
|
||||
// first equals sign.
|
||||
let content = fs::read_to_string(&cfg)?;
|
||||
for line in content.lines() {
|
||||
let Some((key, _value)) = line.split_once('=') else {
|
||||
continue;
|
||||
};
|
||||
match key.trim() {
|
||||
"virtualenv" => {
|
||||
virtualenv = true;
|
||||
}
|
||||
"uv" => {
|
||||
uv = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self { virtualenv, uv })
|
||||
}
|
||||
|
||||
/// Returns true if the virtual environment was created with the `virtualenv` package.
|
||||
pub fn is_virtualenv(&self) -> bool {
|
||||
self.virtualenv
|
||||
}
|
||||
|
||||
/// Returns true if the virtual environment was created with the `uv` package.
|
||||
pub fn is_uv(&self) -> bool {
|
||||
self.uv
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
pub(crate) mod cfg;
|
||||
pub(crate) mod python_environment;
|
||||
pub(crate) mod virtualenv;
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use pypi_types::Scheme;
|
||||
|
||||
/// The layout of a virtual environment.
|
||||
#[derive(Debug)]
|
||||
pub struct Virtualenv {
|
||||
/// The absolute path to the root of the virtualenv, e.g., `/path/to/.venv`.
|
||||
pub root: PathBuf,
|
||||
|
||||
/// The path to the Python interpreter inside the virtualenv, e.g., `.venv/bin/python`
|
||||
/// (Unix, Python 3.11).
|
||||
pub executable: PathBuf,
|
||||
|
||||
/// The [`Scheme`] paths for the virtualenv, as returned by (e.g.) `sysconfig.get_paths()`.
|
||||
pub scheme: Scheme,
|
||||
}
|
||||
|
|
@ -8,9 +8,9 @@ use tracing::{debug, instrument};
|
|||
use uv_cache::Cache;
|
||||
use uv_warnings::warn_user_once;
|
||||
|
||||
use crate::environment::python_environment::{detect_python_executable, detect_virtual_env};
|
||||
use crate::interpreter::InterpreterInfoError;
|
||||
use crate::py_launcher::{py_list_paths, Error as PyLauncherError, PyListPath};
|
||||
use crate::virtualenv::{detect_virtualenv, virtualenv_python_executable};
|
||||
use crate::PythonVersion;
|
||||
use crate::{Error, Interpreter};
|
||||
|
||||
|
|
@ -506,8 +506,8 @@ fn find_version(
|
|||
|
||||
// Check if the venv Python matches.
|
||||
if !system {
|
||||
if let Some(venv) = detect_virtual_env()? {
|
||||
let executable = detect_python_executable(venv);
|
||||
if let Some(venv) = detect_virtualenv()? {
|
||||
let executable = virtualenv_python_executable(venv);
|
||||
let interpreter = Interpreter::query(executable, cache)?;
|
||||
|
||||
if version_matches(&interpreter) {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use pypi_types::Scheme;
|
|||
use uv_cache::{Cache, CacheBucket, CachedByTimestamp, Freshness, Timestamp};
|
||||
use uv_fs::{write_atomic_sync, PythonExt, Simplified};
|
||||
|
||||
use crate::{Error, PythonVersion, Target, Virtualenv};
|
||||
use crate::{Error, PythonVersion, Target, VirtualEnvironment};
|
||||
|
||||
/// A Python executable and its associated platform markers.
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -98,7 +98,7 @@ impl Interpreter {
|
|||
|
||||
/// Return a new [`Interpreter`] with the given virtual environment root.
|
||||
#[must_use]
|
||||
pub fn with_virtualenv(self, virtualenv: Virtualenv) -> Self {
|
||||
pub fn with_virtualenv(self, virtualenv: VirtualEnvironment) -> Self {
|
||||
Self {
|
||||
scheme: virtualenv.scheme,
|
||||
sys_executable: virtualenv.executable,
|
||||
|
|
|
|||
|
|
@ -14,14 +14,13 @@ use std::process::ExitStatus;
|
|||
|
||||
use thiserror::Error;
|
||||
|
||||
pub use crate::environment::cfg::PyVenvConfiguration;
|
||||
pub use crate::environment::python_environment::PythonEnvironment;
|
||||
pub use crate::environment::virtualenv::Virtualenv;
|
||||
pub use crate::environment::PythonEnvironment;
|
||||
pub use crate::find_python::{find_best_python, find_default_python, find_requested_python};
|
||||
pub use crate::interpreter::Interpreter;
|
||||
use crate::interpreter::InterpreterInfoError;
|
||||
pub use crate::python_version::PythonVersion;
|
||||
pub use crate::target::Target;
|
||||
pub use crate::virtualenv::{PyVenvConfiguration, VirtualEnvironment};
|
||||
|
||||
mod environment;
|
||||
mod find_python;
|
||||
|
|
@ -32,6 +31,7 @@ pub mod platform;
|
|||
mod py_launcher;
|
||||
mod python_version;
|
||||
mod target;
|
||||
mod virtualenv;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
|
|
@ -77,7 +77,7 @@ pub enum Error {
|
|||
#[error("Failed to write to cache")]
|
||||
Encode(#[from] rmp_serde::encode::Error),
|
||||
#[error("Broken virtualenv: Failed to parse pyvenv.cfg")]
|
||||
Cfg(#[from] environment::cfg::Error),
|
||||
Cfg(#[from] virtualenv::Error),
|
||||
#[error("Error finding `{}` in PATH", _0.to_string_lossy())]
|
||||
WhichError(OsString, #[source] which::Error),
|
||||
#[error("Can't use Python at `{interpreter}`")]
|
||||
|
|
|
|||
156
crates/uv-interpreter/src/virtualenv.rs
Normal file
156
crates/uv-interpreter/src/virtualenv.rs
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
use std::{
|
||||
env, io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use fs_err as fs;
|
||||
use pypi_types::Scheme;
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, info};
|
||||
|
||||
/// The layout of a virtual environment.
|
||||
#[derive(Debug)]
|
||||
pub struct VirtualEnvironment {
|
||||
/// The absolute path to the root of the virtualenv, e.g., `/path/to/.venv`.
|
||||
pub root: PathBuf,
|
||||
|
||||
/// The path to the Python interpreter inside the virtualenv, e.g., `.venv/bin/python`
|
||||
/// (Unix, Python 3.11).
|
||||
pub executable: PathBuf,
|
||||
|
||||
/// The [`Scheme`] paths for the virtualenv, as returned by (e.g.) `sysconfig.get_paths()`.
|
||||
pub scheme: Scheme,
|
||||
}
|
||||
|
||||
/// A parsed `pyvenv.cfg`
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PyVenvConfiguration {
|
||||
/// If the `virtualenv` package was used to create the virtual environment.
|
||||
pub(crate) virtualenv: bool,
|
||||
/// If the `uv` package was used to create the virtual environment.
|
||||
pub(crate) uv: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Broken virtualenv `{0}`: `pyvenv.cfg` is missing")]
|
||||
MissingPyVenvCfg(PathBuf),
|
||||
#[error("Broken virtualenv `{0}`: `pyvenv.cfg` could not be parsed")]
|
||||
ParsePyVenvCfg(PathBuf, #[source] io::Error),
|
||||
}
|
||||
|
||||
/// Locate the current virtual environment.
|
||||
pub(crate) fn detect_virtualenv() -> Result<Option<PathBuf>, Error> {
|
||||
let from_env = virtualenv_from_env();
|
||||
if from_env.is_some() {
|
||||
return Ok(from_env);
|
||||
}
|
||||
virtualenv_from_working_dir()
|
||||
}
|
||||
|
||||
/// Locate an active virtual environment by inspecting environment variables.
|
||||
///
|
||||
/// Supports `VIRTUAL_ENV` and `CONDA_PREFIX`.
|
||||
pub(crate) fn virtualenv_from_env() -> Option<PathBuf> {
|
||||
if let Some(dir) = env::var_os("VIRTUAL_ENV").filter(|value| !value.is_empty()) {
|
||||
info!(
|
||||
"Found a virtualenv through VIRTUAL_ENV at: {}",
|
||||
Path::new(&dir).display()
|
||||
);
|
||||
return Some(PathBuf::from(dir));
|
||||
}
|
||||
|
||||
if let Some(dir) = env::var_os("CONDA_PREFIX").filter(|value| !value.is_empty()) {
|
||||
info!(
|
||||
"Found a virtualenv through CONDA_PREFIX at: {}",
|
||||
Path::new(&dir).display()
|
||||
);
|
||||
return Some(PathBuf::from(dir));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Locate a virtual environment by searching the file system.
|
||||
///
|
||||
/// Finds a `.venv` directory in the current or any parent directory.
|
||||
pub(crate) fn virtualenv_from_working_dir() -> Result<Option<PathBuf>, Error> {
|
||||
let current_dir = env::current_dir().expect("Failed to detect current directory");
|
||||
for dir in current_dir.ancestors() {
|
||||
let dot_venv = dir.join(".venv");
|
||||
if dot_venv.is_dir() {
|
||||
if !dot_venv.join("pyvenv.cfg").is_file() {
|
||||
return Err(Error::MissingPyVenvCfg(dot_venv));
|
||||
}
|
||||
debug!("Found a virtualenv named .venv at: {}", dot_venv.display());
|
||||
return Ok(Some(dot_venv));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Returns the path to the `python` executable inside a virtual environment.
|
||||
pub(crate) fn virtualenv_python_executable(venv: impl AsRef<Path>) -> PathBuf {
|
||||
let venv = venv.as_ref();
|
||||
if cfg!(windows) {
|
||||
// Search for `python.exe` in the `Scripts` directory.
|
||||
let executable = venv.join("Scripts").join("python.exe");
|
||||
if executable.exists() {
|
||||
return executable;
|
||||
}
|
||||
|
||||
// Apparently, Python installed via msys2 on Windows _might_ produce a POSIX-like layout.
|
||||
// See: https://github.com/PyO3/maturin/issues/1108
|
||||
let executable = venv.join("bin").join("python.exe");
|
||||
if executable.exists() {
|
||||
return executable;
|
||||
}
|
||||
|
||||
// Fallback for Conda environments.
|
||||
venv.join("python.exe")
|
||||
} else {
|
||||
// Search for `python` in the `bin` directory.
|
||||
venv.join("bin").join("python")
|
||||
}
|
||||
}
|
||||
|
||||
impl PyVenvConfiguration {
|
||||
/// Parse a `pyvenv.cfg` file into a [`PyVenvConfiguration`].
|
||||
pub fn parse(cfg: impl AsRef<Path>) -> Result<Self, Error> {
|
||||
let mut virtualenv = false;
|
||||
let mut uv = false;
|
||||
|
||||
// Per https://snarky.ca/how-virtual-environments-work/, the `pyvenv.cfg` file is not a
|
||||
// valid INI file, and is instead expected to be parsed by partitioning each line on the
|
||||
// first equals sign.
|
||||
let content = fs::read_to_string(&cfg)
|
||||
.map_err(|err| Error::ParsePyVenvCfg(cfg.as_ref().to_path_buf(), err))?;
|
||||
for line in content.lines() {
|
||||
let Some((key, _value)) = line.split_once('=') else {
|
||||
continue;
|
||||
};
|
||||
match key.trim() {
|
||||
"virtualenv" => {
|
||||
virtualenv = true;
|
||||
}
|
||||
"uv" => {
|
||||
uv = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self { virtualenv, uv })
|
||||
}
|
||||
|
||||
/// Returns true if the virtual environment was created with the `virtualenv` package.
|
||||
pub fn is_virtualenv(&self) -> bool {
|
||||
self.virtualenv
|
||||
}
|
||||
|
||||
/// Returns true if the virtual environment was created with the `uv` package.
|
||||
pub fn is_uv(&self) -> bool {
|
||||
self.uv
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ use tracing::info;
|
|||
|
||||
use crate::{Error, Prompt};
|
||||
use uv_fs::{cachedir, Simplified};
|
||||
use uv_interpreter::{Interpreter, Virtualenv};
|
||||
use uv_interpreter::{Interpreter, VirtualEnvironment};
|
||||
use uv_version::version;
|
||||
|
||||
/// The bash activate scripts with the venv dependent paths patches out
|
||||
|
|
@ -48,7 +48,7 @@ pub fn create_bare_venv(
|
|||
prompt: Prompt,
|
||||
system_site_packages: bool,
|
||||
force: bool,
|
||||
) -> Result<Virtualenv, Error> {
|
||||
) -> Result<VirtualEnvironment, Error> {
|
||||
// Determine the base Python executable; that is, the Python executable that should be
|
||||
// considered the "base" for the virtual environment. This is typically the Python executable
|
||||
// from the [`Interpreter`]; however, if the interpreter is a virtual environment itself, then
|
||||
|
|
@ -258,7 +258,7 @@ pub fn create_bare_venv(
|
|||
fs::write(site_packages.join("_virtualenv.py"), VIRTUALENV_PATCH)?;
|
||||
fs::write(site_packages.join("_virtualenv.pth"), "import _virtualenv")?;
|
||||
|
||||
Ok(Virtualenv {
|
||||
Ok(VirtualEnvironment {
|
||||
scheme: Scheme {
|
||||
purelib: location.join(&interpreter.virtualenv().purelib),
|
||||
platlib: location.join(&interpreter.virtualenv().platlib),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue