Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
==> 2024-02-28 23:05:07 <==
# cmd: /Users/donjayamanne/Development/vsc/python-environment-tools/crates/pet-conda/tests/unix/conda_env_without_manager_but_found_in_history/some_other_location/conda_install/bin/conda create -n conda1
# cmd: /home/runner/work/python-environment-tools/python-environment-tools/crates/pet-conda/tests/unix/conda_env_without_manager_but_found_in_history/some_other_location/conda_install/bin/conda create -n conda1
# conda version: 23.11.0
==> 2024-02-28 23:08:59 <==
# cmd: /Users/donjayamanne/Development/vsc/python-environment-tools/crates/pet-conda/tests/unix/conda_env_without_manager_but_found_in_history/some_other_location/conda_install/bin/conda install -c conda-forge --name conda1 ipykernel -y
# cmd: /home/runner/work/python-environment-tools/python-environment-tools/crates/pet-conda/tests/unix/conda_env_without_manager_but_found_in_history/some_other_location/conda_install/bin/conda install -c conda-forge --name conda1 ipykernel -y
# conda version: 23.11.0
+conda-forge/noarch::appnope-0.1.4-pyhd8ed1ab_0
+conda-forge/noarch::asttokens-2.4.1-pyhd8ed1ab_0
Expand Down
1 change: 1 addition & 0 deletions crates/pet-core/src/python_environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub enum PythonEnvironmentKind {
LinuxGlobal,
MacXCode,
Venv,
VenvUv,
VirtualEnv,
VirtualEnvWrapper,
WindowsStore,
Expand Down
93 changes: 91 additions & 2 deletions crates/pet-core/src/pyvenv_cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub struct PyVenvCfg {
pub version_major: u64,
pub version_minor: u64,
pub prompt: Option<String>,
pub uv_version: Option<String>,
}

impl PyVenvCfg {
Expand All @@ -31,14 +32,19 @@ impl PyVenvCfg {
version_major: u64,
version_minor: u64,
prompt: Option<String>,
uv_version: Option<String>,
) -> Self {
Self {
version,
version_major,
version_minor,
prompt,
uv_version,
}
}
pub fn is_uv(&self) -> bool {
self.uv_version.is_some()
}
pub fn find(path: &Path) -> Option<Self> {
if let Some(ref file) = find(path) {
parse(file)
Expand Down Expand Up @@ -99,6 +105,7 @@ fn parse(file: &Path) -> Option<PyVenvCfg> {
let mut version_major: Option<u64> = None;
let mut version_minor: Option<u64> = None;
let mut prompt: Option<String> = None;
let mut uv_version: Option<String> = None;

for line in contents.lines() {
if version.is_none() {
Expand All @@ -120,13 +127,20 @@ fn parse(file: &Path) -> Option<PyVenvCfg> {
prompt = Some(p);
}
}
if version.is_some() && prompt.is_some() {
if uv_version.is_none() {
if let Some(uv_ver) = parse_uv_version(line) {
uv_version = Some(uv_ver);
}
}
if version.is_some() && prompt.is_some() && uv_version.is_some() {
break;
}
}

match (version, version_major, version_minor) {
(Some(ver), Some(major), Some(minor)) => Some(PyVenvCfg::new(ver, major, minor, prompt)),
(Some(ver), Some(major), Some(minor)) => {
Some(PyVenvCfg::new(ver, major, minor, prompt, uv_version))
}
_ => None,
}
}
Expand Down Expand Up @@ -177,3 +191,78 @@ fn parse_prompt(line: &str) -> Option<String> {
}
None
}

fn parse_uv_version(line: &str) -> Option<String> {
let trimmed = line.trim();
if trimmed.starts_with("uv") {
if let Some(eq_idx) = trimmed.find('=') {
let mut version = trimmed[eq_idx + 1..].trim().to_string();
// Strip any leading or trailing single or double quotes
if version.starts_with('"') {
version = version.trim_start_matches('"').to_string();
}
if version.ends_with('"') {
version = version.trim_end_matches('"').to_string();
}
if version.starts_with('\'') {
version = version.trim_start_matches('\'').to_string();
}
if version.ends_with('\'') {
version = version.trim_end_matches('\'').to_string();
}
if !version.is_empty() {
return Some(version);
}
}
}
None
}

#[cfg(test)]
mod tests {
use super::*;
use std::{fs, path::PathBuf};

#[test]
fn test_parse_uv_version() {
assert_eq!(parse_uv_version("uv = 0.8.14"), Some("0.8.14".to_string()));
assert_eq!(parse_uv_version("uv=0.8.14"), Some("0.8.14".to_string()));
assert_eq!(
parse_uv_version("uv = \"0.8.14\""),
Some("0.8.14".to_string())
);
assert_eq!(
parse_uv_version("uv = '0.8.14'"),
Some("0.8.14".to_string())
);
assert_eq!(parse_uv_version("version = 3.12.11"), None);
assert_eq!(parse_uv_version("prompt = test-env"), None);
}

#[test]
fn test_pyvenv_cfg_detects_uv() {
let temp_file = "/tmp/test_pyvenv_uv.cfg";
let contents = "home = /usr/bin/python3.12\nimplementation = CPython\nuv = 0.8.14\nversion_info = 3.12.11\ninclude-system-site-packages = false\nprompt = test-uv-env\n";
fs::write(temp_file, contents).unwrap();

let cfg = parse(&PathBuf::from(temp_file)).unwrap();
assert!(cfg.is_uv());
assert_eq!(cfg.uv_version, Some("0.8.14".to_string()));
assert_eq!(cfg.prompt, Some("test-uv-env".to_string()));

fs::remove_file(temp_file).ok();
}

#[test]
fn test_pyvenv_cfg_regular_venv() {
let temp_file = "/tmp/test_pyvenv_regular.cfg";
let contents = "home = /usr/bin/python3.12\ninclude-system-site-packages = false\nversion = 3.13.5\nexecutable = /usr/bin/python3.12\ncommand = python -m venv /path/to/env\n";
fs::write(temp_file, contents).unwrap();

let cfg = parse(&PathBuf::from(temp_file)).unwrap();
assert!(!cfg.is_uv());
assert_eq!(cfg.uv_version, None);

fs::remove_file(temp_file).ok();
}
}
137 changes: 134 additions & 3 deletions crates/pet-venv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,26 @@ pub fn is_venv(env: &PythonEnv) -> bool {
pub fn is_venv_dir(path: &Path) -> bool {
PyVenvCfg::find(path).is_some()
}

pub fn is_venv_uv(env: &PythonEnv) -> bool {
if let Some(cfg) = PyVenvCfg::find(env.executable.parent().unwrap_or(Path::new(""))) {
return cfg.is_uv();
}
if let Some(ref prefix) = env.prefix {
if let Some(cfg) = PyVenvCfg::find(prefix) {
return cfg.is_uv();
}
}
false
}

pub fn is_venv_uv_dir(path: &Path) -> bool {
if let Some(cfg) = PyVenvCfg::find(path) {
cfg.is_uv()
} else {
false
}
}
pub struct Venv {}

impl Venv {
Expand All @@ -43,7 +63,7 @@ impl Locator for Venv {
LocatorKind::Venv
}
fn supported_categories(&self) -> Vec<PythonEnvironmentKind> {
vec![PythonEnvironmentKind::Venv]
vec![PythonEnvironmentKind::Venv, PythonEnvironmentKind::VenvUv]
}

fn try_from(&self, env: &PythonEnv) -> Option<PythonEnvironment> {
Expand All @@ -67,10 +87,17 @@ impl Locator for Venv {
// Get the name from the prefix if it exists.
let cfg = PyVenvCfg::find(env.executable.parent()?)
.or_else(|| PyVenvCfg::find(&env.prefix.clone()?));
let name = cfg.and_then(|cfg| cfg.prompt);
let name = cfg.as_ref().and_then(|cfg| cfg.prompt.clone());

// Determine the environment kind based on whether it was created with UV
let kind = if cfg.as_ref().is_some_and(|c| c.is_uv()) {
PythonEnvironmentKind::VenvUv
} else {
PythonEnvironmentKind::Venv
};

Some(
PythonEnvironmentBuilder::new(Some(PythonEnvironmentKind::Venv))
PythonEnvironmentBuilder::new(Some(kind))
.name(name)
.executable(Some(env.executable.clone()))
.version(version)
Expand All @@ -88,3 +115,107 @@ impl Locator for Venv {
// We expect the user of this class to call `is_compatible`
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;

#[test]
fn test_is_venv_uv_dir_detects_uv_environment() {
use std::fs;
let test_dir = PathBuf::from("/tmp/test_uv_env_venv");
fs::create_dir_all(&test_dir).unwrap();
let pyvenv_cfg = test_dir.join("pyvenv.cfg");
let contents = "home = /usr/bin/python3.12\nimplementation = CPython\nuv = 0.8.14\nversion_info = 3.12.11\ninclude-system-site-packages = false\nprompt = test-uv-env\n";
fs::write(&pyvenv_cfg, contents).unwrap();

assert!(is_venv_uv_dir(&test_dir), "Should detect UV environment");

fs::remove_dir_all(&test_dir).ok();
}

#[test]
fn test_is_venv_uv_dir_does_not_detect_regular_environment() {
use std::fs;
let test_dir = PathBuf::from("/tmp/test_regular_env_venv");
fs::create_dir_all(&test_dir).unwrap();
let pyvenv_cfg = test_dir.join("pyvenv.cfg");
let contents = "home = /usr/bin/python3.12\ninclude-system-site-packages = false\nversion = 3.13.5\nexecutable = /usr/bin/python3.12\ncommand = python -m venv /path/to/env\n";
fs::write(&pyvenv_cfg, contents).unwrap();

assert!(
!is_venv_uv_dir(&test_dir),
"Should not detect regular venv as UV environment"
);

fs::remove_dir_all(&test_dir).ok();
}

#[test]
fn test_is_venv_uv_dir_handles_nonexistent_environment() {
let nonexistent_path = PathBuf::from("/tmp/nonexistent_env");
assert!(
!is_venv_uv_dir(&nonexistent_path),
"Should not detect non-existent environment as UV"
);
}

#[test]
fn test_venv_locator_detects_uv_kind() {
use pet_core::env::PythonEnv;
use std::fs;

// Create a test UV environment
let test_dir = PathBuf::from("/tmp/test_locator_uv");
let bin_dir = test_dir.join("bin");
fs::create_dir_all(&bin_dir).unwrap();

let pyvenv_cfg = test_dir.join("pyvenv.cfg");
let contents = "home = /usr/bin/python3.12\nimplementation = CPython\nuv = 0.8.14\nversion_info = 3.12.11\ninclude-system-site-packages = false\nprompt = test-uv-env\n";
fs::write(&pyvenv_cfg, contents).unwrap();

let python_exe = bin_dir.join("python");
fs::write(&python_exe, "").unwrap(); // Create dummy python executable

let env = PythonEnv::new(python_exe.clone(), Some(test_dir.clone()), Some("3.12.11".to_string()));
let locator = Venv::new();

if let Some(result) = locator.try_from(&env) {
assert_eq!(result.kind, Some(PythonEnvironmentKind::VenvUv), "UV environment should be detected as VenvUv");
} else {
panic!("Locator should detect UV environment");
}

fs::remove_dir_all(&test_dir).ok();
}

#[test]
fn test_venv_locator_detects_regular_venv_kind() {
use pet_core::env::PythonEnv;
use std::fs;

// Create a test regular venv environment
let test_dir = PathBuf::from("/tmp/test_locator_regular");
let bin_dir = test_dir.join("bin");
fs::create_dir_all(&bin_dir).unwrap();

let pyvenv_cfg = test_dir.join("pyvenv.cfg");
let contents = "home = /usr/bin/python3.12\ninclude-system-site-packages = false\nversion = 3.13.5\nexecutable = /usr/bin/python3.12\ncommand = python -m venv /path/to/env\n";
fs::write(&pyvenv_cfg, contents).unwrap();

let python_exe = bin_dir.join("python");
fs::write(&python_exe, "").unwrap(); // Create dummy python executable

let env = PythonEnv::new(python_exe.clone(), Some(test_dir.clone()), Some("3.13.5".to_string()));
let locator = Venv::new();

if let Some(result) = locator.try_from(&env) {
assert_eq!(result.kind, Some(PythonEnvironmentKind::Venv), "Regular venv should be detected as Venv");
} else {
panic!("Locator should detect regular venv environment");
}

fs::remove_dir_all(&test_dir).ok();
}
}
Loading