Skip to content

Commit c8c64c2

Browse files
committed
Add detection for in-project Poetry environments and update tests
1 parent 4b67cdd commit c8c64c2

File tree

4 files changed

+226
-23
lines changed

4 files changed

+226
-23
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/pet-poetry/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,8 @@ sha2 = "0.10.6"
2222
base64 = "0.22.0"
2323
toml = "0.8.14"
2424

25+
[dev-dependencies]
26+
tempfile = "3.12"
27+
2528
[features]
2629
ci = []

crates/pet-poetry/src/lib.rs

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ lazy_static! {
3535
.expect("Error generating RegEx for poetry environment name pattern");
3636
}
3737

38-
/// Check if a path looks like a Poetry environment by examining the directory structure
39-
/// Poetry environments typically have names like: {name}-{hash}-py{version}
40-
/// and are located in cache directories or as .venv in project directories
41-
fn is_poetry_environment(path: &Path) -> bool {
38+
/// Check if a path looks like a Poetry environment in the cache directory
39+
/// Poetry cache environments have names like: {name}-{hash}-py{version}
40+
/// and are located in cache directories containing "pypoetry/virtualenvs"
41+
fn is_poetry_cache_environment(path: &Path) -> bool {
4242
// Check if the environment is in a directory that looks like Poetry's virtualenvs cache
4343
// Common patterns:
4444
// - Linux: ~/.cache/pypoetry/virtualenvs/
@@ -62,6 +62,41 @@ fn is_poetry_environment(path: &Path) -> bool {
6262
false
6363
}
6464

65+
/// Check if a .venv directory is an in-project Poetry environment
66+
/// This is for the case when virtualenvs.in-project = true is set.
67+
/// We check if the parent directory has a pyproject.toml with Poetry configuration.
68+
fn is_in_project_poetry_environment(path: &Path) -> bool {
69+
// Check if this is a .venv directory
70+
let dir_name = path.file_name().and_then(|n| n.to_str()).unwrap_or_default();
71+
if dir_name != ".venv" {
72+
return false;
73+
}
74+
75+
// Check if the parent directory has a pyproject.toml with Poetry configuration
76+
if let Some(parent) = path.parent() {
77+
let pyproject_toml = parent.join("pyproject.toml");
78+
if pyproject_toml.is_file() {
79+
// Check if pyproject.toml contains Poetry configuration
80+
if let Ok(contents) = std::fs::read_to_string(&pyproject_toml) {
81+
// Look for [tool.poetry] or [project] with poetry as build backend
82+
if contents.contains("[tool.poetry]")
83+
|| (contents.contains("poetry.core.masonry.api")
84+
|| contents.contains("poetry-core"))
85+
{
86+
trace!(
87+
"Found in-project Poetry environment: {:?} with pyproject.toml at {:?}",
88+
path,
89+
pyproject_toml
90+
);
91+
return true;
92+
}
93+
}
94+
}
95+
}
96+
97+
false
98+
}
99+
65100
pub trait PoetryLocator: Send + Sync {
66101
fn find_and_report_missing_envs(
67102
&self,
@@ -203,9 +238,9 @@ impl Locator for Poetry {
203238
// This handles cases where the environment wasn't discovered during find()
204239
// (e.g., workspace directories not configured, or pyproject.toml not found)
205240
if let Some(prefix) = &env.prefix {
206-
if is_poetry_environment(prefix) {
241+
if is_poetry_cache_environment(prefix) {
207242
trace!(
208-
"Identified Poetry environment by path pattern: {:?}",
243+
"Identified Poetry environment by cache path pattern: {:?}",
209244
prefix
210245
);
211246
return environment::create_poetry_env(
@@ -214,6 +249,21 @@ impl Locator for Poetry {
214249
None, // No manager available in this fallback case
215250
);
216251
}
252+
253+
// Check for in-project .venv Poetry environment
254+
if is_in_project_poetry_environment(prefix) {
255+
trace!(
256+
"Identified in-project Poetry environment: {:?}",
257+
prefix
258+
);
259+
// For in-project .venv, the project directory is the parent
260+
let project_dir = prefix.parent().unwrap_or(prefix).to_path_buf();
261+
return environment::create_poetry_env(
262+
prefix,
263+
project_dir,
264+
None, // No manager available in this fallback case
265+
);
266+
}
217267
}
218268

219269
None

crates/pet-poetry/tests/path_identification_test.rs

Lines changed: 166 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,24 @@
77
//! - Workspace directories are not configured
88
//! - The pyproject.toml is not in the workspace directories
99
//! - The environment is in the Poetry cache but wasn't enumerated
10+
//! - The environment is an in-project .venv with virtualenvs.in-project = true
1011
//!
11-
//! The fix adds a fallback path-based detection that checks if the environment
12-
//! path matches Poetry's naming pattern ({name}-{8-char-hash}-py{version}) and
13-
//! is located in a Poetry cache directory (containing "pypoetry/virtualenvs").
12+
//! The fix adds fallback path-based detection that checks:
13+
//! 1. If the environment path matches Poetry's cache naming pattern
14+
//! ({name}-{8-char-hash}-py{version}) in "pypoetry/virtualenvs"
15+
//! 2. If the environment is an in-project .venv with a pyproject.toml
16+
//! containing Poetry configuration
1417
18+
use std::fs;
1519
use std::path::PathBuf;
1620

1721
#[cfg(test)]
1822
mod tests {
1923
use super::*;
2024

21-
// Helper function to test the regex pattern matching
25+
// Helper function to test the regex pattern matching for cache environments
2226
// This tests the core logic without needing actual filesystem structures
23-
fn test_poetry_path_pattern(path_str: &str) -> bool {
27+
fn test_poetry_cache_path_pattern(path_str: &str) -> bool {
2428
use regex::Regex;
2529
let path = PathBuf::from(path_str);
2630
let path_str = path.to_str().unwrap_or_default();
@@ -34,70 +38,215 @@ mod tests {
3438
false
3539
}
3640

41+
// Helper function to test in-project poetry environment detection
42+
// Requires actual filesystem structure
43+
fn test_in_project_poetry_env(path: &std::path::Path) -> bool {
44+
// Check if this is a .venv directory
45+
let dir_name = path
46+
.file_name()
47+
.and_then(|n| n.to_str())
48+
.unwrap_or_default();
49+
if dir_name != ".venv" {
50+
return false;
51+
}
52+
53+
// Check if the parent directory has a pyproject.toml with Poetry configuration
54+
if let Some(parent) = path.parent() {
55+
let pyproject_toml = parent.join("pyproject.toml");
56+
if pyproject_toml.is_file() {
57+
if let Ok(contents) = std::fs::read_to_string(&pyproject_toml) {
58+
if contents.contains("[tool.poetry]")
59+
|| contents.contains("poetry.core.masonry.api")
60+
|| contents.contains("poetry-core")
61+
{
62+
return true;
63+
}
64+
}
65+
}
66+
}
67+
false
68+
}
69+
3770
#[test]
3871
fn test_poetry_path_pattern_macos() {
39-
assert!(test_poetry_path_pattern(
72+
assert!(test_poetry_cache_path_pattern(
4073
"/Users/eleanorboyd/Library/Caches/pypoetry/virtualenvs/nestedpoetry-yJwtIF_Q-py3.11"
4174
));
4275
}
4376

4477
#[test]
4578
fn test_poetry_path_pattern_linux() {
46-
assert!(test_poetry_path_pattern(
79+
assert!(test_poetry_cache_path_pattern(
4780
"/home/user/.cache/pypoetry/virtualenvs/myproject-a1B2c3D4-py3.10"
4881
));
4982
}
5083

5184
#[test]
5285
fn test_poetry_path_pattern_windows() {
53-
assert!(test_poetry_path_pattern(
86+
assert!(test_poetry_cache_path_pattern(
5487
r"C:\Users\user\AppData\Local\pypoetry\Cache\virtualenvs\myproject-f7sQRtG5-py3.11"
5588
));
5689
}
5790

5891
#[test]
5992
fn test_poetry_path_pattern_no_version() {
60-
assert!(test_poetry_path_pattern(
93+
assert!(test_poetry_cache_path_pattern(
6194
"/home/user/.cache/pypoetry/virtualenvs/testproject-XyZ12345-py"
6295
));
6396
}
6497

6598
#[test]
6699
fn test_non_poetry_path_rejected() {
67-
assert!(!test_poetry_path_pattern("/home/user/projects/myenv"));
68-
assert!(!test_poetry_path_pattern("/home/user/.venv"));
69-
assert!(!test_poetry_path_pattern("/usr/local/venv"));
100+
assert!(!test_poetry_cache_path_pattern("/home/user/projects/myenv"));
101+
assert!(!test_poetry_cache_path_pattern("/home/user/.venv"));
102+
assert!(!test_poetry_cache_path_pattern("/usr/local/venv"));
70103
}
71104

72105
#[test]
73106
fn test_poetry_path_without_pypoetry_rejected() {
74107
// Should reject paths that look like the pattern but aren't in pypoetry directory
75-
assert!(!test_poetry_path_pattern(
108+
assert!(!test_poetry_cache_path_pattern(
76109
"/home/user/virtualenvs/myproject-a1B2c3D4-py3.10"
77110
));
78111
}
79112

80113
#[test]
81114
fn test_poetry_path_wrong_hash_length_rejected() {
82115
// Hash should be exactly 8 characters
83-
assert!(!test_poetry_path_pattern(
116+
assert!(!test_poetry_cache_path_pattern(
84117
"/home/user/.cache/pypoetry/virtualenvs/myproject-a1B2c3D456-py3.10"
85118
));
86-
assert!(!test_poetry_path_pattern(
119+
assert!(!test_poetry_cache_path_pattern(
87120
"/home/user/.cache/pypoetry/virtualenvs/myproject-a1B2c3-py3.10"
88121
));
89122
}
90123

91124
#[test]
92125
fn test_real_world_poetry_paths() {
93126
// Test actual Poetry paths from the bug report and real usage
94-
assert!(test_poetry_path_pattern(
127+
assert!(test_poetry_cache_path_pattern(
95128
"/Users/eleanorboyd/Library/Caches/pypoetry/virtualenvs/nestedpoetry-yJwtIF_Q-py3.11"
96129
));
97130

98131
// Another real-world example from documentation
99-
assert!(test_poetry_path_pattern(
132+
assert!(test_poetry_cache_path_pattern(
100133
"/Users/donjayamanne/.cache/pypoetry/virtualenvs/poetry-demo-gNT2WXAV-py3.9"
101134
));
102135
}
136+
137+
// Tests for in-project Poetry environment detection (issue #282)
138+
139+
#[test]
140+
fn test_in_project_poetry_env_with_tool_poetry() {
141+
let temp_dir = tempfile::tempdir().unwrap();
142+
let project_dir = temp_dir.path();
143+
let venv_dir = project_dir.join(".venv");
144+
145+
// Create .venv directory
146+
fs::create_dir(&venv_dir).unwrap();
147+
148+
// Create pyproject.toml with [tool.poetry] section
149+
let pyproject_content = r#"
150+
[tool.poetry]
151+
name = "my-project"
152+
version = "0.1.0"
153+
description = ""
154+
authors = ["Test User <test@example.com>"]
155+
156+
[tool.poetry.dependencies]
157+
python = "^3.10"
158+
159+
[build-system]
160+
requires = ["poetry-core"]
161+
build-backend = "poetry.core.masonry.api"
162+
"#;
163+
fs::write(project_dir.join("pyproject.toml"), pyproject_content).unwrap();
164+
165+
// Test that the .venv is recognized as a Poetry environment
166+
assert!(test_in_project_poetry_env(&venv_dir));
167+
}
168+
169+
#[test]
170+
fn test_in_project_poetry_env_with_poetry_core_backend() {
171+
let temp_dir = tempfile::tempdir().unwrap();
172+
let project_dir = temp_dir.path();
173+
let venv_dir = project_dir.join(".venv");
174+
175+
// Create .venv directory
176+
fs::create_dir(&venv_dir).unwrap();
177+
178+
// Create pyproject.toml with poetry.core.masonry.api as build backend
179+
let pyproject_content = r#"
180+
[project]
181+
name = "my-project"
182+
version = "0.1.0"
183+
184+
[build-system]
185+
requires = ["poetry-core>=1.0.0"]
186+
build-backend = "poetry.core.masonry.api"
187+
"#;
188+
fs::write(project_dir.join("pyproject.toml"), pyproject_content).unwrap();
189+
190+
// Test that the .venv is recognized as a Poetry environment
191+
assert!(test_in_project_poetry_env(&venv_dir));
192+
}
193+
194+
#[test]
195+
fn test_in_project_non_poetry_env_rejected() {
196+
let temp_dir = tempfile::tempdir().unwrap();
197+
let project_dir = temp_dir.path();
198+
let venv_dir = project_dir.join(".venv");
199+
200+
// Create .venv directory
201+
fs::create_dir(&venv_dir).unwrap();
202+
203+
// Create pyproject.toml without Poetry configuration
204+
let pyproject_content = r#"
205+
[project]
206+
name = "my-project"
207+
version = "0.1.0"
208+
209+
[build-system]
210+
requires = ["setuptools>=45"]
211+
build-backend = "setuptools.build_meta"
212+
"#;
213+
fs::write(project_dir.join("pyproject.toml"), pyproject_content).unwrap();
214+
215+
// Test that the .venv is NOT recognized as a Poetry environment
216+
assert!(!test_in_project_poetry_env(&venv_dir));
217+
}
218+
219+
#[test]
220+
fn test_in_project_env_no_pyproject_rejected() {
221+
let temp_dir = tempfile::tempdir().unwrap();
222+
let project_dir = temp_dir.path();
223+
let venv_dir = project_dir.join(".venv");
224+
225+
// Create .venv directory without pyproject.toml
226+
fs::create_dir(&venv_dir).unwrap();
227+
228+
// Test that the .venv is NOT recognized as a Poetry environment
229+
assert!(!test_in_project_poetry_env(&venv_dir));
230+
}
231+
232+
#[test]
233+
fn test_non_venv_directory_rejected() {
234+
let temp_dir = tempfile::tempdir().unwrap();
235+
let project_dir = temp_dir.path();
236+
let custom_venv = project_dir.join("myenv");
237+
238+
// Create custom env directory (not named .venv)
239+
fs::create_dir(&custom_venv).unwrap();
240+
241+
// Create pyproject.toml with Poetry configuration
242+
let pyproject_content = r#"
243+
[tool.poetry]
244+
name = "my-project"
245+
version = "0.1.0"
246+
"#;
247+
fs::write(project_dir.join("pyproject.toml"), pyproject_content).unwrap();
248+
249+
// Test that non-.venv directories are NOT recognized
250+
assert!(!test_in_project_poetry_env(&custom_venv));
251+
}
103252
}

0 commit comments

Comments
 (0)