From 9b02261b90ec6175d740eb534399a67a4624aa14 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 18 Sep 2025 10:08:11 -0700 Subject: [PATCH 1/2] Add venv-uv label for venvs created with `uv` --- Cargo.lock | 57 +++++++- crates/pet-core/src/python_environment.rs | 1 + crates/pet-core/src/pyvenv_cfg.rs | 32 ++++- crates/pet-venv/Cargo.toml | 3 + crates/pet-venv/src/lib.rs | 35 ++++- crates/pet-venv/tests/mod.rs | 152 ++++++++++++++++++++++ 6 files changed, 274 insertions(+), 6 deletions(-) create mode 100644 crates/pet-venv/tests/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 3e0837f9..4b025afa 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" @@ -90,6 +90,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + [[package]] name = "block-buffer" version = "0.10.4" @@ -214,6 +220,22 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "generic-array" version = "0.14.7" @@ -306,6 +328,12 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "log" version = "0.4.21" @@ -626,6 +654,7 @@ dependencies = [ "pet-core", "pet-python-utils", "pet-virtualenv", + "tempfile", ] [[package]] @@ -730,6 +759,19 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "ryu" version = "1.0.18" @@ -804,6 +846,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "termcolor" version = "1.4.1" diff --git a/crates/pet-core/src/python_environment.rs b/crates/pet-core/src/python_environment.rs index a611ced8..d061ff26 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, // Virtual environments created with UV VirtualEnv, VirtualEnvWrapper, WindowsStore, diff --git a/crates/pet-core/src/pyvenv_cfg.rs b/crates/pet-core/src/pyvenv_cfg.rs index 1dd454a5..b8cb1f89 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, // UV version if this was created by UV } impl PyVenvCfg { @@ -31,14 +32,22 @@ impl PyVenvCfg { version_major: u64, version_minor: u64, prompt: Option, + uv_version: Option, ) -> Self { Self { version, version_major, version_minor, prompt, + uv_version, } } + + /// Returns true if this virtual environment was created with UV + 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 +108,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 +130,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 +192,16 @@ 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 value = trimmed[eq_idx + 1..].trim(); + if !value.is_empty() { + return Some(value.to_string()); + } + } + } + None +} diff --git a/crates/pet-venv/Cargo.toml b/crates/pet-venv/Cargo.toml index 411e1ad9..ab9f48b3 100644 --- a/crates/pet-venv/Cargo.toml +++ b/crates/pet-venv/Cargo.toml @@ -12,3 +12,6 @@ pet-core = { path = "../pet-core" } pet-virtualenv = { path = "../pet-virtualenv" } pet-python-utils = { path = "../pet-python-utils" } log = "0.4.21" + +[dev-dependencies] +tempfile = "3.0" diff --git a/crates/pet-venv/src/lib.rs b/crates/pet-venv/src/lib.rs index 5ca5575c..b50ab3bd 100644 --- a/crates/pet-venv/src/lib.rs +++ b/crates/pet-venv/src/lib.rs @@ -20,12 +20,34 @@ fn is_venv_internal(env: &PythonEnv) -> Option { || PyVenvCfg::find(&env.prefix.clone()?).is_some(), ) } + pub fn is_venv(env: &PythonEnv) -> bool { is_venv_internal(env).unwrap_or_default() } + pub fn is_venv_dir(path: &Path) -> bool { PyVenvCfg::find(path).is_some() } + +/// Check if this is a UV-created virtual environment +pub fn is_venv_uv(env: &PythonEnv) -> bool { + if let Some(cfg) = PyVenvCfg::find(env.executable.parent().unwrap_or(&env.executable)) + .or_else(|| PyVenvCfg::find(&env.prefix.clone().unwrap_or_else(|| env.executable.parent().unwrap().parent().unwrap().to_path_buf()))) + { + cfg.is_uv() + } else { + false + } +} + +/// Check if a directory contains a UV-created virtual environment +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 +65,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 +89,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 environment kind based on whether UV was used + let kind = match &cfg { + Some(cfg) if cfg.is_uv() => Some(PythonEnvironmentKind::VenvUv), + Some(_) => Some(PythonEnvironmentKind::Venv), + None => Some(PythonEnvironmentKind::Venv), // Default to Venv if no cfg found + }; Some( - PythonEnvironmentBuilder::new(Some(PythonEnvironmentKind::Venv)) + PythonEnvironmentBuilder::new(kind) .name(name) .executable(Some(env.executable.clone())) .version(version) diff --git a/crates/pet-venv/tests/mod.rs b/crates/pet-venv/tests/mod.rs new file mode 100644 index 00000000..34d6027c --- /dev/null +++ b/crates/pet-venv/tests/mod.rs @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Tests for venv environment detection, including UV-created environments + +use std::fs; +use tempfile::TempDir; + +use pet_core::pyvenv_cfg::PyVenvCfg; +use pet_core::{env::PythonEnv, python_environment::PythonEnvironmentKind}; +use pet_venv::{is_venv, is_venv_dir, is_venv_uv, is_venv_uv_dir, Venv}; +use pet_core::Locator; + +/// Test that we can detect regular venv environments +#[test] +fn test_detect_regular_venv() { + let temp_dir = TempDir::new().unwrap(); + let venv_dir = temp_dir.path().join("test_venv"); + fs::create_dir_all(&venv_dir).unwrap(); + + // Create a regular pyvenv.cfg (without UV) + let pyvenv_cfg_content = r#"home = /usr/bin +implementation = CPython +version_info = 3.12.11 +include-system-site-packages = false +prompt = test_venv +"#; + let pyvenv_cfg_path = venv_dir.join("pyvenv.cfg"); + fs::write(&pyvenv_cfg_path, pyvenv_cfg_content).unwrap(); + + // Create a dummy python executable + let scripts_dir = if cfg!(windows) { "Scripts" } else { "bin" }; + let bin_dir = venv_dir.join(scripts_dir); + fs::create_dir_all(&bin_dir).unwrap(); + let python_exe = bin_dir.join(if cfg!(windows) { "python.exe" } else { "python" }); + fs::write(&python_exe, "dummy").unwrap(); + + // Test PyVenvCfg detection + let cfg = PyVenvCfg::find(&venv_dir).unwrap(); + assert!(!cfg.is_uv()); + assert_eq!(cfg.version, "3.12.11"); + assert_eq!(cfg.prompt, Some("test_venv".to_string())); + + // Test directory detection + assert!(is_venv_dir(&venv_dir)); + assert!(!is_venv_uv_dir(&venv_dir)); + + // Test with PythonEnv + let python_env = PythonEnv::new(python_exe.clone(), None, None); + assert!(is_venv(&python_env)); + assert!(!is_venv_uv(&python_env)); + + // Test locator + let locator = Venv::new(); + let result = locator.try_from(&python_env).unwrap(); + assert_eq!(result.kind, Some(PythonEnvironmentKind::Venv)); + assert_eq!(result.name, Some("test_venv".to_string())); +} + +/// Test that we can detect UV venv environments +#[test] +fn test_detect_uv_venv() { + let temp_dir = TempDir::new().unwrap(); + let venv_dir = temp_dir.path().join("test_uv_venv"); + fs::create_dir_all(&venv_dir).unwrap(); + + // Create a UV pyvenv.cfg (with UV entry) + let pyvenv_cfg_content = r#"home = /usr/bin +implementation = CPython +uv = 0.8.14 +version_info = 3.12.11 +include-system-site-packages = false +prompt = test_uv_venv +"#; + let pyvenv_cfg_path = venv_dir.join("pyvenv.cfg"); + fs::write(&pyvenv_cfg_path, pyvenv_cfg_content).unwrap(); + + // Create a dummy python executable + let scripts_dir = if cfg!(windows) { "Scripts" } else { "bin" }; + let bin_dir = venv_dir.join(scripts_dir); + fs::create_dir_all(&bin_dir).unwrap(); + let python_exe = bin_dir.join(if cfg!(windows) { "python.exe" } else { "python" }); + fs::write(&python_exe, "dummy").unwrap(); + + // Test PyVenvCfg detection + let cfg = PyVenvCfg::find(&venv_dir).unwrap(); + assert!(cfg.is_uv()); + assert_eq!(cfg.uv_version, Some("0.8.14".to_string())); + assert_eq!(cfg.version, "3.12.11"); + assert_eq!(cfg.prompt, Some("test_uv_venv".to_string())); + + // Test directory detection + assert!(is_venv_dir(&venv_dir)); + assert!(is_venv_uv_dir(&venv_dir)); + + // Test with PythonEnv + let python_env = PythonEnv::new(python_exe.clone(), None, None); + assert!(is_venv(&python_env)); + assert!(is_venv_uv(&python_env)); + + // Test locator + let locator = Venv::new(); + let result = locator.try_from(&python_env).unwrap(); + assert_eq!(result.kind, Some(PythonEnvironmentKind::VenvUv)); + assert_eq!(result.name, Some("test_uv_venv".to_string())); +} + +/// Test that UV version parsing works with different UV version formats +#[test] +fn test_uv_version_parsing() { + let temp_dir = TempDir::new().unwrap(); + let venv_dir = temp_dir.path().join("test_uv_version"); + fs::create_dir_all(&venv_dir).unwrap(); + + // Test different UV version formats + let test_cases = vec![ + ("uv = 0.8.14", Some("0.8.14".to_string())), + ("uv=0.8.14", Some("0.8.14".to_string())), + ("uv = 1.0.0-beta", Some("1.0.0-beta".to_string())), + ("uv= 2.1.3 ", Some("2.1.3".to_string())), + ]; + + for (uv_line, expected) in test_cases { + let pyvenv_cfg_content = format!( + r#"home = /usr/bin +implementation = CPython +{} +version_info = 3.12.11 +include-system-site-packages = false +prompt = test_uv +"#, + uv_line + ); + let pyvenv_cfg_path = venv_dir.join("pyvenv.cfg"); + fs::write(&pyvenv_cfg_path, pyvenv_cfg_content).unwrap(); + + let cfg = PyVenvCfg::find(&venv_dir).unwrap(); + assert_eq!(cfg.uv_version, expected, "Failed for UV line: {}", uv_line); + assert!(cfg.is_uv()); + } +} + +/// Test locator supported categories +#[test] +fn test_locator_supported_categories() { + let locator = Venv::new(); + let categories = locator.supported_categories(); + + assert!(categories.contains(&PythonEnvironmentKind::Venv)); + assert!(categories.contains(&PythonEnvironmentKind::VenvUv)); + assert_eq!(categories.len(), 2); +} \ No newline at end of file From 85bcc76b47001f7e3f974ebe485c4d5a76fd860a Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 18 Sep 2025 10:18:34 -0700 Subject: [PATCH 2/2] Formatting --- crates/pet-core/src/pyvenv_cfg.rs | 8 ++++-- crates/pet-venv/src/lib.rs | 15 ++++++++-- crates/pet-venv/tests/mod.rs | 48 ++++++++++++++++++------------- 3 files changed, 45 insertions(+), 26 deletions(-) diff --git a/crates/pet-core/src/pyvenv_cfg.rs b/crates/pet-core/src/pyvenv_cfg.rs index b8cb1f89..1ff5d4a7 100644 --- a/crates/pet-core/src/pyvenv_cfg.rs +++ b/crates/pet-core/src/pyvenv_cfg.rs @@ -42,12 +42,12 @@ impl PyVenvCfg { uv_version, } } - + /// Returns true if this virtual environment was created with UV 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) @@ -141,7 +141,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, } } diff --git a/crates/pet-venv/src/lib.rs b/crates/pet-venv/src/lib.rs index b50ab3bd..82e57b47 100644 --- a/crates/pet-venv/src/lib.rs +++ b/crates/pet-venv/src/lib.rs @@ -31,8 +31,17 @@ pub fn is_venv_dir(path: &Path) -> bool { /// Check if this is a UV-created virtual environment pub fn is_venv_uv(env: &PythonEnv) -> bool { - if let Some(cfg) = PyVenvCfg::find(env.executable.parent().unwrap_or(&env.executable)) - .or_else(|| PyVenvCfg::find(&env.prefix.clone().unwrap_or_else(|| env.executable.parent().unwrap().parent().unwrap().to_path_buf()))) + if let Some(cfg) = + PyVenvCfg::find(env.executable.parent().unwrap_or(&env.executable)).or_else(|| { + PyVenvCfg::find(&env.prefix.clone().unwrap_or_else(|| { + env.executable + .parent() + .unwrap() + .parent() + .unwrap() + .to_path_buf() + })) + }) { cfg.is_uv() } else { @@ -90,7 +99,7 @@ impl Locator for Venv { let cfg = PyVenvCfg::find(env.executable.parent()?) .or_else(|| PyVenvCfg::find(&env.prefix.clone()?)); let name = cfg.as_ref().and_then(|cfg| cfg.prompt.clone()); - + // Determine environment kind based on whether UV was used let kind = match &cfg { Some(cfg) if cfg.is_uv() => Some(PythonEnvironmentKind::VenvUv), diff --git a/crates/pet-venv/tests/mod.rs b/crates/pet-venv/tests/mod.rs index 34d6027c..840f895c 100644 --- a/crates/pet-venv/tests/mod.rs +++ b/crates/pet-venv/tests/mod.rs @@ -7,9 +7,9 @@ use std::fs; use tempfile::TempDir; use pet_core::pyvenv_cfg::PyVenvCfg; +use pet_core::Locator; use pet_core::{env::PythonEnv, python_environment::PythonEnvironmentKind}; use pet_venv::{is_venv, is_venv_dir, is_venv_uv, is_venv_uv_dir, Venv}; -use pet_core::Locator; /// Test that we can detect regular venv environments #[test] @@ -17,7 +17,7 @@ fn test_detect_regular_venv() { let temp_dir = TempDir::new().unwrap(); let venv_dir = temp_dir.path().join("test_venv"); fs::create_dir_all(&venv_dir).unwrap(); - + // Create a regular pyvenv.cfg (without UV) let pyvenv_cfg_content = r#"home = /usr/bin implementation = CPython @@ -27,29 +27,33 @@ prompt = test_venv "#; let pyvenv_cfg_path = venv_dir.join("pyvenv.cfg"); fs::write(&pyvenv_cfg_path, pyvenv_cfg_content).unwrap(); - + // Create a dummy python executable let scripts_dir = if cfg!(windows) { "Scripts" } else { "bin" }; let bin_dir = venv_dir.join(scripts_dir); fs::create_dir_all(&bin_dir).unwrap(); - let python_exe = bin_dir.join(if cfg!(windows) { "python.exe" } else { "python" }); + let python_exe = bin_dir.join(if cfg!(windows) { + "python.exe" + } else { + "python" + }); fs::write(&python_exe, "dummy").unwrap(); - + // Test PyVenvCfg detection let cfg = PyVenvCfg::find(&venv_dir).unwrap(); assert!(!cfg.is_uv()); assert_eq!(cfg.version, "3.12.11"); assert_eq!(cfg.prompt, Some("test_venv".to_string())); - + // Test directory detection assert!(is_venv_dir(&venv_dir)); assert!(!is_venv_uv_dir(&venv_dir)); - + // Test with PythonEnv let python_env = PythonEnv::new(python_exe.clone(), None, None); assert!(is_venv(&python_env)); assert!(!is_venv_uv(&python_env)); - + // Test locator let locator = Venv::new(); let result = locator.try_from(&python_env).unwrap(); @@ -63,7 +67,7 @@ fn test_detect_uv_venv() { let temp_dir = TempDir::new().unwrap(); let venv_dir = temp_dir.path().join("test_uv_venv"); fs::create_dir_all(&venv_dir).unwrap(); - + // Create a UV pyvenv.cfg (with UV entry) let pyvenv_cfg_content = r#"home = /usr/bin implementation = CPython @@ -74,30 +78,34 @@ prompt = test_uv_venv "#; let pyvenv_cfg_path = venv_dir.join("pyvenv.cfg"); fs::write(&pyvenv_cfg_path, pyvenv_cfg_content).unwrap(); - + // Create a dummy python executable let scripts_dir = if cfg!(windows) { "Scripts" } else { "bin" }; let bin_dir = venv_dir.join(scripts_dir); fs::create_dir_all(&bin_dir).unwrap(); - let python_exe = bin_dir.join(if cfg!(windows) { "python.exe" } else { "python" }); + let python_exe = bin_dir.join(if cfg!(windows) { + "python.exe" + } else { + "python" + }); fs::write(&python_exe, "dummy").unwrap(); - + // Test PyVenvCfg detection let cfg = PyVenvCfg::find(&venv_dir).unwrap(); assert!(cfg.is_uv()); assert_eq!(cfg.uv_version, Some("0.8.14".to_string())); assert_eq!(cfg.version, "3.12.11"); assert_eq!(cfg.prompt, Some("test_uv_venv".to_string())); - + // Test directory detection assert!(is_venv_dir(&venv_dir)); assert!(is_venv_uv_dir(&venv_dir)); - + // Test with PythonEnv let python_env = PythonEnv::new(python_exe.clone(), None, None); assert!(is_venv(&python_env)); assert!(is_venv_uv(&python_env)); - + // Test locator let locator = Venv::new(); let result = locator.try_from(&python_env).unwrap(); @@ -111,7 +119,7 @@ fn test_uv_version_parsing() { let temp_dir = TempDir::new().unwrap(); let venv_dir = temp_dir.path().join("test_uv_version"); fs::create_dir_all(&venv_dir).unwrap(); - + // Test different UV version formats let test_cases = vec![ ("uv = 0.8.14", Some("0.8.14".to_string())), @@ -119,7 +127,7 @@ fn test_uv_version_parsing() { ("uv = 1.0.0-beta", Some("1.0.0-beta".to_string())), ("uv= 2.1.3 ", Some("2.1.3".to_string())), ]; - + for (uv_line, expected) in test_cases { let pyvenv_cfg_content = format!( r#"home = /usr/bin @@ -133,7 +141,7 @@ prompt = test_uv ); let pyvenv_cfg_path = venv_dir.join("pyvenv.cfg"); fs::write(&pyvenv_cfg_path, pyvenv_cfg_content).unwrap(); - + let cfg = PyVenvCfg::find(&venv_dir).unwrap(); assert_eq!(cfg.uv_version, expected, "Failed for UV line: {}", uv_line); assert!(cfg.is_uv()); @@ -145,8 +153,8 @@ prompt = test_uv fn test_locator_supported_categories() { let locator = Venv::new(); let categories = locator.supported_categories(); - + assert!(categories.contains(&PythonEnvironmentKind::Venv)); assert!(categories.contains(&PythonEnvironmentKind::VenvUv)); assert_eq!(categories.len(), 2); -} \ No newline at end of file +}