Skip to content
Draft
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
57 changes: 56 additions & 1 deletion Cargo.lock

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

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, // Virtual environments created with UV
VirtualEnv,
VirtualEnvWrapper,
WindowsStore,
Expand Down
34 changes: 32 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>, // UV version if this was created by UV
}

impl PyVenvCfg {
Expand All @@ -31,14 +32,22 @@ 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,
}
}

/// 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<Self> {
if let Some(ref file) = find(path) {
parse(file)
Expand Down Expand Up @@ -99,6 +108,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 +130,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 +194,16 @@ 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 value = trimmed[eq_idx + 1..].trim();
if !value.is_empty() {
return Some(value.to_string());
}
}
}
None
}
3 changes: 3 additions & 0 deletions crates/pet-venv/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
44 changes: 41 additions & 3 deletions crates/pet-venv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,43 @@ fn is_venv_internal(env: &PythonEnv) -> Option<bool> {
|| 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 {
Expand All @@ -43,7 +74,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 +98,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)
Expand Down
Loading
Loading