@@ -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`]).
430439async 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]\n name = \" 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]\n name = \" 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 ! (
0 commit comments