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
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
56 changes: 55 additions & 1 deletion crates/pet-poetry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

use env_variables::EnvVariables;
use environment_locations::list_environments;
use lazy_static::lazy_static;
use log::trace;
use manager::PoetryManager;
use pet_core::{
Expand All @@ -13,8 +14,9 @@ use pet_core::{
Configuration, Locator, LocatorKind, LocatorResult,
};
use pet_virtualenv::is_virtualenv;
use regex::Regex;
use std::{
path::PathBuf,
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
use telemetry::report_missing_envs;
Expand All @@ -28,6 +30,38 @@ pub mod manager;
mod pyproject_toml;
mod telemetry;

lazy_static! {
static ref POETRY_ENV_NAME_PATTERN: Regex = Regex::new(r"^.+-[A-Za-z0-9_-]{8}-py.*$")
.expect("Error generating RegEx for poetry environment name pattern");
}

/// Check if a path looks like a Poetry environment by examining the directory structure
/// Poetry environments typically have names like: {name}-{hash}-py{version}
/// and are located in cache directories or as .venv in project directories
fn is_poetry_environment(path: &Path) -> bool {
// Check if the environment is in a directory that looks like Poetry's virtualenvs cache
// Common patterns:
// - Linux: ~/.cache/pypoetry/virtualenvs/
// - macOS: ~/Library/Caches/pypoetry/virtualenvs/
// - Windows: %LOCALAPPDATA%\pypoetry\Cache\virtualenvs\
let path_str = path.to_str().unwrap_or_default();

// Check if path contains typical Poetry cache directory structure
if path_str.contains("pypoetry") && path_str.contains("virtualenvs") {
// Further validate by checking if the directory name matches Poetry's naming pattern
// Pattern: {name}-{8-char-hash}-py or just .venv
if let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) {
// Check for Poetry's hash-based naming: name-XXXXXXXX-py
// The hash is 8 characters of base64url encoding
if POETRY_ENV_NAME_PATTERN.is_match(dir_name) {
return true;
}
}
}

false
}

pub trait PoetryLocator: Send + Sync {
fn find_and_report_missing_envs(
&self,
Expand Down Expand Up @@ -153,6 +187,8 @@ impl Locator for Poetry {
if !is_virtualenv(env) {
return None;
}

// First, check if the environment is in our cache
if let Some(result) = self.find_with_cache() {
for found_env in result.environments {
if let Some(symlinks) = &found_env.symlinks {
Expand All @@ -162,6 +198,24 @@ impl Locator for Poetry {
}
}
}

// Fallback: Check if the path looks like a Poetry environment
// This handles cases where the environment wasn't discovered during find()
// (e.g., workspace directories not configured, or pyproject.toml not found)
if let Some(prefix) = &env.prefix {
if is_poetry_environment(prefix) {
trace!(
"Identified Poetry environment by path pattern: {:?}",
prefix
);
return environment::create_poetry_env(
prefix,
prefix.clone(), // We don't have the project directory, use prefix
None, // No manager available in this fallback case
);
}
}

None
}

Expand Down
104 changes: 104 additions & 0 deletions crates/pet-poetry/tests/path_identification_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Tests for Poetry environment identification by path pattern.
//!
//! This test module verifies that Poetry environments are correctly identified
//! even when they are not discovered during the find() phase. This can happen when:
//! - Workspace directories are not configured
//! - The pyproject.toml is not in the workspace directories
//! - The environment is in the Poetry cache but wasn't enumerated
//!
//! The fix adds a fallback path-based detection that checks if the environment
//! path matches Poetry's naming pattern ({name}-{8-char-hash}-py{version}) and
//! is located in a Poetry cache directory (containing "pypoetry/virtualenvs").

use std::path::PathBuf;

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

// Helper function to test the regex pattern matching
// This tests the core logic without needing actual filesystem structures
fn test_poetry_path_pattern(path_str: &str) -> bool {
use regex::Regex;
let path = PathBuf::from(path_str);
let path_str = path.to_str().unwrap_or_default();

if path_str.contains("pypoetry") && path_str.contains("virtualenvs") {
if let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) {
let re = Regex::new(r"^.+-[A-Za-z0-9_-]{8}-py.*$").unwrap();
return re.is_match(dir_name);
}
}
false
}

#[test]
fn test_poetry_path_pattern_macos() {
assert!(test_poetry_path_pattern(
"/Users/eleanorboyd/Library/Caches/pypoetry/virtualenvs/nestedpoetry-yJwtIF_Q-py3.11"
));
}

#[test]
fn test_poetry_path_pattern_linux() {
assert!(test_poetry_path_pattern(
"/home/user/.cache/pypoetry/virtualenvs/myproject-a1B2c3D4-py3.10"
));
}

#[test]
fn test_poetry_path_pattern_windows() {
assert!(test_poetry_path_pattern(
r"C:\Users\user\AppData\Local\pypoetry\Cache\virtualenvs\myproject-f7sQRtG5-py3.11"
));
}

#[test]
fn test_poetry_path_pattern_no_version() {
assert!(test_poetry_path_pattern(
"/home/user/.cache/pypoetry/virtualenvs/testproject-XyZ12345-py"
));
}

#[test]
fn test_non_poetry_path_rejected() {
assert!(!test_poetry_path_pattern("/home/user/projects/myenv"));
assert!(!test_poetry_path_pattern("/home/user/.venv"));
assert!(!test_poetry_path_pattern("/usr/local/venv"));
}

#[test]
fn test_poetry_path_without_pypoetry_rejected() {
// Should reject paths that look like the pattern but aren't in pypoetry directory
assert!(!test_poetry_path_pattern(
"/home/user/virtualenvs/myproject-a1B2c3D4-py3.10"
));
}

#[test]
fn test_poetry_path_wrong_hash_length_rejected() {
// Hash should be exactly 8 characters
assert!(!test_poetry_path_pattern(
"/home/user/.cache/pypoetry/virtualenvs/myproject-a1B2c3D456-py3.10"
));
assert!(!test_poetry_path_pattern(
"/home/user/.cache/pypoetry/virtualenvs/myproject-a1B2c3-py3.10"
));
}

#[test]
fn test_real_world_poetry_paths() {
// Test actual Poetry paths from the bug report and real usage
assert!(test_poetry_path_pattern(
"/Users/eleanorboyd/Library/Caches/pypoetry/virtualenvs/nestedpoetry-yJwtIF_Q-py3.11"
));

// Another real-world example from documentation
assert!(test_poetry_path_pattern(
"/Users/donjayamanne/.cache/pypoetry/virtualenvs/poetry-demo-gNT2WXAV-py3.9"
));
}
}
Loading