@@ -45,14 +45,11 @@ fn is_poetry_cache_environment(path: &Path) -> bool {
4545 // - Linux: ~/.cache/pypoetry/virtualenvs/
4646 // - macOS: ~/Library/Caches/pypoetry/virtualenvs/
4747 // - Windows: %LOCALAPPDATA%\pypoetry\Cache\virtualenvs\
48- let path_str = path. to_str ( ) . unwrap_or_default ( ) ;
49-
50- // Check if path contains typical Poetry cache directory structure
51- if path_str. contains ( "pypoetry" ) && path_str. contains ( "virtualenvs" ) {
48+ if has_poetry_cache_components ( path) {
5249 // Further validate by checking if the directory name matches Poetry's naming pattern
53- // Pattern: {name}-{8-char-hash}-py or just .venv
50+ // Pattern: {name}-{8-char-hash}-py{version}
5451 if let Some ( dir_name) = path. file_name ( ) . and_then ( |n| n. to_str ( ) ) {
55- // Check for Poetry's hash-based naming: name-XXXXXXXX-py
52+ // Check for Poetry's hash-based naming: name-XXXXXXXX-py<major>.<minor>
5653 // The hash is 8 characters of base64url encoding
5754 if POETRY_ENV_NAME_PATTERN . is_match ( dir_name) {
5855 return true ;
@@ -63,6 +60,27 @@ fn is_poetry_cache_environment(path: &Path) -> bool {
6360 false
6461}
6562
63+ fn has_poetry_cache_components ( path : & Path ) -> bool {
64+ let mut found_pypoetry = false ;
65+
66+ for component in path. components ( ) {
67+ let Some ( component) = component. as_os_str ( ) . to_str ( ) else {
68+ return false ;
69+ } ;
70+
71+ if component. eq_ignore_ascii_case ( "pypoetry" ) {
72+ found_pypoetry = true ;
73+ continue ;
74+ }
75+
76+ if found_pypoetry && component. eq_ignore_ascii_case ( "virtualenvs" ) {
77+ return true ;
78+ }
79+ }
80+
81+ false
82+ }
83+
6684/// Check if a .venv directory is an in-project Poetry environment
6785/// This is for the case when virtualenvs.in-project = true is set.
6886/// We check if the parent directory has Poetry configuration files.
@@ -359,6 +377,100 @@ mod tests {
359377 use super :: * ;
360378 use pet_core:: os_environment:: EnvironmentApi ;
361379
380+ fn path_from_components ( components : & [ & str ] ) -> PathBuf {
381+ let mut path = PathBuf :: new ( ) ;
382+ for component in components {
383+ path. push ( component) ;
384+ }
385+ path
386+ }
387+
388+ #[ test]
389+ fn test_poetry_cache_environment_requires_exact_cache_components ( ) {
390+ let path = path_from_components ( & [
391+ "home" ,
392+ "user" ,
393+ ".cache" ,
394+ "pypoetry" ,
395+ "virtualenvs" ,
396+ "project-1a2b3c4d-py3.11" ,
397+ ] ) ;
398+
399+ assert ! ( is_poetry_cache_environment( & path) ) ;
400+ }
401+
402+ #[ test]
403+ fn test_poetry_cache_environment_allows_windows_cache_component ( ) {
404+ let path = path_from_components ( & [
405+ "Users" ,
406+ "user" ,
407+ "AppData" ,
408+ "Local" ,
409+ "pypoetry" ,
410+ "Cache" ,
411+ "virtualenvs" ,
412+ "project-1a2b3c4d-py3.11" ,
413+ ] ) ;
414+
415+ assert ! ( is_poetry_cache_environment( & path) ) ;
416+ }
417+
418+ #[ test]
419+ fn test_poetry_cache_environment_rejects_substring_cache_components ( ) {
420+ let path = path_from_components ( & [
421+ "Users" ,
422+ "pypoetry_user" ,
423+ "virtualenvs_backup" ,
424+ "project-1a2b3c4d-py3.11" ,
425+ ] ) ;
426+
427+ assert ! ( !is_poetry_cache_environment( & path) ) ;
428+ }
429+
430+ #[ test]
431+ fn test_poetry_cache_environment_requires_ordered_cache_components ( ) {
432+ let path = path_from_components ( & [
433+ "home" ,
434+ "user" ,
435+ ".cache" ,
436+ "virtualenvs" ,
437+ "pypoetry" ,
438+ "project-1a2b3c4d-py3.11" ,
439+ ] ) ;
440+
441+ assert ! ( !is_poetry_cache_environment( & path) ) ;
442+ }
443+
444+ #[ test]
445+ fn test_poetry_cache_environment_allows_mixed_case_cache_components ( ) {
446+ let path = path_from_components ( & [
447+ "Users" ,
448+ "user" ,
449+ "AppData" ,
450+ "Local" ,
451+ "PyPoetry" ,
452+ "Cache" ,
453+ "VirtualEnvs" ,
454+ "project-1a2b3c4d-py3.11" ,
455+ ] ) ;
456+
457+ assert ! ( is_poetry_cache_environment( & path) ) ;
458+ }
459+
460+ #[ test]
461+ fn test_poetry_cache_environment_requires_poetry_env_name ( ) {
462+ let path = path_from_components ( & [
463+ "home" ,
464+ "user" ,
465+ ".cache" ,
466+ "pypoetry" ,
467+ "virtualenvs" ,
468+ "not-a-poetry-env" ,
469+ ] ) ;
470+
471+ assert ! ( !is_poetry_cache_environment( & path) ) ;
472+ }
473+
362474 #[ test]
363475 fn test_sync_search_result_from_replaces_cached_result ( ) {
364476 let environment = EnvironmentApi :: new ( ) ;
0 commit comments