From 4c9314e07a1b0ba9d892e937994922c845cb0b7f Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 15 Apr 2026 16:19:30 -0700 Subject: [PATCH 1/2] test: add Mac python.org locator coverage (Refs #389) --- crates/pet-mac-python-org/src/lib.rs | 143 ++++++++++++++++++++++++++- 1 file changed, 139 insertions(+), 4 deletions(-) diff --git a/crates/pet-mac-python-org/src/lib.rs b/crates/pet-mac-python-org/src/lib.rs index 73642ef1..f69a26b9 100644 --- a/crates/pet-mac-python-org/src/lib.rs +++ b/crates/pet-mac-python-org/src/lib.rs @@ -46,10 +46,7 @@ impl Locator for MacPythonOrg { } let mut executable = resolve_symlink(&env.executable).unwrap_or(env.executable.clone()); - if !executable - .to_string_lossy() - .starts_with("/Library/Frameworks/Python.framework/Versions/") - { + if !is_mac_python_org_framework_path(&executable) { return None; } @@ -166,3 +163,141 @@ impl Locator for MacPythonOrg { } } } + +fn is_mac_python_org_framework_path(executable: &std::path::Path) -> bool { + let executable = executable.to_string_lossy(); + let Some(framework_entry) = + executable.strip_prefix("/Library/Frameworks/Python.framework/Versions/") + else { + return false; + }; + + let mut framework_parts = framework_entry.split('/'); + framework_parts + .next() + .is_some_and(|version| !version.is_empty()) + && framework_parts.next() == Some("bin") + && framework_parts + .next() + .is_some_and(is_macos_python_executable_name) + && framework_parts.next().is_none() +} + +fn is_macos_python_executable_name(executable: &str) -> bool { + let Some(version) = executable.strip_prefix("python") else { + return false; + }; + + if version.is_empty() { + return true; + } + + version.chars().any(|ch| ch.is_ascii_digit()) + && !version.starts_with('.') + && !version.ends_with('.') + && !version.contains("..") + && version.chars().all(|ch| ch.is_ascii_digit() || ch == '.') +} + +#[cfg(test)] +mod tests { + use super::*; + use pet_core::Locator; + use std::path::Path; + + #[test] + fn locator_metadata_matches_python_org_kind() { + let locator = MacPythonOrg::new(); + + assert_eq!(locator.get_kind(), LocatorKind::MacPythonOrg); + assert_eq!( + locator.supported_categories(), + vec![PythonEnvironmentKind::MacPythonOrg] + ); + } + + #[test] + fn framework_path_accepts_versioned_python3() { + assert!(is_mac_python_org_framework_path(Path::new( + "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3" + ))); + } + + #[test] + fn framework_path_accepts_versioned_python_executable() { + assert!(is_mac_python_org_framework_path(Path::new( + "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3.12" + ))); + } + + #[test] + fn framework_path_accepts_current_python() { + assert!(is_mac_python_org_framework_path(Path::new( + "/Library/Frameworks/Python.framework/Versions/Current/bin/python3" + ))); + } + + #[test] + fn framework_path_rejects_non_python_file() { + assert!(!is_mac_python_org_framework_path(Path::new( + "/Library/Frameworks/Python.framework/Versions/3.12/Resources/Info.plist" + ))); + } + + #[test] + fn framework_path_rejects_python_config_script() { + assert!(!is_mac_python_org_framework_path(Path::new( + "/Library/Frameworks/Python.framework/Versions/3.12/bin/python-config" + ))); + } + + #[test] + fn framework_path_rejects_versioned_python_config_script() { + assert!(!is_mac_python_org_framework_path(Path::new( + "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3.12-config" + ))); + } + + #[test] + fn framework_path_rejects_other_framework() { + assert!(!is_mac_python_org_framework_path(Path::new( + "/Library/Frameworks/Other.framework/Versions/3.12/bin/python3" + ))); + } + + #[test] + fn framework_path_rejects_non_library_path() { + assert!(!is_mac_python_org_framework_path(Path::new( + "/tmp/Python.framework/Versions/3.12/bin/python3" + ))); + } + + #[test] + fn framework_path_rejects_homebrew_framework_path() { + assert!(!is_mac_python_org_framework_path(Path::new( + "/opt/homebrew/Cellar/python@3.12/3.12.1/Frameworks/Python.framework/Versions/3.12/bin/python3" + ))); + } + + #[test] + fn framework_path_rejects_nested_bin_entry() { + assert!(!is_mac_python_org_framework_path(Path::new( + "/Library/Frameworks/Python.framework/Versions/3.12/bin/nested/python3" + ))); + } + + #[cfg(not(target_os = "macos"))] + #[test] + fn try_from_rejects_python_org_path_off_macos() { + let locator = MacPythonOrg::new(); + let env = PythonEnv::new( + PathBuf::from("/Library/Frameworks/Python.framework/Versions/3.12/bin/python3"), + Some(PathBuf::from( + "/Library/Frameworks/Python.framework/Versions/3.12", + )), + Some("3.12.0".to_string()), + ); + + assert!(locator.try_from(&env).is_none()); + } +} From 7cd6bd5c12f351ed3a70651a8464177d5c6a1959 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 16 Apr 2026 22:09:54 -0700 Subject: [PATCH 2/2] fix: tighten Mac python.org path matching (PR #430) --- crates/pet-mac-python-org/src/lib.rs | 94 ++++++++++++++++++++++------ 1 file changed, 75 insertions(+), 19 deletions(-) diff --git a/crates/pet-mac-python-org/src/lib.rs b/crates/pet-mac-python-org/src/lib.rs index f69a26b9..a477f0c6 100644 --- a/crates/pet-mac-python-org/src/lib.rs +++ b/crates/pet-mac-python-org/src/lib.rs @@ -165,38 +165,52 @@ impl Locator for MacPythonOrg { } fn is_mac_python_org_framework_path(executable: &std::path::Path) -> bool { - let executable = executable.to_string_lossy(); - let Some(framework_entry) = - executable.strip_prefix("/Library/Frameworks/Python.framework/Versions/") + let Ok(framework_entry) = + executable.strip_prefix("/Library/Frameworks/Python.framework/Versions") else { return false; }; - let mut framework_parts = framework_entry.split('/'); - framework_parts - .next() - .is_some_and(|version| !version.is_empty()) - && framework_parts.next() == Some("bin") - && framework_parts - .next() - .is_some_and(is_macos_python_executable_name) - && framework_parts.next().is_none() + let mut framework_parts = framework_entry.components(); + matches!( + framework_parts.next(), + Some(std::path::Component::Normal(version)) + if version.to_str().is_some_and(is_macos_framework_version_dir) + ) && matches!( + framework_parts.next(), + Some(std::path::Component::Normal(part)) if part == std::ffi::OsStr::new("bin") + ) && matches!( + framework_parts.next(), + Some(std::path::Component::Normal(executable_name)) + if executable_name.to_str().is_some_and(is_macos_python_executable_name) + ) && framework_parts.next().is_none() } fn is_macos_python_executable_name(executable: &str) -> bool { - let Some(version) = executable.strip_prefix("python") else { + if executable == "python" || executable == "python3" { + return true; + } + + let Some(minor) = executable.strip_prefix("python3.") else { return false; }; - if version.is_empty() { + !minor.is_empty() && minor.chars().all(|ch| ch.is_ascii_digit()) +} + +fn is_macos_framework_version_dir(version: &str) -> bool { + if version == "Current" { return true; } - version.chars().any(|ch| ch.is_ascii_digit()) - && !version.starts_with('.') - && !version.ends_with('.') - && !version.contains("..") - && version.chars().all(|ch| ch.is_ascii_digit() || ch == '.') + let mut parts = version.split('.'); + parts + .next() + .is_some_and(|major| !major.is_empty() && major.chars().all(|ch| ch.is_ascii_digit())) + && parts + .next() + .is_some_and(|minor| !minor.is_empty() && minor.chars().all(|ch| ch.is_ascii_digit())) + && parts.next().is_none() } #[cfg(test)] @@ -223,6 +237,13 @@ mod tests { ))); } + #[test] + fn framework_path_accepts_unversioned_python() { + assert!(is_mac_python_org_framework_path(Path::new( + "/Library/Frameworks/Python.framework/Versions/3.12/bin/python" + ))); + } + #[test] fn framework_path_accepts_versioned_python_executable() { assert!(is_mac_python_org_framework_path(Path::new( @@ -258,6 +279,41 @@ mod tests { ))); } + #[test] + fn framework_path_rejects_patch_version_python_name() { + assert!(!is_mac_python_org_framework_path(Path::new( + "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3.12.0" + ))); + } + + #[test] + fn framework_path_rejects_compact_version_python_name() { + assert!(!is_mac_python_org_framework_path(Path::new( + "/Library/Frameworks/Python.framework/Versions/3.12/bin/python312" + ))); + } + + #[test] + fn framework_path_rejects_python2_name() { + assert!(!is_mac_python_org_framework_path(Path::new( + "/Library/Frameworks/Python.framework/Versions/3.12/bin/python2" + ))); + } + + #[test] + fn framework_path_rejects_patch_version_dir() { + assert!(!is_mac_python_org_framework_path(Path::new( + "/Library/Frameworks/Python.framework/Versions/3.12.0/bin/python3" + ))); + } + + #[test] + fn framework_path_rejects_invalid_version_dir() { + assert!(!is_mac_python_org_framework_path(Path::new( + "/Library/Frameworks/Python.framework/Versions/Foo/bin/python3" + ))); + } + #[test] fn framework_path_rejects_other_framework() { assert!(!is_mac_python_org_framework_path(Path::new(