Skip to content

Commit 5ecac79

Browse files
committed
Respect Poetry and PDM venv order in env probes
A Poetry project without poetry.lock ([tool.poetry] or poetry.toml) is Poetry's, so an ambient UV_PROJECT_ENVIRONMENT no longer takes it over. With no saved PDM interpreter, PDM uses an activated or in-project venv before PEP 582, so __pypackages__ is now the last resort instead of beating VIRTUAL_ENV and ./.venv. Found in review. Assisted-by: Claude Code:claude-opus-5-5
1 parent 60dfb81 commit 5ecac79

2 files changed

Lines changed: 96 additions & 27 deletions

File tree

‎crates/socket-patch-core/src/crawlers/python_crawler.rs‎

Lines changed: 94 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,13 @@ async fn find_local_venv_site_packages_with(
397397
results.extend(matches);
398398
}
399399

400+
// 5. A PDM project with no recorded interpreter and no venv for PDM to
401+
// pick (an activated one, `./.venv`) is a PEP 582 project (PDM 1.x's
402+
// default): its packages live in `__pypackages__/<X.Y>/lib`.
403+
if results.is_empty() && pdm_drives_project(cwd).await {
404+
results = pdm_pep582_dirs(cwd).await;
405+
}
406+
400407
results
401408
}
402409

@@ -424,25 +431,16 @@ async fn package_manager_recorded_site_packages(
424431
uv_project_environment_site_packages(cwd, var).await
425432
}
426433

427-
/// PDM's env for `cwd` (see [`package_manager_recorded_site_packages`]).
428-
/// `PDM_PYTHON` outranks the saved interpreter, and
429-
/// `PDM_IGNORE_SAVED_PYTHON` makes PDM disregard the saved one.
434+
/// The env of PDM's interpreter for `cwd` (see
435+
/// [`package_manager_recorded_site_packages`]): `PDM_PYTHON`, else the saved
436+
/// one unless `PDM_IGNORE_SAVED_PYTHON`. With neither, PDM picks an active
437+
/// or project venv first, so the generic probes decide (PEP 582 is their
438+
/// last resort, see [`find_local_venv_site_packages_with`]).
430439
async fn pdm_project_site_packages(
431440
cwd: &Path,
432441
var: &impl Fn(&str) -> Option<String>,
433442
) -> Option<Vec<PathBuf>> {
434-
let pep582 = || async {
435-
let found = find_python_dirs(&cwd.join("__pypackages__"), &["*", "lib"]).await;
436-
(!found.is_empty()).then_some(found)
437-
};
438-
// `uv.lock` and `poetry.lock` drive installs ahead of `pdm.lock` (the
439-
// hosted rewriters' precedence), so a leftover PDM record next to one
440-
// is not where the project is installed.
441-
if cwd.join("uv.lock").is_file() || cwd.join("poetry.lock").is_file() {
442-
return None;
443-
}
444-
let pdm_project = cwd.join(".pdm-python").is_file() || is_pdm_project(cwd).await;
445-
if !pdm_project {
443+
if !pdm_drives_project(cwd).await {
446444
return None;
447445
}
448446
// `PDM_PYTHON` outranks the saved interpreter.
@@ -455,16 +453,27 @@ async fn pdm_project_site_packages(
455453
None if ignore_saved => None,
456454
None => pdm_saved_interpreter(cwd).await,
457455
};
458-
match interpreter {
459-
Some(python) => match venv_root_of_interpreter(&python) {
460-
Some(root) => {
461-
let found = find_site_packages_under(&root, "site-packages").await;
462-
(!found.is_empty()).then_some(found)
463-
}
464-
None => pep582().await,
465-
},
466-
None => pep582().await,
456+
let found = match venv_root_of_interpreter(&interpreter?) {
457+
Some(root) => find_site_packages_under(&root, "site-packages").await,
458+
None => pdm_pep582_dirs(cwd).await,
459+
};
460+
(!found.is_empty()).then_some(found)
461+
}
462+
463+
/// Whether PDM installs the project at `cwd`: a PDM project (see
464+
/// [`is_pdm_project`], or a `.pdm-python`) with no `uv.lock` or
465+
/// `poetry.lock`, which drive installs ahead of `pdm.lock` (the hosted
466+
/// rewriters' precedence).
467+
async fn pdm_drives_project(cwd: &Path) -> bool {
468+
if cwd.join("uv.lock").is_file() || cwd.join("poetry.lock").is_file() {
469+
return false;
467470
}
471+
cwd.join(".pdm-python").is_file() || is_pdm_project(cwd).await
472+
}
473+
474+
/// PEP 582 package dirs: `__pypackages__/<X.Y>/lib`.
475+
async fn pdm_pep582_dirs(cwd: &Path) -> Vec<PathBuf> {
476+
find_python_dirs(&cwd.join("__pypackages__"), &["*", "lib"]).await
468477
}
469478

470479
/// The interpreter PDM saved for `cwd`: `.pdm-python` (PDM 2.x), else
@@ -518,15 +527,21 @@ async fn uv_project_environment_site_packages(
518527
var: &impl Fn(&str) -> Option<String>,
519528
) -> Option<Vec<PathBuf>> {
520529
let env = var("UV_PROJECT_ENVIRONMENT").filter(|v| !v.trim().is_empty())?;
521-
let other_manager = [
530+
let other_lock = [
522531
"poetry.lock",
532+
"poetry.toml",
523533
"pdm.lock",
524534
".pdm-python",
525535
"Pipfile",
526536
"Pipfile.lock",
527537
]
528538
.iter()
529539
.any(|marker| cwd.join(marker).exists());
540+
// A lockless Poetry project is still Poetry's (`[tool.poetry]`).
541+
let other_manager = other_lock
542+
|| read_regular_to_string(&cwd.join("pyproject.toml"))
543+
.await
544+
.is_ok_and(|text| text.contains("[tool.poetry"));
530545
let uv_project =
531546
cwd.join("uv.lock").is_file() || (cwd.join("pyproject.toml").is_file() && !other_manager);
532547
if !uv_project {
@@ -2365,6 +2380,37 @@ mod tests {
23652380
vec![lib.clone()]
23662381
);
23672382

2383+
// With no saved interpreter (or one PDM ignores), PDM picks an
2384+
// activated venv or `./.venv` before PEP 582.
2385+
let other = tempfile::tempdir().unwrap();
2386+
let active_site = fake_venv(other.path(), "active");
2387+
let active = env_of(&[(
2388+
"VIRTUAL_ENV",
2389+
other.path().join("active").to_string_lossy().into_owned(),
2390+
)]);
2391+
assert_eq!(
2392+
find_local_venv_site_packages_with(&project, &active).await,
2393+
vec![active_site]
2394+
);
2395+
let dot_venv = fake_venv(&project, ".venv");
2396+
assert_eq!(
2397+
find_local_venv_site_packages_with(&project, &no_env).await,
2398+
vec![dot_venv]
2399+
);
2400+
std::fs::write(project.join(".pdm-python"), base.display().to_string()).unwrap();
2401+
let ignored = env_of(&[("PDM_IGNORE_SAVED_PYTHON", "1".to_string())]);
2402+
assert_eq!(
2403+
find_local_venv_site_packages_with(&project, &ignored).await,
2404+
vec![fake_venv(&project, ".venv")]
2405+
);
2406+
// ...while a saved base interpreter still means PEP 582.
2407+
assert_eq!(
2408+
find_local_venv_site_packages_with(&project, &no_env).await,
2409+
vec![lib.clone()]
2410+
);
2411+
std::fs::remove_file(project.join(".pdm-python")).unwrap();
2412+
std::fs::remove_dir_all(project.join(".venv")).unwrap();
2413+
23682414
// A uv project with a leftover PDM lock is uv's, not PEP 582.
23692415
std::fs::write(project.join("uv.lock"), "version = 1\n").unwrap();
23702416
assert!(find_local_venv_site_packages_with(&project, &no_env)
@@ -2426,6 +2472,29 @@ mod tests {
24262472
vec![abs_site]
24272473
);
24282474

2475+
// A lockless Poetry project (`[tool.poetry]`, or `poetry.toml`) is
2476+
// Poetry's: an ambient UV_PROJECT_ENVIRONMENT does not take it over.
2477+
std::fs::write(
2478+
project.join("pyproject.toml"),
2479+
"[tool.poetry]\nname = \"app\"\n",
2480+
)
2481+
.unwrap();
2482+
assert_eq!(
2483+
find_local_venv_site_packages_with(&project, &abs_env).await,
2484+
vec![stray.clone()]
2485+
);
2486+
std::fs::write(
2487+
project.join("pyproject.toml"),
2488+
"[project]\nname = \"app\"\n",
2489+
)
2490+
.unwrap();
2491+
std::fs::write(project.join("poetry.toml"), "").unwrap();
2492+
assert_eq!(
2493+
find_local_venv_site_packages_with(&project, &abs_env).await,
2494+
vec![stray.clone()]
2495+
);
2496+
std::fs::remove_file(project.join("poetry.toml")).unwrap();
2497+
24292498
// ...but not a project another manager drives, nor a non-project.
24302499
std::fs::write(project.join("poetry.lock"), "").unwrap();
24312500
assert_eq!(

‎docs/testing/pdm-compatibility.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ Measured details:
7474
`.pdm.toml` on older PDM), ahead of an activated venv or a stray `./.venv`.
7575
A project with `uv.lock` or `poetry.lock` is not treated as PDM's. That covers an out-of-tree
7676
venv (`venv.in_project = false`) and one bound with `pdm use <venv>`. When
77-
the interpreter is a base Python, or a PDM 0.x/1.x project saved none, the
78-
env is `__pypackages__/<X.Y>/lib` (PEP 582; PDM 2.x under
77+
the interpreter is a base Python, or a PDM 0.x/1.x project saved none and
78+
has no activated or in-project venv, the env is `__pypackages__/<X.Y>/lib` (PEP 582; PDM 2.x under
7979
`python.use_venv = false`). Agent mode patches it there, and the hosted
8080
stale-install warning and `vex` check it.
8181
- **A non-default lock filename (`pdm lock -L custom.lock`) is invisible** to the

0 commit comments

Comments
 (0)