Skip to content

Commit 9b02261

Browse files
committed
Add venv-uv label for venvs created with uv
1 parent d658378 commit 9b02261

File tree

6 files changed

+274
-6
lines changed

6 files changed

+274
-6
lines changed

Cargo.lock

Lines changed: 56 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/pet-core/src/python_environment.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub enum PythonEnvironmentKind {
2424
LinuxGlobal,
2525
MacXCode,
2626
Venv,
27+
VenvUv, // Virtual environments created with UV
2728
VirtualEnv,
2829
VirtualEnvWrapper,
2930
WindowsStore,

crates/pet-core/src/pyvenv_cfg.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub struct PyVenvCfg {
2323
pub version_major: u64,
2424
pub version_minor: u64,
2525
pub prompt: Option<String>,
26+
pub uv_version: Option<String>, // UV version if this was created by UV
2627
}
2728

2829
impl PyVenvCfg {
@@ -31,14 +32,22 @@ impl PyVenvCfg {
3132
version_major: u64,
3233
version_minor: u64,
3334
prompt: Option<String>,
35+
uv_version: Option<String>,
3436
) -> Self {
3537
Self {
3638
version,
3739
version_major,
3840
version_minor,
3941
prompt,
42+
uv_version,
4043
}
4144
}
45+
46+
/// Returns true if this virtual environment was created with UV
47+
pub fn is_uv(&self) -> bool {
48+
self.uv_version.is_some()
49+
}
50+
4251
pub fn find(path: &Path) -> Option<Self> {
4352
if let Some(ref file) = find(path) {
4453
parse(file)
@@ -99,6 +108,7 @@ fn parse(file: &Path) -> Option<PyVenvCfg> {
99108
let mut version_major: Option<u64> = None;
100109
let mut version_minor: Option<u64> = None;
101110
let mut prompt: Option<String> = None;
111+
let mut uv_version: Option<String> = None;
102112

103113
for line in contents.lines() {
104114
if version.is_none() {
@@ -120,13 +130,18 @@ fn parse(file: &Path) -> Option<PyVenvCfg> {
120130
prompt = Some(p);
121131
}
122132
}
123-
if version.is_some() && prompt.is_some() {
133+
if uv_version.is_none() {
134+
if let Some(uv_ver) = parse_uv_version(line) {
135+
uv_version = Some(uv_ver);
136+
}
137+
}
138+
if version.is_some() && prompt.is_some() && uv_version.is_some() {
124139
break;
125140
}
126141
}
127142

128143
match (version, version_major, version_minor) {
129-
(Some(ver), Some(major), Some(minor)) => Some(PyVenvCfg::new(ver, major, minor, prompt)),
144+
(Some(ver), Some(major), Some(minor)) => Some(PyVenvCfg::new(ver, major, minor, prompt, uv_version)),
130145
_ => None,
131146
}
132147
}
@@ -177,3 +192,16 @@ fn parse_prompt(line: &str) -> Option<String> {
177192
}
178193
None
179194
}
195+
196+
fn parse_uv_version(line: &str) -> Option<String> {
197+
let trimmed = line.trim();
198+
if trimmed.starts_with("uv") {
199+
if let Some(eq_idx) = trimmed.find('=') {
200+
let value = trimmed[eq_idx + 1..].trim();
201+
if !value.is_empty() {
202+
return Some(value.to_string());
203+
}
204+
}
205+
}
206+
None
207+
}

crates/pet-venv/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,6 @@ pet-core = { path = "../pet-core" }
1212
pet-virtualenv = { path = "../pet-virtualenv" }
1313
pet-python-utils = { path = "../pet-python-utils" }
1414
log = "0.4.21"
15+
16+
[dev-dependencies]
17+
tempfile = "3.0"

crates/pet-venv/src/lib.rs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,34 @@ fn is_venv_internal(env: &PythonEnv) -> Option<bool> {
2020
|| PyVenvCfg::find(&env.prefix.clone()?).is_some(),
2121
)
2222
}
23+
2324
pub fn is_venv(env: &PythonEnv) -> bool {
2425
is_venv_internal(env).unwrap_or_default()
2526
}
27+
2628
pub fn is_venv_dir(path: &Path) -> bool {
2729
PyVenvCfg::find(path).is_some()
2830
}
31+
32+
/// Check if this is a UV-created virtual environment
33+
pub fn is_venv_uv(env: &PythonEnv) -> bool {
34+
if let Some(cfg) = PyVenvCfg::find(env.executable.parent().unwrap_or(&env.executable))
35+
.or_else(|| PyVenvCfg::find(&env.prefix.clone().unwrap_or_else(|| env.executable.parent().unwrap().parent().unwrap().to_path_buf())))
36+
{
37+
cfg.is_uv()
38+
} else {
39+
false
40+
}
41+
}
42+
43+
/// Check if a directory contains a UV-created virtual environment
44+
pub fn is_venv_uv_dir(path: &Path) -> bool {
45+
if let Some(cfg) = PyVenvCfg::find(path) {
46+
cfg.is_uv()
47+
} else {
48+
false
49+
}
50+
}
2951
pub struct Venv {}
3052

3153
impl Venv {
@@ -43,7 +65,7 @@ impl Locator for Venv {
4365
LocatorKind::Venv
4466
}
4567
fn supported_categories(&self) -> Vec<PythonEnvironmentKind> {
46-
vec![PythonEnvironmentKind::Venv]
68+
vec![PythonEnvironmentKind::Venv, PythonEnvironmentKind::VenvUv]
4769
}
4870

4971
fn try_from(&self, env: &PythonEnv) -> Option<PythonEnvironment> {
@@ -67,10 +89,17 @@ impl Locator for Venv {
6789
// Get the name from the prefix if it exists.
6890
let cfg = PyVenvCfg::find(env.executable.parent()?)
6991
.or_else(|| PyVenvCfg::find(&env.prefix.clone()?));
70-
let name = cfg.and_then(|cfg| cfg.prompt);
92+
let name = cfg.as_ref().and_then(|cfg| cfg.prompt.clone());
93+
94+
// Determine environment kind based on whether UV was used
95+
let kind = match &cfg {
96+
Some(cfg) if cfg.is_uv() => Some(PythonEnvironmentKind::VenvUv),
97+
Some(_) => Some(PythonEnvironmentKind::Venv),
98+
None => Some(PythonEnvironmentKind::Venv), // Default to Venv if no cfg found
99+
};
71100

72101
Some(
73-
PythonEnvironmentBuilder::new(Some(PythonEnvironmentKind::Venv))
102+
PythonEnvironmentBuilder::new(kind)
74103
.name(name)
75104
.executable(Some(env.executable.clone()))
76105
.version(version)

0 commit comments

Comments
 (0)