From 5ee751715083fbc0f38a013f1dd7a6253eddf27d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Sep 2025 16:17:11 +0000 Subject: [PATCH 1/6] Initial plan From 66d95912ed6dcb0297e97496d4d4e5c863cc7bc3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Sep 2025 16:30:35 +0000 Subject: [PATCH 2/6] Implement venv-uv environment type for UV-created virtual environments Co-authored-by: karthiknadig <3840081+karthiknadig@users.noreply.github.com> --- Cargo.lock | 13 +- .../env_python_3/conda-meta/history | 4 +- crates/pet-core/src/lib.rs | 1 + crates/pet-core/src/python_environment.rs | 1 + crates/pet-core/src/pyvenv_cfg.rs | 85 +++++++++++- crates/pet-venv-uv/Cargo.toml | 13 ++ crates/pet-venv-uv/src/lib.rs | 130 ++++++++++++++++++ crates/pet/Cargo.toml | 1 + crates/pet/src/locators.rs | 4 +- 9 files changed, 246 insertions(+), 6 deletions(-) create mode 100644 crates/pet-venv-uv/Cargo.toml create mode 100644 crates/pet-venv-uv/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 3e0837f9..8081eb70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ahash" @@ -361,6 +361,7 @@ dependencies = [ "pet-reporter", "pet-telemetry", "pet-venv", + "pet-venv-uv", "pet-virtualenv", "pet-virtualenvwrapper", "pet-windows-registry", @@ -628,6 +629,16 @@ dependencies = [ "pet-virtualenv", ] +[[package]] +name = "pet-venv-uv" +version = "0.1.0" +dependencies = [ + "log", + "msvc_spectre_libs", + "pet-core", + "pet-python-utils", +] + [[package]] name = "pet-virtualenv" version = "0.1.0" diff --git a/crates/pet-conda/tests/unix/conda_env_without_manager_but_found_in_history/env_python_3/conda-meta/history b/crates/pet-conda/tests/unix/conda_env_without_manager_but_found_in_history/env_python_3/conda-meta/history index 198ee3a3..fc09724d 100644 --- a/crates/pet-conda/tests/unix/conda_env_without_manager_but_found_in_history/env_python_3/conda-meta/history +++ b/crates/pet-conda/tests/unix/conda_env_without_manager_but_found_in_history/env_python_3/conda-meta/history @@ -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 diff --git a/crates/pet-core/src/lib.rs b/crates/pet-core/src/lib.rs index 8d123469..2bc80adb 100644 --- a/crates/pet-core/src/lib.rs +++ b/crates/pet-core/src/lib.rs @@ -51,6 +51,7 @@ pub enum LocatorKind { Poetry, PyEnv, Venv, + VenvUv, VirtualEnv, VirtualEnvWrapper, WindowsRegistry, diff --git a/crates/pet-core/src/python_environment.rs b/crates/pet-core/src/python_environment.rs index a611ced8..1d98e198 100644 --- a/crates/pet-core/src/python_environment.rs +++ b/crates/pet-core/src/python_environment.rs @@ -24,6 +24,7 @@ pub enum PythonEnvironmentKind { LinuxGlobal, MacXCode, Venv, + VenvUv, VirtualEnv, VirtualEnvWrapper, WindowsStore, diff --git a/crates/pet-core/src/pyvenv_cfg.rs b/crates/pet-core/src/pyvenv_cfg.rs index 1dd454a5..90377205 100644 --- a/crates/pet-core/src/pyvenv_cfg.rs +++ b/crates/pet-core/src/pyvenv_cfg.rs @@ -23,6 +23,7 @@ pub struct PyVenvCfg { pub version_major: u64, pub version_minor: u64, pub prompt: Option, + pub uv_version: Option, } impl PyVenvCfg { @@ -31,14 +32,19 @@ impl PyVenvCfg { version_major: u64, version_minor: u64, prompt: Option, + uv_version: Option, ) -> 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 { if let Some(ref file) = find(path) { parse(file) @@ -99,6 +105,7 @@ fn parse(file: &Path) -> Option { let mut version_major: Option = None; let mut version_minor: Option = None; let mut prompt: Option = None; + let mut uv_version: Option = None; for line in contents.lines() { if version.is_none() { @@ -120,13 +127,18 @@ fn parse(file: &Path) -> Option { 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, } } @@ -177,3 +189,72 @@ fn parse_prompt(line: &str) -> Option { } None } + +fn parse_uv_version(line: &str) -> Option { + 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::{path::PathBuf, fs}; + + #[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(); + } +} diff --git a/crates/pet-venv-uv/Cargo.toml b/crates/pet-venv-uv/Cargo.toml new file mode 100644 index 00000000..98bae6cf --- /dev/null +++ b/crates/pet-venv-uv/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pet-venv-uv" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[target.'cfg(target_os = "windows")'.dependencies] +msvc_spectre_libs = { version = "0.1.1", features = ["error"] } + +[dependencies] +pet-core = { path = "../pet-core" } +pet-python-utils = { path = "../pet-python-utils" } +log = "0.4.21" \ No newline at end of file diff --git a/crates/pet-venv-uv/src/lib.rs b/crates/pet-venv-uv/src/lib.rs new file mode 100644 index 00000000..4bca0c27 --- /dev/null +++ b/crates/pet-venv-uv/src/lib.rs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::path::Path; + +use pet_core::{ + env::PythonEnv, + python_environment::{PythonEnvironment, PythonEnvironmentBuilder, PythonEnvironmentKind}, + pyvenv_cfg::PyVenvCfg, + reporter::Reporter, + Locator, LocatorKind, +}; +use pet_python_utils::executable::find_executables; +use pet_python_utils::version; + +fn is_venv_uv_internal(env: &PythonEnv) -> Option { + // Check if there's a pyvenv.cfg file with uv entry + if let Some(cfg) = PyVenvCfg::find(env.executable.parent()?) { + return Some(cfg.is_uv()); + } + if let Some(cfg) = PyVenvCfg::find(&env.prefix.clone()?) { + return Some(cfg.is_uv()); + } + Some(false) +} + +pub fn is_venv_uv(env: &PythonEnv) -> bool { + is_venv_uv_internal(env).unwrap_or_default() +} + +pub fn is_venv_uv_dir(path: &Path) -> bool { + if let Some(cfg) = PyVenvCfg::find(path) { + cfg.is_uv() + } else { + false + } +} + +pub struct VenvUv {} + +impl VenvUv { + pub fn new() -> VenvUv { + VenvUv {} + } +} + +impl Default for VenvUv { + fn default() -> Self { + Self::new() + } +} + +impl Locator for VenvUv { + fn get_kind(&self) -> LocatorKind { + LocatorKind::VenvUv + } + + fn supported_categories(&self) -> Vec { + vec![PythonEnvironmentKind::VenvUv] + } + + fn try_from(&self, env: &PythonEnv) -> Option { + if is_venv_uv(env) { + let mut prefix = env.prefix.clone(); + if prefix.is_none() { + prefix = Some(env.executable.parent()?.parent()?.to_path_buf()); + } + let version = match env.version { + Some(ref v) => Some(v.clone()), + None => match &prefix { + Some(prefix) => version::from_creator_for_virtual_env(prefix), + None => None, + }, + }; + let mut symlinks = vec![]; + if let Some(ref prefix) = prefix { + symlinks.append(&mut find_executables(prefix)); + } + + // 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); + + Some( + PythonEnvironmentBuilder::new(Some(PythonEnvironmentKind::VenvUv)) + .name(name) + .executable(Some(env.executable.clone())) + .version(version) + .prefix(prefix) + .symlinks(Some(symlinks)) + .build(), + ) + } else { + None + } + } + + fn find(&self, _reporter: &dyn Reporter) { + // There are no common global locations for uv virtual environments. + // 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() { + // This test checks if we can detect a UV environment from pyvenv.cfg + let uv_env_path = PathBuf::from("/tmp/test_uv_env"); + assert!(is_venv_uv_dir(&uv_env_path), "Should detect UV environment"); + } + + #[test] + fn test_is_venv_uv_dir_does_not_detect_regular_environment() { + // This test checks if we can properly ignore regular venv environments + let regular_env_path = PathBuf::from("/tmp/test_regular_env"); + assert!(!is_venv_uv_dir(®ular_env_path), "Should not detect regular venv as UV environment"); + } + + #[test] + fn test_is_venv_uv_dir_handles_nonexistent_environment() { + // This test checks if we handle non-existent environments gracefully + let nonexistent_path = PathBuf::from("/tmp/nonexistent_env"); + assert!(!is_venv_uv_dir(&nonexistent_path), "Should not detect non-existent environment as UV"); + } +} \ No newline at end of file diff --git a/crates/pet/Cargo.toml b/crates/pet/Cargo.toml index ee2efdf7..ea5024e7 100644 --- a/crates/pet/Cargo.toml +++ b/crates/pet/Cargo.toml @@ -31,6 +31,7 @@ pet-linux-global-python = { path = "../pet-linux-global-python" } pet-mac-xcode = { path = "../pet-mac-xcode" } pet-mac-python-org = { path = "../pet-mac-python-org" } pet-venv = { path = "../pet-venv" } +pet-venv-uv = { path = "../pet-venv-uv" } pet-virtualenv = { path = "../pet-virtualenv" } pet-pipenv = { path = "../pet-pipenv" } pet-telemetry = { path = "../pet-telemetry" } diff --git a/crates/pet/src/locators.rs b/crates/pet/src/locators.rs index 5b6b3e81..0401bb90 100644 --- a/crates/pet/src/locators.rs +++ b/crates/pet/src/locators.rs @@ -20,6 +20,7 @@ use pet_poetry::Poetry; use pet_pyenv::PyEnv; use pet_python_utils::env::ResolvedPythonEnv; use pet_venv::Venv; +use pet_venv_uv::VenvUv; use pet_virtualenv::VirtualEnv; use pet_virtualenvwrapper::VirtualEnvWrapper; use std::path::PathBuf; @@ -57,10 +58,11 @@ pub fn create_locators( // 6. Support for Virtual Envs // The order of these matter. - // Basically PipEnv is a superset of VirtualEnvWrapper, which is a superset of Venv, which is a superset of VirtualEnv. + // Basically PipEnv is a superset of VirtualEnvWrapper, which is a superset of VenvUv, which is a superset of Venv, which is a superset of VirtualEnv. locators.push(poetry_locator); locators.push(Arc::new(PipEnv::from(environment))); locators.push(Arc::new(VirtualEnvWrapper::from(environment))); + locators.push(Arc::new(VenvUv::new())); locators.push(Arc::new(Venv::new())); // VirtualEnv is the most generic, hence should be the last. locators.push(Arc::new(VirtualEnv::new())); From da76b77a52d43da467ef1a8f21a400b29df431f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Sep 2025 16:34:53 +0000 Subject: [PATCH 3/6] Fix code formatting for venv-uv implementation Co-authored-by: karthiknadig <3840081+karthiknadig@users.noreply.github.com> --- crates/pet-core/src/pyvenv_cfg.rs | 24 ++++++++++++++++-------- crates/pet-venv-uv/src/lib.rs | 12 +++++++++--- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/crates/pet-core/src/pyvenv_cfg.rs b/crates/pet-core/src/pyvenv_cfg.rs index 90377205..32aac73b 100644 --- a/crates/pet-core/src/pyvenv_cfg.rs +++ b/crates/pet-core/src/pyvenv_cfg.rs @@ -138,7 +138,9 @@ fn parse(file: &Path) -> Option { } match (version, version_major, version_minor) { - (Some(ver), Some(major), Some(minor)) => Some(PyVenvCfg::new(ver, major, minor, prompt, uv_version)), + (Some(ver), Some(major), Some(minor)) => { + Some(PyVenvCfg::new(ver, major, minor, prompt, uv_version)) + } _ => None, } } @@ -219,14 +221,20 @@ fn parse_uv_version(line: &str) -> Option { #[cfg(test)] mod tests { use super::*; - use std::{path::PathBuf, fs}; + 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("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); } @@ -236,12 +244,12 @@ mod tests { 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(); } @@ -250,11 +258,11 @@ mod tests { 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(); } } diff --git a/crates/pet-venv-uv/src/lib.rs b/crates/pet-venv-uv/src/lib.rs index 4bca0c27..0b35ead8 100644 --- a/crates/pet-venv-uv/src/lib.rs +++ b/crates/pet-venv-uv/src/lib.rs @@ -118,13 +118,19 @@ mod tests { fn test_is_venv_uv_dir_does_not_detect_regular_environment() { // This test checks if we can properly ignore regular venv environments let regular_env_path = PathBuf::from("/tmp/test_regular_env"); - assert!(!is_venv_uv_dir(®ular_env_path), "Should not detect regular venv as UV environment"); + assert!( + !is_venv_uv_dir(®ular_env_path), + "Should not detect regular venv as UV environment" + ); } #[test] fn test_is_venv_uv_dir_handles_nonexistent_environment() { // This test checks if we handle non-existent environments gracefully let nonexistent_path = PathBuf::from("/tmp/nonexistent_env"); - assert!(!is_venv_uv_dir(&nonexistent_path), "Should not detect non-existent environment as UV"); + assert!( + !is_venv_uv_dir(&nonexistent_path), + "Should not detect non-existent environment as UV" + ); } -} \ No newline at end of file +} From b3faf31a8b239676e2986bc85304d3d46320e394 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Sep 2025 16:36:25 +0000 Subject: [PATCH 4/6] Fix UV environment detection tests to be self-contained Co-authored-by: karthiknadig <3840081+karthiknadig@users.noreply.github.com> --- crates/pet-venv-uv/src/lib.rs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/crates/pet-venv-uv/src/lib.rs b/crates/pet-venv-uv/src/lib.rs index 0b35ead8..455d84ad 100644 --- a/crates/pet-venv-uv/src/lib.rs +++ b/crates/pet-venv-uv/src/lib.rs @@ -110,18 +110,34 @@ mod tests { #[test] fn test_is_venv_uv_dir_detects_uv_environment() { // This test checks if we can detect a UV environment from pyvenv.cfg - let uv_env_path = PathBuf::from("/tmp/test_uv_env"); - assert!(is_venv_uv_dir(&uv_env_path), "Should detect UV environment"); + use std::fs; + let test_dir = PathBuf::from("/tmp/test_uv_env_venv_uv"); + 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() { // This test checks if we can properly ignore regular venv environments - let regular_env_path = PathBuf::from("/tmp/test_regular_env"); + use std::fs; + let test_dir = PathBuf::from("/tmp/test_regular_env_venv_uv"); + 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(®ular_env_path), + !is_venv_uv_dir(&test_dir), "Should not detect regular venv as UV environment" ); + + fs::remove_dir_all(&test_dir).ok(); } #[test] From d33381b3e07b9ee0d727384e50c97e61804efdd4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Sep 2025 16:59:56 +0000 Subject: [PATCH 5/6] Refactor venv-uv implementation to extend existing venv locator instead of creating new crate Co-authored-by: karthiknadig <3840081+karthiknadig@users.noreply.github.com> --- Cargo.lock | 11 --- crates/pet-core/src/lib.rs | 1 - crates/pet-venv-uv/Cargo.toml | 13 --- crates/pet-venv-uv/src/lib.rs | 152 ---------------------------------- crates/pet-venv/src/lib.rs | 137 +++++++++++++++++++++++++++++- crates/pet/Cargo.toml | 1 - crates/pet/src/locators.rs | 4 +- 7 files changed, 135 insertions(+), 184 deletions(-) delete mode 100644 crates/pet-venv-uv/Cargo.toml delete mode 100644 crates/pet-venv-uv/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 8081eb70..a7b9842d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -361,7 +361,6 @@ dependencies = [ "pet-reporter", "pet-telemetry", "pet-venv", - "pet-venv-uv", "pet-virtualenv", "pet-virtualenvwrapper", "pet-windows-registry", @@ -629,16 +628,6 @@ dependencies = [ "pet-virtualenv", ] -[[package]] -name = "pet-venv-uv" -version = "0.1.0" -dependencies = [ - "log", - "msvc_spectre_libs", - "pet-core", - "pet-python-utils", -] - [[package]] name = "pet-virtualenv" version = "0.1.0" diff --git a/crates/pet-core/src/lib.rs b/crates/pet-core/src/lib.rs index 2bc80adb..8d123469 100644 --- a/crates/pet-core/src/lib.rs +++ b/crates/pet-core/src/lib.rs @@ -51,7 +51,6 @@ pub enum LocatorKind { Poetry, PyEnv, Venv, - VenvUv, VirtualEnv, VirtualEnvWrapper, WindowsRegistry, diff --git a/crates/pet-venv-uv/Cargo.toml b/crates/pet-venv-uv/Cargo.toml deleted file mode 100644 index 98bae6cf..00000000 --- a/crates/pet-venv-uv/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "pet-venv-uv" -version = "0.1.0" -edition = "2021" -license = "MIT" - -[target.'cfg(target_os = "windows")'.dependencies] -msvc_spectre_libs = { version = "0.1.1", features = ["error"] } - -[dependencies] -pet-core = { path = "../pet-core" } -pet-python-utils = { path = "../pet-python-utils" } -log = "0.4.21" \ No newline at end of file diff --git a/crates/pet-venv-uv/src/lib.rs b/crates/pet-venv-uv/src/lib.rs deleted file mode 100644 index 455d84ad..00000000 --- a/crates/pet-venv-uv/src/lib.rs +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -use std::path::Path; - -use pet_core::{ - env::PythonEnv, - python_environment::{PythonEnvironment, PythonEnvironmentBuilder, PythonEnvironmentKind}, - pyvenv_cfg::PyVenvCfg, - reporter::Reporter, - Locator, LocatorKind, -}; -use pet_python_utils::executable::find_executables; -use pet_python_utils::version; - -fn is_venv_uv_internal(env: &PythonEnv) -> Option { - // Check if there's a pyvenv.cfg file with uv entry - if let Some(cfg) = PyVenvCfg::find(env.executable.parent()?) { - return Some(cfg.is_uv()); - } - if let Some(cfg) = PyVenvCfg::find(&env.prefix.clone()?) { - return Some(cfg.is_uv()); - } - Some(false) -} - -pub fn is_venv_uv(env: &PythonEnv) -> bool { - is_venv_uv_internal(env).unwrap_or_default() -} - -pub fn is_venv_uv_dir(path: &Path) -> bool { - if let Some(cfg) = PyVenvCfg::find(path) { - cfg.is_uv() - } else { - false - } -} - -pub struct VenvUv {} - -impl VenvUv { - pub fn new() -> VenvUv { - VenvUv {} - } -} - -impl Default for VenvUv { - fn default() -> Self { - Self::new() - } -} - -impl Locator for VenvUv { - fn get_kind(&self) -> LocatorKind { - LocatorKind::VenvUv - } - - fn supported_categories(&self) -> Vec { - vec![PythonEnvironmentKind::VenvUv] - } - - fn try_from(&self, env: &PythonEnv) -> Option { - if is_venv_uv(env) { - let mut prefix = env.prefix.clone(); - if prefix.is_none() { - prefix = Some(env.executable.parent()?.parent()?.to_path_buf()); - } - let version = match env.version { - Some(ref v) => Some(v.clone()), - None => match &prefix { - Some(prefix) => version::from_creator_for_virtual_env(prefix), - None => None, - }, - }; - let mut symlinks = vec![]; - if let Some(ref prefix) = prefix { - symlinks.append(&mut find_executables(prefix)); - } - - // 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); - - Some( - PythonEnvironmentBuilder::new(Some(PythonEnvironmentKind::VenvUv)) - .name(name) - .executable(Some(env.executable.clone())) - .version(version) - .prefix(prefix) - .symlinks(Some(symlinks)) - .build(), - ) - } else { - None - } - } - - fn find(&self, _reporter: &dyn Reporter) { - // There are no common global locations for uv virtual environments. - // 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() { - // This test checks if we can detect a UV environment from pyvenv.cfg - use std::fs; - let test_dir = PathBuf::from("/tmp/test_uv_env_venv_uv"); - 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() { - // This test checks if we can properly ignore regular venv environments - use std::fs; - let test_dir = PathBuf::from("/tmp/test_regular_env_venv_uv"); - 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() { - // This test checks if we handle non-existent environments gracefully - let nonexistent_path = PathBuf::from("/tmp/nonexistent_env"); - assert!( - !is_venv_uv_dir(&nonexistent_path), - "Should not detect non-existent environment as UV" - ); - } -} diff --git a/crates/pet-venv/src/lib.rs b/crates/pet-venv/src/lib.rs index 5ca5575c..f6518fcf 100644 --- a/crates/pet-venv/src/lib.rs +++ b/crates/pet-venv/src/lib.rs @@ -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 { @@ -43,7 +63,7 @@ impl Locator for Venv { LocatorKind::Venv } fn supported_categories(&self) -> Vec { - vec![PythonEnvironmentKind::Venv] + vec![PythonEnvironmentKind::Venv, PythonEnvironmentKind::VenvUv] } fn try_from(&self, env: &PythonEnv) -> Option { @@ -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().map_or(false, |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) @@ -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(); + } +} diff --git a/crates/pet/Cargo.toml b/crates/pet/Cargo.toml index ea5024e7..ee2efdf7 100644 --- a/crates/pet/Cargo.toml +++ b/crates/pet/Cargo.toml @@ -31,7 +31,6 @@ pet-linux-global-python = { path = "../pet-linux-global-python" } pet-mac-xcode = { path = "../pet-mac-xcode" } pet-mac-python-org = { path = "../pet-mac-python-org" } pet-venv = { path = "../pet-venv" } -pet-venv-uv = { path = "../pet-venv-uv" } pet-virtualenv = { path = "../pet-virtualenv" } pet-pipenv = { path = "../pet-pipenv" } pet-telemetry = { path = "../pet-telemetry" } diff --git a/crates/pet/src/locators.rs b/crates/pet/src/locators.rs index 0401bb90..5b6b3e81 100644 --- a/crates/pet/src/locators.rs +++ b/crates/pet/src/locators.rs @@ -20,7 +20,6 @@ use pet_poetry::Poetry; use pet_pyenv::PyEnv; use pet_python_utils::env::ResolvedPythonEnv; use pet_venv::Venv; -use pet_venv_uv::VenvUv; use pet_virtualenv::VirtualEnv; use pet_virtualenvwrapper::VirtualEnvWrapper; use std::path::PathBuf; @@ -58,11 +57,10 @@ pub fn create_locators( // 6. Support for Virtual Envs // The order of these matter. - // Basically PipEnv is a superset of VirtualEnvWrapper, which is a superset of VenvUv, which is a superset of Venv, which is a superset of VirtualEnv. + // Basically PipEnv is a superset of VirtualEnvWrapper, which is a superset of Venv, which is a superset of VirtualEnv. locators.push(poetry_locator); locators.push(Arc::new(PipEnv::from(environment))); locators.push(Arc::new(VirtualEnvWrapper::from(environment))); - locators.push(Arc::new(VenvUv::new())); locators.push(Arc::new(Venv::new())); // VirtualEnv is the most generic, hence should be the last. locators.push(Arc::new(VirtualEnv::new())); From dd4ab6479faac2fcc0b7cf0693abcc56d75fb2fb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Sep 2025 17:09:32 +0000 Subject: [PATCH 6/6] Fix clippy error: use is_some_and instead of map_or Co-authored-by: karthiknadig <3840081+karthiknadig@users.noreply.github.com> --- crates/pet-venv/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/pet-venv/src/lib.rs b/crates/pet-venv/src/lib.rs index f6518fcf..f3c76468 100644 --- a/crates/pet-venv/src/lib.rs +++ b/crates/pet-venv/src/lib.rs @@ -90,7 +90,7 @@ impl Locator for Venv { 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().map_or(false, |c| c.is_uv()) { + let kind = if cfg.as_ref().is_some_and(|c| c.is_uv()) { PythonEnvironmentKind::VenvUv } else { PythonEnvironmentKind::Venv