@@ -46,10 +46,7 @@ impl Locator for MacPythonOrg {
4646 }
4747
4848 let mut executable = resolve_symlink ( & env. executable ) . unwrap_or ( env. executable . clone ( ) ) ;
49- if !executable
50- . to_string_lossy ( )
51- . starts_with ( "/Library/Frameworks/Python.framework/Versions/" )
52- {
49+ if !is_mac_python_org_framework_path ( & executable) {
5350 return None ;
5451 }
5552
@@ -166,3 +163,197 @@ impl Locator for MacPythonOrg {
166163 }
167164 }
168165}
166+
167+ fn is_mac_python_org_framework_path ( executable : & std:: path:: Path ) -> bool {
168+ let Ok ( framework_entry) =
169+ executable. strip_prefix ( "/Library/Frameworks/Python.framework/Versions" )
170+ else {
171+ return false ;
172+ } ;
173+
174+ let mut framework_parts = framework_entry. components ( ) ;
175+ matches ! (
176+ framework_parts. next( ) ,
177+ Some ( std:: path:: Component :: Normal ( version) )
178+ if version. to_str( ) . is_some_and( is_macos_framework_version_dir)
179+ ) && matches ! (
180+ framework_parts. next( ) ,
181+ Some ( std:: path:: Component :: Normal ( part) ) if part == std:: ffi:: OsStr :: new( "bin" )
182+ ) && matches ! (
183+ framework_parts. next( ) ,
184+ Some ( std:: path:: Component :: Normal ( executable_name) )
185+ if executable_name. to_str( ) . is_some_and( is_macos_python_executable_name)
186+ ) && framework_parts. next ( ) . is_none ( )
187+ }
188+
189+ fn is_macos_python_executable_name ( executable : & str ) -> bool {
190+ if executable == "python" || executable == "python3" {
191+ return true ;
192+ }
193+
194+ let Some ( minor) = executable. strip_prefix ( "python3." ) else {
195+ return false ;
196+ } ;
197+
198+ !minor. is_empty ( ) && minor. chars ( ) . all ( |ch| ch. is_ascii_digit ( ) )
199+ }
200+
201+ fn is_macos_framework_version_dir ( version : & str ) -> bool {
202+ if version == "Current" {
203+ return true ;
204+ }
205+
206+ let mut parts = version. split ( '.' ) ;
207+ parts
208+ . next ( )
209+ . is_some_and ( |major| !major. is_empty ( ) && major. chars ( ) . all ( |ch| ch. is_ascii_digit ( ) ) )
210+ && parts
211+ . next ( )
212+ . is_some_and ( |minor| !minor. is_empty ( ) && minor. chars ( ) . all ( |ch| ch. is_ascii_digit ( ) ) )
213+ && parts. next ( ) . is_none ( )
214+ }
215+
216+ #[ cfg( test) ]
217+ mod tests {
218+ use super :: * ;
219+ use pet_core:: Locator ;
220+ use std:: path:: Path ;
221+
222+ #[ test]
223+ fn locator_metadata_matches_python_org_kind ( ) {
224+ let locator = MacPythonOrg :: new ( ) ;
225+
226+ assert_eq ! ( locator. get_kind( ) , LocatorKind :: MacPythonOrg ) ;
227+ assert_eq ! (
228+ locator. supported_categories( ) ,
229+ vec![ PythonEnvironmentKind :: MacPythonOrg ]
230+ ) ;
231+ }
232+
233+ #[ test]
234+ fn framework_path_accepts_versioned_python3 ( ) {
235+ assert ! ( is_mac_python_org_framework_path( Path :: new(
236+ "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3"
237+ ) ) ) ;
238+ }
239+
240+ #[ test]
241+ fn framework_path_accepts_unversioned_python ( ) {
242+ assert ! ( is_mac_python_org_framework_path( Path :: new(
243+ "/Library/Frameworks/Python.framework/Versions/3.12/bin/python"
244+ ) ) ) ;
245+ }
246+
247+ #[ test]
248+ fn framework_path_accepts_versioned_python_executable ( ) {
249+ assert ! ( is_mac_python_org_framework_path( Path :: new(
250+ "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3.12"
251+ ) ) ) ;
252+ }
253+
254+ #[ test]
255+ fn framework_path_accepts_current_python ( ) {
256+ assert ! ( is_mac_python_org_framework_path( Path :: new(
257+ "/Library/Frameworks/Python.framework/Versions/Current/bin/python3"
258+ ) ) ) ;
259+ }
260+
261+ #[ test]
262+ fn framework_path_rejects_non_python_file ( ) {
263+ assert ! ( !is_mac_python_org_framework_path( Path :: new(
264+ "/Library/Frameworks/Python.framework/Versions/3.12/Resources/Info.plist"
265+ ) ) ) ;
266+ }
267+
268+ #[ test]
269+ fn framework_path_rejects_python_config_script ( ) {
270+ assert ! ( !is_mac_python_org_framework_path( Path :: new(
271+ "/Library/Frameworks/Python.framework/Versions/3.12/bin/python-config"
272+ ) ) ) ;
273+ }
274+
275+ #[ test]
276+ fn framework_path_rejects_versioned_python_config_script ( ) {
277+ assert ! ( !is_mac_python_org_framework_path( Path :: new(
278+ "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3.12-config"
279+ ) ) ) ;
280+ }
281+
282+ #[ test]
283+ fn framework_path_rejects_patch_version_python_name ( ) {
284+ assert ! ( !is_mac_python_org_framework_path( Path :: new(
285+ "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3.12.0"
286+ ) ) ) ;
287+ }
288+
289+ #[ test]
290+ fn framework_path_rejects_compact_version_python_name ( ) {
291+ assert ! ( !is_mac_python_org_framework_path( Path :: new(
292+ "/Library/Frameworks/Python.framework/Versions/3.12/bin/python312"
293+ ) ) ) ;
294+ }
295+
296+ #[ test]
297+ fn framework_path_rejects_python2_name ( ) {
298+ assert ! ( !is_mac_python_org_framework_path( Path :: new(
299+ "/Library/Frameworks/Python.framework/Versions/3.12/bin/python2"
300+ ) ) ) ;
301+ }
302+
303+ #[ test]
304+ fn framework_path_rejects_patch_version_dir ( ) {
305+ assert ! ( !is_mac_python_org_framework_path( Path :: new(
306+ "/Library/Frameworks/Python.framework/Versions/3.12.0/bin/python3"
307+ ) ) ) ;
308+ }
309+
310+ #[ test]
311+ fn framework_path_rejects_invalid_version_dir ( ) {
312+ assert ! ( !is_mac_python_org_framework_path( Path :: new(
313+ "/Library/Frameworks/Python.framework/Versions/Foo/bin/python3"
314+ ) ) ) ;
315+ }
316+
317+ #[ test]
318+ fn framework_path_rejects_other_framework ( ) {
319+ assert ! ( !is_mac_python_org_framework_path( Path :: new(
320+ "/Library/Frameworks/Other.framework/Versions/3.12/bin/python3"
321+ ) ) ) ;
322+ }
323+
324+ #[ test]
325+ fn framework_path_rejects_non_library_path ( ) {
326+ assert ! ( !is_mac_python_org_framework_path( Path :: new(
327+ "/tmp/Python.framework/Versions/3.12/bin/python3"
328+ ) ) ) ;
329+ }
330+
331+ #[ test]
332+ fn framework_path_rejects_homebrew_framework_path ( ) {
333+ assert ! ( !is_mac_python_org_framework_path( Path :: new(
334+ "/opt/homebrew/Cellar/python@3.12/3.12.1/Frameworks/Python.framework/Versions/3.12/bin/python3"
335+ ) ) ) ;
336+ }
337+
338+ #[ test]
339+ fn framework_path_rejects_nested_bin_entry ( ) {
340+ assert ! ( !is_mac_python_org_framework_path( Path :: new(
341+ "/Library/Frameworks/Python.framework/Versions/3.12/bin/nested/python3"
342+ ) ) ) ;
343+ }
344+
345+ #[ cfg( not( target_os = "macos" ) ) ]
346+ #[ test]
347+ fn try_from_rejects_python_org_path_off_macos ( ) {
348+ let locator = MacPythonOrg :: new ( ) ;
349+ let env = PythonEnv :: new (
350+ PathBuf :: from ( "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3" ) ,
351+ Some ( PathBuf :: from (
352+ "/Library/Frameworks/Python.framework/Versions/3.12" ,
353+ ) ) ,
354+ Some ( "3.12.0" . to_string ( ) ) ,
355+ ) ;
356+
357+ assert ! ( locator. try_from( & env) . is_none( ) ) ;
358+ }
359+ }
0 commit comments