Skip to main content

uv_python/
lib.rs

1//! Find requested Python interpreters and query interpreters for information.
2use thiserror::Error;
3
4#[cfg(test)]
5use uv_static::EnvVars;
6
7pub use crate::discovery::{
8    EnvironmentPreference, Error as DiscoveryError, PythonDownloads, PythonNotFound,
9    PythonPreference, PythonRequest, PythonSource, PythonVariant, VersionRequest,
10    find_all_python_installations, find_python_installations,
11};
12pub use crate::downloads::PlatformRequest;
13pub use crate::environment::{InvalidEnvironmentKind, PythonEnvironment};
14pub use crate::implementation::{ImplementationName, LenientImplementationName};
15pub use crate::installation::{
16    PythonInstallation, PythonInstallationKey, PythonInstallationMinorVersionKey,
17};
18pub use crate::interpreter::{
19    BrokenLink, Error as InterpreterError, Interpreter, canonicalize_executable,
20};
21pub use crate::pointer_size::PointerSize;
22pub use crate::prefix::Prefix;
23pub use crate::python_version::{BuildVersionError, PythonVersion};
24pub use crate::target::Target;
25pub use crate::version_files::{
26    DiscoveryOptions as VersionFileDiscoveryOptions, FilePreference as VersionFilePreference,
27    PYTHON_VERSION_FILENAME, PYTHON_VERSIONS_FILENAME, PythonVersionFile,
28};
29pub use crate::virtualenv::{Error as VirtualEnvError, PyVenvConfiguration, VirtualEnvironment};
30
31mod discovery;
32pub mod downloads;
33mod environment;
34mod implementation;
35mod installation;
36mod interpreter;
37pub mod macos_dylib;
38pub mod managed;
39#[cfg(windows)]
40mod microsoft_store;
41mod pointer_size;
42mod prefix;
43mod python_version;
44mod sysconfig;
45mod target;
46mod version_files;
47mod virtualenv;
48#[cfg(windows)]
49pub mod windows_registry;
50
51#[cfg(windows)]
52pub(crate) const COMPANY_KEY: &str = "Astral";
53#[cfg(windows)]
54pub(crate) const COMPANY_DISPLAY_NAME: &str = "Astral Software Inc.";
55
56#[cfg(not(test))]
57fn current_dir() -> Result<std::path::PathBuf, std::io::Error> {
58    std::env::current_dir()
59}
60
61#[cfg(test)]
62fn current_dir() -> Result<std::path::PathBuf, std::io::Error> {
63    std::env::var_os(EnvVars::PWD)
64        .map(std::path::PathBuf::from)
65        .map(Ok)
66        .unwrap_or(std::env::current_dir())
67}
68
69#[derive(Debug, Error)]
70pub enum Error {
71    #[error(transparent)]
72    Io(#[from] std::io::Error),
73
74    #[error(transparent)]
75    VirtualEnv(#[from] virtualenv::Error),
76
77    #[error(transparent)]
78    Query(#[from] interpreter::Error),
79
80    #[error(transparent)]
81    Discovery(#[from] discovery::Error),
82
83    #[error(transparent)]
84    ManagedPython(#[from] managed::Error),
85
86    #[error(transparent)]
87    Download(#[from] downloads::Error),
88
89    #[error(transparent)]
90    ClientBuild(#[from] uv_client::ClientBuildError),
91
92    // TODO(zanieb) We might want to ensure this is always wrapped in another type
93    #[error(transparent)]
94    KeyError(#[from] installation::PythonInstallationKeyError),
95
96    #[error("{}", .0)]
97    MissingPython(PythonNotFound, Option<Box<MissingPythonHint>>),
98
99    #[error(transparent)]
100    MissingEnvironment(#[from] environment::EnvironmentNotFound),
101
102    #[error(transparent)]
103    InvalidEnvironment(#[from] environment::InvalidEnvironment),
104
105    #[error(transparent)]
106    RetryParsing(#[from] uv_client::RetryParsingError),
107}
108
109/// The reason a managed Python download could not be used.
110#[derive(Debug)]
111pub enum MissingPythonHint {
112    /// uv's embedded download metadata may be stale.
113    RequiresUpdate,
114    /// Downloads are set to `manual`.
115    DownloadsManual(PythonRequest),
116    /// Downloads are set to `never`.
117    DownloadsNever(PythonRequest),
118    /// Python preference is set to `only-system`.
119    PreferenceOnlySystem(PythonRequest),
120    /// uv is in offline mode.
121    Offline(PythonRequest),
122}
123
124impl MissingPythonHint {
125    fn for_request(request: &PythonRequest) -> String {
126        match request {
127            PythonRequest::Default | PythonRequest::Any => String::new(),
128            _ => format!(" for {request}"),
129        }
130    }
131}
132
133impl std::fmt::Display for MissingPythonHint {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        match self {
136            Self::RequiresUpdate => {
137                write!(
138                    f,
139                    "uv embeds available Python downloads and may require an update to install new versions. Consider retrying on a newer version of uv."
140                )
141            }
142            Self::DownloadsManual(request) => {
143                write!(
144                    f,
145                    "A managed Python download is available{}, but Python downloads are set to 'manual', use `uv python install {}` to install the required version",
146                    Self::for_request(request),
147                    request.to_canonical_string(),
148                )
149            }
150            Self::DownloadsNever(request) => {
151                write!(
152                    f,
153                    "A managed Python download is available{}, but Python downloads are set to 'never'",
154                    Self::for_request(request),
155                )
156            }
157            Self::PreferenceOnlySystem(request) => {
158                write!(
159                    f,
160                    "A managed Python download is available{}, but the Python preference is set to 'only system'",
161                    Self::for_request(request),
162                )
163            }
164            Self::Offline(request) => {
165                write!(
166                    f,
167                    "A managed Python download is available{}, but uv is set to offline mode",
168                    Self::for_request(request),
169                )
170            }
171        }
172    }
173}
174
175impl uv_errors::Hint for Error {
176    fn hints(&self) -> uv_errors::Hints<'_> {
177        match self {
178            Self::MissingPython(_, Some(hint)) => uv_errors::Hints::from(hint.to_string()),
179            Self::Discovery(err) => err.hints(),
180            _ => uv_errors::Hints::none(),
181        }
182    }
183}
184
185impl Error {
186    fn with_hint(self, hint: MissingPythonHint) -> Self {
187        match self {
188            Self::MissingPython(err, _) => Self::MissingPython(err, Some(Box::new(hint))),
189            _ => self,
190        }
191    }
192}
193
194impl From<PythonNotFound> for Error {
195    fn from(err: PythonNotFound) -> Self {
196        Self::MissingPython(err, None)
197    }
198}
199
200// The mock interpreters are not valid on Windows so we don't have unit test coverage there
201// TODO(zanieb): We should write a mock interpreter script that works on Windows
202#[cfg(all(test, unix))]
203mod tests {
204    use std::{
205        env,
206        ffi::{OsStr, OsString},
207        path::{Path, PathBuf},
208        str::FromStr,
209    };
210
211    use anyhow::Result;
212    use assert_fs::{TempDir, fixture::ChildPath, prelude::*};
213    use indoc::{formatdoc, indoc};
214    use temp_env::with_vars;
215    use test_log::test;
216    use uv_client::BaseClientBuilder;
217    use uv_preview::PreviewFeature;
218    use uv_static::EnvVars;
219
220    use uv_cache::Cache;
221
222    use crate::{
223        PythonDownloads, PythonNotFound, PythonRequest, PythonSource, PythonVersion,
224        find_all_python_installations, find_python_installations,
225        implementation::ImplementationName, installation::PythonInstallation,
226        managed::ManagedPythonInstallations, virtualenv::virtualenv_python_executable,
227    };
228    use crate::{
229        PythonPreference,
230        discovery::{
231            self, EnvironmentPreference, find_best_python_installation, find_python_installation,
232        },
233    };
234
235    struct TestContext {
236        tempdir: TempDir,
237        cache: Cache,
238        installations: ManagedPythonInstallations,
239        search_path: Option<Vec<PathBuf>>,
240        workdir: ChildPath,
241    }
242
243    impl TestContext {
244        fn new() -> Result<Self> {
245            let tempdir = TempDir::new()?;
246            let workdir = tempdir.child("workdir");
247            workdir.create_dir_all()?;
248
249            Ok(Self {
250                tempdir,
251                cache: Cache::temp()?,
252                installations: ManagedPythonInstallations::temp()?,
253                search_path: None,
254                workdir,
255            })
256        }
257
258        /// Clear the search path.
259        fn reset_search_path(&mut self) {
260            self.search_path = None;
261        }
262
263        /// Add a directory to the search path.
264        fn add_to_search_path(&mut self, path: PathBuf) {
265            match self.search_path.as_mut() {
266                Some(paths) => paths.push(path),
267                None => self.search_path = Some(vec![path]),
268            }
269        }
270
271        /// Create a new directory and add it to the search path.
272        fn new_search_path_directory(&mut self, name: impl AsRef<Path>) -> Result<ChildPath> {
273            let child = self.tempdir.child(name);
274            child.create_dir_all()?;
275            self.add_to_search_path(child.to_path_buf());
276            Ok(child)
277        }
278
279        fn run<F, R>(&self, closure: F) -> R
280        where
281            F: FnOnce() -> R,
282        {
283            self.run_with_vars(&[], closure)
284        }
285
286        fn run_with_vars<F, R>(&self, vars: &[(&str, Option<&OsStr>)], closure: F) -> R
287        where
288            F: FnOnce() -> R,
289        {
290            let path = self
291                .search_path
292                .as_ref()
293                .map(|paths| env::join_paths(paths).unwrap());
294
295            let mut run_vars: Vec<(&str, Option<&OsStr>)> = EnvVars::all_names()
296                .iter()
297                .copied()
298                .map(|name| (name, None))
299                .collect();
300            run_vars.extend([
301                // Keep discovery hermetic by disabling registry-based sources unless a test opts in.
302                (EnvVars::UV_PYTHON_NO_REGISTRY, Some(OsStr::new("1"))),
303                (EnvVars::PATH, path.as_deref()),
304                // Use the temporary python directory
305                (
306                    EnvVars::UV_PYTHON_INSTALL_DIR,
307                    Some(self.installations.root().as_os_str()),
308                ),
309                // Set a working directory
310                (EnvVars::PWD, Some(self.workdir.path().as_os_str())),
311            ]);
312            run_vars.extend(vars.iter().copied());
313            with_vars(&run_vars, closure)
314        }
315
316        fn run_with_vars_and_preview<F, R>(
317            &self,
318            vars: &[(&str, Option<&OsStr>)],
319            preview_features: &[PreviewFeature],
320            closure: F,
321        ) -> R
322        where
323            F: FnOnce() -> R,
324        {
325            let _preview = uv_preview::test::with_features(preview_features);
326            self.run_with_vars(vars, closure)
327        }
328
329        /// Create a fake Python interpreter executable which returns fixed metadata mocking our interpreter
330        /// query script output.
331        fn create_mock_interpreter(
332            path: &Path,
333            version: &PythonVersion,
334            implementation: ImplementationName,
335            system: bool,
336            free_threaded: bool,
337        ) -> Result<()> {
338            let json = indoc! {r##"
339                {
340                    "result": "success",
341                    "platform": {
342                        "os": {
343                            "name": "manylinux",
344                            "major": 2,
345                            "minor": 38
346                        },
347                        "arch": "x86_64"
348                    },
349                    "manylinux_compatible": true,
350                    "standalone": true,
351                    "markers": {
352                        "implementation_name": "{IMPLEMENTATION}",
353                        "implementation_version": "{FULL_VERSION}",
354                        "os_name": "posix",
355                        "platform_machine": "x86_64",
356                        "platform_python_implementation": "{IMPLEMENTATION}",
357                        "platform_release": "6.5.0-13-generic",
358                        "platform_system": "Linux",
359                        "platform_version": "#13-Ubuntu SMP PREEMPT_DYNAMIC Fri Nov  3 12:16:05 UTC 2023",
360                        "python_full_version": "{FULL_VERSION}",
361                        "python_version": "{VERSION}",
362                        "sys_platform": "linux"
363                    },
364                    "sys_base_exec_prefix": "/home/ferris/.pyenv/versions/{FULL_VERSION}",
365                    "sys_base_prefix": "/home/ferris/.pyenv/versions/{FULL_VERSION}",
366                    "sys_prefix": "{PREFIX}",
367                    "sys_executable": "{PATH}",
368                    "sys_path": [
369                        "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}/lib/python{VERSION}",
370                        "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}/site-packages"
371                    ],
372                    "site_packages": [
373                        "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}/site-packages"
374                    ],
375                    "stdlib": "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}",
376                    "extension_suffixes": [".cpython-{VERSION}-x86_64-linux-gnu.so", ".abi3.so", ".so"],
377                    "scheme": {
378                        "data": "/home/ferris/.pyenv/versions/{FULL_VERSION}",
379                        "include": "/home/ferris/.pyenv/versions/{FULL_VERSION}/include",
380                        "platlib": "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}/site-packages",
381                        "purelib": "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}/site-packages",
382                        "scripts": "/home/ferris/.pyenv/versions/{FULL_VERSION}/bin"
383                    },
384                    "virtualenv": {
385                        "data": "",
386                        "include": "include",
387                        "platlib": "lib/python{VERSION}/site-packages",
388                        "purelib": "lib/python{VERSION}/site-packages",
389                        "scripts": "bin"
390                    },
391                    "pointer_size": "64",
392                    "gil_disabled": {FREE_THREADED},
393                    "debug_enabled": false
394                }
395            "##};
396
397            let json = if system {
398                json.replace("{PREFIX}", "/home/ferris/.pyenv/versions/{FULL_VERSION}")
399            } else {
400                json.replace("{PREFIX}", "/home/ferris/projects/uv/.venv")
401            };
402
403            let json = json
404                .replace(
405                    "{PATH}",
406                    path.to_str().expect("Path can be represented as string"),
407                )
408                .replace("{FULL_VERSION}", &version.to_string())
409                .replace(
410                    "{VERSION}",
411                    &format!("{}.{}", version.major(), version.minor()),
412                )
413                .replace("{FREE_THREADED}", &free_threaded.to_string())
414                .replace("{IMPLEMENTATION}", (&implementation).into());
415
416            fs_err::create_dir_all(path.parent().unwrap())?;
417            fs_err::write(
418                path,
419                formatdoc! {r"
420                #!/bin/sh
421                echo '{json}'
422                "},
423            )?;
424
425            fs_err::set_permissions(path, std::os::unix::fs::PermissionsExt::from_mode(0o770))?;
426
427            Ok(())
428        }
429
430        fn create_mock_pyodide_interpreter(path: &Path, version: &PythonVersion) -> Result<()> {
431            let json = indoc! {r##"
432                {
433                    "result": "success",
434                    "platform": {
435                        "os": {
436                            "name": "pyodide",
437                            "major": 2025,
438                            "minor": 0
439                        },
440                        "arch": "wasm32"
441                    },
442                    "manylinux_compatible": false,
443                    "standalone": false,
444                    "markers": {
445                        "implementation_name": "cpython",
446                        "implementation_version": "{FULL_VERSION}",
447                        "os_name": "posix",
448                        "platform_machine": "wasm32",
449                        "platform_python_implementation": "CPython",
450                        "platform_release": "4.0.9",
451                        "platform_system": "Emscripten",
452                        "platform_version": "#1",
453                        "python_full_version": "{FULL_VERSION}",
454                        "python_version": "{VERSION}",
455                        "sys_platform": "emscripten"
456                    },
457                    "sys_base_exec_prefix": "/",
458                    "sys_base_prefix": "/",
459                    "sys_prefix": "/",
460                    "sys_executable": "{PATH}",
461                    "sys_path": [
462                        "",
463                        "/lib/python313.zip",
464                        "/lib/python{VERSION}",
465                        "/lib/python{VERSION}/lib-dynload",
466                        "/lib/python{VERSION}/site-packages"
467                    ],
468                    "site_packages": [
469                        "/lib/python{VERSION}/site-packages"
470                    ],
471                    "stdlib": "//lib/python{VERSION}",
472                    "extension_suffixes": [".cpython-{VERSION}-wasm32-emscripten.so", ".so"],
473                    "scheme": {
474                        "platlib": "//lib/python{VERSION}/site-packages",
475                        "purelib": "//lib/python{VERSION}/site-packages",
476                        "include": "//include/python{VERSION}",
477                        "scripts": "//bin",
478                        "data": "/"
479                    },
480                    "virtualenv": {
481                        "purelib": "lib/python{VERSION}/site-packages",
482                        "platlib": "lib/python{VERSION}/site-packages",
483                        "include": "include/site/python{VERSION}",
484                        "scripts": "bin",
485                        "data": ""
486                    },
487                    "pointer_size": "32",
488                    "gil_disabled": false,
489                    "debug_enabled": false
490                }
491            "##};
492
493            let json = json
494                .replace(
495                    "{PATH}",
496                    path.to_str().expect("Path can be represented as string"),
497                )
498                .replace("{FULL_VERSION}", &version.to_string())
499                .replace(
500                    "{VERSION}",
501                    &format!("{}.{}", version.major(), version.minor()),
502                );
503
504            fs_err::create_dir_all(path.parent().unwrap())?;
505            fs_err::write(
506                path,
507                formatdoc! {r"
508                #!/bin/sh
509                echo '{json}'
510                "},
511            )?;
512
513            fs_err::set_permissions(path, std::os::unix::fs::PermissionsExt::from_mode(0o770))?;
514
515            Ok(())
516        }
517
518        /// Create a mock Python 2 interpreter executable which returns a fixed error message mocking
519        /// invocation of Python 2 with the `-I` flag as done by our query script.
520        fn create_mock_python2_interpreter(path: &Path) -> Result<()> {
521            let output = indoc! { r"
522                Unknown option: -I
523                usage: /usr/bin/python [option] ... [-c cmd | -m mod | file | -] [arg] ...
524                Try `python -h` for more information.
525            "};
526
527            fs_err::write(
528                path,
529                formatdoc! {r"
530                #!/bin/sh
531                echo '{output}' 1>&2
532                "},
533            )?;
534
535            fs_err::set_permissions(path, std::os::unix::fs::PermissionsExt::from_mode(0o770))?;
536
537            Ok(())
538        }
539
540        /// Create child directories in a temporary directory.
541        fn new_search_path_directories(
542            &mut self,
543            names: &[impl AsRef<Path>],
544        ) -> Result<Vec<ChildPath>> {
545            let paths = names
546                .iter()
547                .map(|name| self.new_search_path_directory(name))
548                .collect::<Result<Vec<_>>>()?;
549            Ok(paths)
550        }
551
552        /// Create fake Python interpreters the given Python versions.
553        ///
554        /// Adds them to the test context search path.
555        fn add_python_to_workdir(&self, name: &str, version: &str) -> Result<()> {
556            Self::create_mock_interpreter(
557                self.workdir.child(name).as_ref(),
558                &PythonVersion::from_str(version).expect("Test uses valid version"),
559                ImplementationName::default(),
560                true,
561                false,
562            )
563        }
564
565        fn add_pyodide_version(&mut self, version: &'static str) -> Result<()> {
566            let path = self.new_search_path_directory(format!("pyodide-{version}"))?;
567            let python = format!("pyodide{}", env::consts::EXE_SUFFIX);
568            Self::create_mock_pyodide_interpreter(
569                &path.join(python),
570                &PythonVersion::from_str(version).unwrap(),
571            )?;
572            Ok(())
573        }
574
575        /// Create fake Python interpreters the given Python versions.
576        ///
577        /// Adds them to the test context search path.
578        fn add_python_versions(&mut self, versions: &[&'static str]) -> Result<()> {
579            let interpreters: Vec<_> = versions
580                .iter()
581                .map(|version| (true, ImplementationName::default(), "python", *version))
582                .collect();
583            self.add_python_interpreters(interpreters.as_slice())
584        }
585
586        /// Create fake Python interpreters the given Python implementations and versions.
587        ///
588        /// Adds them to the test context search path.
589        fn add_python_interpreters(
590            &mut self,
591            kinds: &[(bool, ImplementationName, &'static str, &'static str)],
592        ) -> Result<()> {
593            // Generate a "unique" folder name for each interpreter
594            let names: Vec<OsString> = kinds
595                .iter()
596                .map(|(system, implementation, name, version)| {
597                    OsString::from_str(&format!("{system}-{implementation}-{name}-{version}"))
598                        .unwrap()
599                })
600                .collect();
601            let paths = self.new_search_path_directories(names.as_slice())?;
602            for (path, (system, implementation, executable, version)) in
603                itertools::zip_eq(&paths, kinds)
604            {
605                let python = format!("{executable}{}", env::consts::EXE_SUFFIX);
606                Self::create_mock_interpreter(
607                    &path.join(python),
608                    &PythonVersion::from_str(version).unwrap(),
609                    *implementation,
610                    *system,
611                    false,
612                )?;
613            }
614            Ok(())
615        }
616
617        /// Create a mock virtual environment at the given directory
618        fn mock_venv(path: impl AsRef<Path>, version: &'static str) -> Result<()> {
619            let executable = virtualenv_python_executable(path.as_ref());
620            fs_err::create_dir_all(
621                executable
622                    .parent()
623                    .expect("A Python executable path should always have a parent"),
624            )?;
625            Self::create_mock_interpreter(
626                &executable,
627                &PythonVersion::from_str(version)
628                    .expect("A valid Python version is used for tests"),
629                ImplementationName::default(),
630                false,
631                false,
632            )?;
633            ChildPath::new(path.as_ref().join("pyvenv.cfg")).touch()?;
634            Ok(())
635        }
636
637        /// Create a mock conda prefix at the given directory.
638        ///
639        /// These are like virtual environments but they look like system interpreters because `prefix` and `base_prefix` are equal.
640        fn mock_conda_prefix(path: impl AsRef<Path>, version: &'static str) -> Result<()> {
641            let executable = virtualenv_python_executable(&path);
642            fs_err::create_dir_all(
643                executable
644                    .parent()
645                    .expect("A Python executable path should always have a parent"),
646            )?;
647            Self::create_mock_interpreter(
648                &executable,
649                &PythonVersion::from_str(version)
650                    .expect("A valid Python version is used for tests"),
651                ImplementationName::default(),
652                true,
653                false,
654            )?;
655            ChildPath::new(path.as_ref().join("pyvenv.cfg")).touch()?;
656            Ok(())
657        }
658    }
659
660    #[test]
661    fn find_python_empty_path() -> Result<()> {
662        let mut context = TestContext::new()?;
663
664        context.search_path = Some(vec![]);
665        let result = context.run(|| {
666            find_python_installation(
667                &PythonRequest::Default,
668                EnvironmentPreference::OnlySystem,
669                PythonPreference::default(),
670                &context.cache,
671            )
672        });
673        assert!(
674            matches!(result, Ok(Err(PythonNotFound { .. }))),
675            "With an empty path, no Python installation should be detected got {result:?}"
676        );
677
678        context.search_path = None;
679        let result = context.run(|| {
680            find_python_installation(
681                &PythonRequest::Default,
682                EnvironmentPreference::OnlySystem,
683                PythonPreference::default(),
684                &context.cache,
685            )
686        });
687        assert!(
688            matches!(result, Ok(Err(PythonNotFound { .. }))),
689            "With an unset path, no Python installation should be detected got {result:?}"
690        );
691
692        Ok(())
693    }
694
695    #[test]
696    fn find_python_unexecutable_file() -> Result<()> {
697        let mut context = TestContext::new()?;
698        context
699            .new_search_path_directory("path")?
700            .child(format!("python{}", env::consts::EXE_SUFFIX))
701            .touch()?;
702
703        let result = context.run(|| {
704            find_python_installation(
705                &PythonRequest::Default,
706                EnvironmentPreference::OnlySystem,
707                PythonPreference::default(),
708                &context.cache,
709            )
710        });
711        assert!(
712            matches!(result, Ok(Err(PythonNotFound { .. }))),
713            "With a non-executable Python, no Python installation should be detected; got {result:?}"
714        );
715
716        Ok(())
717    }
718
719    #[test]
720    fn find_python_valid_executable() -> Result<()> {
721        let mut context = TestContext::new()?;
722        context.add_python_versions(&["3.12.1"])?;
723
724        let interpreter = context.run(|| {
725            find_python_installation(
726                &PythonRequest::Default,
727                EnvironmentPreference::OnlySystem,
728                PythonPreference::default(),
729                &context.cache,
730            )
731        })??;
732        assert!(
733            matches!(
734                interpreter,
735                PythonInstallation {
736                    source: PythonSource::SearchPathFirst,
737                    interpreter: _
738                }
739            ),
740            "We should find the valid executable; got {interpreter:?}"
741        );
742
743        Ok(())
744    }
745
746    #[test]
747    fn find_or_download_skips_download_metadata_when_python_is_found() -> Result<()> {
748        let mut context = TestContext::new()?;
749        context.add_python_versions(&["3.12.1"])?;
750        // Pass a missing metadata file to assert that an already-installed Python can
751        // be returned without reading the download list.
752        let missing_downloads = context.tempdir.child("missing-downloads.json");
753
754        let interpreter = context.run(|| {
755            let client_builder = BaseClientBuilder::default();
756            tokio::runtime::Builder::new_current_thread()
757                .enable_all()
758                .build()
759                .expect("Failed to build runtime")
760                .block_on(PythonInstallation::find_or_download(
761                    None,
762                    EnvironmentPreference::OnlySystem,
763                    PythonPreference::OnlySystem,
764                    PythonDownloads::Never,
765                    &client_builder,
766                    &context.cache,
767                    None,
768                    None,
769                    None,
770                    missing_downloads.path().to_str(),
771                ))
772        })?;
773
774        assert!(
775            matches!(
776                interpreter,
777                PythonInstallation {
778                    source: PythonSource::SearchPathFirst,
779                    interpreter: _
780                }
781            ),
782            "We should find the local Python without reading download metadata; got {interpreter:?}"
783        );
784        assert_eq!(
785            &interpreter.interpreter().python_full_version().to_string(),
786            "3.12.1",
787            "We should find the local interpreter"
788        );
789
790        Ok(())
791    }
792
793    #[test]
794    fn find_python_valid_executable_after_invalid() -> Result<()> {
795        let mut context = TestContext::new()?;
796        let children = context.new_search_path_directories(&[
797            "query-parse-error",
798            "not-executable",
799            "empty",
800            "good",
801        ])?;
802
803        // An executable file with a bad response
804        #[cfg(unix)]
805        fs_err::write(
806            children[0].join(format!("python{}", env::consts::EXE_SUFFIX)),
807            formatdoc! {r"
808        #!/bin/sh
809        echo 'foo'
810        "},
811        )?;
812        fs_err::set_permissions(
813            children[0].join(format!("python{}", env::consts::EXE_SUFFIX)),
814            std::os::unix::fs::PermissionsExt::from_mode(0o770),
815        )?;
816
817        // A non-executable file
818        ChildPath::new(children[1].join(format!("python{}", env::consts::EXE_SUFFIX))).touch()?;
819
820        // An empty directory at `children[2]`
821
822        // An good interpreter!
823        let python_path = children[3].join(format!("python{}", env::consts::EXE_SUFFIX));
824        TestContext::create_mock_interpreter(
825            &python_path,
826            &PythonVersion::from_str("3.12.1").unwrap(),
827            ImplementationName::default(),
828            true,
829            false,
830        )?;
831
832        let python = context.run(|| {
833            find_python_installation(
834                &PythonRequest::Default,
835                EnvironmentPreference::OnlySystem,
836                PythonPreference::default(),
837                &context.cache,
838            )
839        })??;
840        assert!(
841            matches!(
842                python,
843                PythonInstallation {
844                    source: PythonSource::SearchPath,
845                    interpreter: _
846                }
847            ),
848            "We should skip the bad executables in favor of the good one; got {python:?}"
849        );
850        assert_eq!(python.interpreter().sys_executable(), python_path);
851
852        Ok(())
853    }
854
855    #[test]
856    fn find_python_installations_discovers_search_path_lazily() -> Result<()> {
857        let context = TestContext::new()?;
858        let first_directory = context.tempdir.child("first");
859        let second_directory = context.tempdir.child("second");
860
861        let python = first_directory.join(format!("python{}", env::consts::EXE_SUFFIX));
862        let second = second_directory.join(format!("python{}", env::consts::EXE_SUFFIX));
863
864        let installation = context.run(|| -> Result<_> {
865            let mut installations = find_python_installations(
866                &PythonRequest::Default,
867                EnvironmentPreference::OnlySystem,
868                PythonPreference::OnlySystem,
869                &context.cache,
870            );
871
872            TestContext::create_mock_interpreter(
873                &python,
874                &PythonVersion::from_str("3.12.1").expect("Test uses a valid Python version"),
875                ImplementationName::CPython,
876                true,
877                false,
878            )?;
879
880            let search_path = env::join_paths([first_directory.path(), second_directory.path()])?;
881            with_vars(
882                [(EnvVars::PATH, Some(search_path.as_os_str()))],
883                || -> Result<_> {
884                    let installation = installations
885                        .next()
886                        .expect("Deferred search path should contain an interpreter")??;
887
888                    TestContext::create_mock_interpreter(
889                        &second,
890                        &PythonVersion::from_str("3.11.9")
891                            .expect("Test uses a valid Python version"),
892                        ImplementationName::CPython,
893                        true,
894                        false,
895                    )?;
896                    let second_installation = installations
897                        .next()
898                        .expect("Later search path directory should be discovered")??;
899                    assert_eq!(second_installation.interpreter().sys_executable(), second);
900
901                    Ok(installation)
902                },
903            )
904        })?;
905
906        assert_eq!(installation.interpreter().sys_executable(), python);
907
908        Ok(())
909    }
910
911    #[test]
912    fn find_python_installation_queries_lazily() -> Result<()> {
913        let mut context = TestContext::new()?;
914        let first_directory = context.new_search_path_directory("first")?;
915        let second_directory = context.new_search_path_directory("second")?;
916
917        let first = first_directory.join(format!("python{}", env::consts::EXE_SUFFIX));
918        TestContext::create_mock_interpreter(
919            &first,
920            &PythonVersion::from_str("3.12.1").expect("Test uses a valid Python version"),
921            ImplementationName::CPython,
922            true,
923            false,
924        )?;
925
926        let second = second_directory.join(format!("python{}", env::consts::EXE_SUFFIX));
927        TestContext::create_mock_interpreter(
928            &second,
929            &PythonVersion::from_str("3.11.9").expect("Test uses a valid Python version"),
930            ImplementationName::CPython,
931            true,
932            false,
933        )?;
934        let second_target =
935            second_directory.join(format!("python-real{}", env::consts::EXE_SUFFIX));
936        fs_err::rename(&second, &second_target)?;
937
938        let marker = context.tempdir.child("second-was-queried");
939        fs_err::write(
940            &second,
941            formatdoc! {r#"
942                #!/bin/sh
943                : > "{marker}"
944                exec "{target}" "$@"
945            "#,
946            marker = marker.path().display(),
947            target = second_target.display()},
948        )?;
949        fs_err::set_permissions(&second, std::os::unix::fs::PermissionsExt::from_mode(0o770))?;
950
951        let installation = context.run(|| {
952            find_python_installation(
953                &PythonRequest::Default,
954                EnvironmentPreference::OnlySystem,
955                PythonPreference::OnlySystem,
956                &context.cache,
957            )
958        })??;
959
960        assert_eq!(installation.interpreter().sys_executable(), first);
961        assert!(
962            !marker.path().exists(),
963            "Sequential discovery should not query candidates after finding a match"
964        );
965
966        Ok(())
967    }
968
969    #[test]
970    fn find_all_python_installations_matches_sequential_discovery() -> Result<()> {
971        let mut context = TestContext::new()?;
972        let sequential_cache = Cache::temp()?;
973        let parallel_cache = Cache::temp()?;
974
975        let broken_directory = context.new_search_path_directory("broken")?;
976        let broken = broken_directory.join(format!("python{}", env::consts::EXE_SUFFIX));
977        fs_err::write(
978            &broken,
979            formatdoc! {r"
980                #!/bin/sh
981                echo 'not interpreter metadata'
982            "},
983        )?;
984        fs_err::set_permissions(&broken, std::os::unix::fs::PermissionsExt::from_mode(0o770))?;
985
986        let cpython_311_directory = context.new_search_path_directory("cpython-3.11")?;
987        let cpython_311 = cpython_311_directory.join(format!("python{}", env::consts::EXE_SUFFIX));
988        TestContext::create_mock_interpreter(
989            &cpython_311,
990            &PythonVersion::from_str("3.11.9").expect("Test uses a valid Python version"),
991            ImplementationName::CPython,
992            true,
993            false,
994        )?;
995        let cpython_311_target =
996            cpython_311_directory.join(format!("python-real{}", env::consts::EXE_SUFFIX));
997        fs_err::rename(&cpython_311, &cpython_311_target)?;
998        fs_err::write(
999            &cpython_311,
1000            formatdoc! {r#"
1001                #!/bin/sh
1002                sleep 1
1003                exec "{target}" "$@"
1004            "#,
1005            target = cpython_311_target.display()},
1006        )?;
1007        fs_err::set_permissions(
1008            &cpython_311,
1009            std::os::unix::fs::PermissionsExt::from_mode(0o770),
1010        )?;
1011
1012        let cpython_312_directory = context.new_search_path_directory("cpython-3.12")?;
1013        let cpython_312 = cpython_312_directory.join(format!("python{}", env::consts::EXE_SUFFIX));
1014        TestContext::create_mock_interpreter(
1015            &cpython_312,
1016            &PythonVersion::from_str("3.12.1").expect("Test uses a valid Python version"),
1017            ImplementationName::CPython,
1018            true,
1019            false,
1020        )?;
1021
1022        let pypy_directory = context.new_search_path_directory("pypy-3.10")?;
1023        let pypy = pypy_directory.join(format!("pypy{}", env::consts::EXE_SUFFIX));
1024        TestContext::create_mock_interpreter(
1025            &pypy,
1026            &PythonVersion::from_str("3.10.14").expect("Test uses a valid Python version"),
1027            ImplementationName::PyPy,
1028            true,
1029            false,
1030        )?;
1031
1032        let virtual_environment = context.tempdir.child("virtual-environment");
1033        TestContext::mock_venv(&virtual_environment, "3.12.1")?;
1034
1035        let key = context
1036            .run(|| {
1037                find_python_installation(
1038                    &PythonRequest::File(cpython_312.clone()),
1039                    EnvironmentPreference::OnlySystem,
1040                    PythonPreference::OnlySystem,
1041                    &context.cache,
1042                )
1043            })??
1044            .key()
1045            .to_string();
1046        let key_request = PythonRequest::parse(&key);
1047        assert!(
1048            matches!(key_request, PythonRequest::Key(_)),
1049            "Expected an installation key request, got {key_request:?}"
1050        );
1051
1052        let requests = [
1053            PythonRequest::Any,
1054            PythonRequest::Default,
1055            PythonRequest::parse("3.12"),
1056            PythonRequest::parse("cpython"),
1057            PythonRequest::parse("pypy@3.10"),
1058            PythonRequest::ExecutableName(format!("pypy{}", env::consts::EXE_SUFFIX)),
1059            PythonRequest::File(cpython_312),
1060            PythonRequest::Directory(virtual_environment.to_path_buf()),
1061            key_request,
1062        ];
1063
1064        for request in requests {
1065            let (sequential, parallel) = context.run(|| {
1066                let mut sequential = Vec::new();
1067                for result in find_python_installations(
1068                    &request,
1069                    EnvironmentPreference::OnlySystem,
1070                    PythonPreference::OnlySystem,
1071                    &sequential_cache,
1072                ) {
1073                    match result {
1074                        Ok(Ok(installation)) => sequential.push(installation),
1075                        Ok(Err(_)) => {}
1076                        Err(err) if err.is_critical() => return Err(err),
1077                        Err(_) => {}
1078                    }
1079                }
1080
1081                let parallel = find_all_python_installations(
1082                    &request,
1083                    EnvironmentPreference::OnlySystem,
1084                    PythonPreference::OnlySystem,
1085                    &parallel_cache,
1086                )?;
1087                Ok::<_, discovery::Error>((sequential, parallel))
1088            })?;
1089
1090            let identifiers = |installations: Vec<PythonInstallation>| {
1091                installations
1092                    .into_iter()
1093                    .map(|installation| {
1094                        (
1095                            *installation.source(),
1096                            installation.interpreter().sys_executable().to_path_buf(),
1097                            installation.key().to_string(),
1098                        )
1099                    })
1100                    .collect::<Vec<_>>()
1101            };
1102            assert_eq!(
1103                identifiers(sequential),
1104                identifiers(parallel),
1105                "Sequential and parallel discovery differ for {request}"
1106            );
1107        }
1108
1109        Ok(())
1110    }
1111
1112    #[test]
1113    fn find_python_only_python2_executable() -> Result<()> {
1114        let mut context = TestContext::new()?;
1115        let python = context
1116            .new_search_path_directory("python2")?
1117            .child(format!("python{}", env::consts::EXE_SUFFIX));
1118        TestContext::create_mock_python2_interpreter(&python)?;
1119
1120        let result = context.run(|| {
1121            find_python_installation(
1122                &PythonRequest::Default,
1123                EnvironmentPreference::OnlySystem,
1124                PythonPreference::default(),
1125                &context.cache,
1126            )
1127        });
1128        assert!(
1129            matches!(result, Err(discovery::Error::Query(..))),
1130            "If only Python 2 is available, we should report the interpreter query error; got {result:?}"
1131        );
1132
1133        Ok(())
1134    }
1135
1136    #[test]
1137    fn find_python_skip_python2_executable() -> Result<()> {
1138        let mut context = TestContext::new()?;
1139
1140        let python2 = context
1141            .new_search_path_directory("python2")?
1142            .child(format!("python{}", env::consts::EXE_SUFFIX));
1143        TestContext::create_mock_python2_interpreter(&python2)?;
1144
1145        let python3 = context
1146            .new_search_path_directory("python3")?
1147            .child(format!("python{}", env::consts::EXE_SUFFIX));
1148        TestContext::create_mock_interpreter(
1149            &python3,
1150            &PythonVersion::from_str("3.12.1").unwrap(),
1151            ImplementationName::default(),
1152            true,
1153            false,
1154        )?;
1155
1156        let python = context.run(|| {
1157            find_python_installation(
1158                &PythonRequest::Default,
1159                EnvironmentPreference::OnlySystem,
1160                PythonPreference::default(),
1161                &context.cache,
1162            )
1163        })??;
1164        assert!(
1165            matches!(
1166                python,
1167                PythonInstallation {
1168                    source: PythonSource::SearchPath,
1169                    interpreter: _
1170                }
1171            ),
1172            "We should skip the Python 2 installation and find the Python 3 interpreter; got {python:?}"
1173        );
1174        assert_eq!(python.interpreter().sys_executable(), python3.path());
1175
1176        Ok(())
1177    }
1178
1179    #[test]
1180    fn find_python_system_python_allowed() -> Result<()> {
1181        let mut context = TestContext::new()?;
1182        context.add_python_interpreters(&[
1183            (false, ImplementationName::CPython, "python", "3.10.0"),
1184            (true, ImplementationName::CPython, "python", "3.10.1"),
1185        ])?;
1186
1187        let python = context.run(|| {
1188            find_python_installation(
1189                &PythonRequest::Default,
1190                EnvironmentPreference::Any,
1191                PythonPreference::OnlySystem,
1192                &context.cache,
1193            )
1194        })??;
1195        assert_eq!(
1196            python.interpreter().python_full_version().to_string(),
1197            "3.10.0",
1198            "Should find the first interpreter regardless of system"
1199        );
1200
1201        // Reverse the order of the virtual environment and system
1202        context.reset_search_path();
1203        context.add_python_interpreters(&[
1204            (true, ImplementationName::CPython, "python", "3.10.1"),
1205            (false, ImplementationName::CPython, "python", "3.10.0"),
1206        ])?;
1207
1208        let python = context.run(|| {
1209            find_python_installation(
1210                &PythonRequest::Default,
1211                EnvironmentPreference::Any,
1212                PythonPreference::OnlySystem,
1213                &context.cache,
1214            )
1215        })??;
1216        assert_eq!(
1217            python.interpreter().python_full_version().to_string(),
1218            "3.10.1",
1219            "Should find the first interpreter regardless of system"
1220        );
1221
1222        Ok(())
1223    }
1224
1225    #[test]
1226    fn find_python_system_python_required() -> Result<()> {
1227        let mut context = TestContext::new()?;
1228        context.add_python_interpreters(&[
1229            (false, ImplementationName::CPython, "python", "3.10.0"),
1230            (true, ImplementationName::CPython, "python", "3.10.1"),
1231        ])?;
1232
1233        let python = context.run(|| {
1234            find_python_installation(
1235                &PythonRequest::Default,
1236                EnvironmentPreference::OnlySystem,
1237                PythonPreference::OnlySystem,
1238                &context.cache,
1239            )
1240        })??;
1241        assert_eq!(
1242            python.interpreter().python_full_version().to_string(),
1243            "3.10.1",
1244            "Should skip the virtual environment"
1245        );
1246
1247        Ok(())
1248    }
1249
1250    #[test]
1251    fn find_python_system_python_disallowed() -> Result<()> {
1252        let mut context = TestContext::new()?;
1253        context.add_python_interpreters(&[
1254            (true, ImplementationName::CPython, "python", "3.10.0"),
1255            (false, ImplementationName::CPython, "python", "3.10.1"),
1256        ])?;
1257
1258        let python = context.run(|| {
1259            find_python_installation(
1260                &PythonRequest::Default,
1261                EnvironmentPreference::Any,
1262                PythonPreference::OnlySystem,
1263                &context.cache,
1264            )
1265        })??;
1266        assert_eq!(
1267            python.interpreter().python_full_version().to_string(),
1268            "3.10.0",
1269            "Should skip the system Python"
1270        );
1271
1272        Ok(())
1273    }
1274
1275    #[test]
1276    fn find_python_version_minor() -> Result<()> {
1277        let mut context = TestContext::new()?;
1278        context.add_python_versions(&["3.10.1", "3.11.2", "3.12.3"])?;
1279
1280        let python = context.run(|| {
1281            find_python_installation(
1282                &PythonRequest::parse("3.11"),
1283                EnvironmentPreference::Any,
1284                PythonPreference::OnlySystem,
1285                &context.cache,
1286            )
1287        })??;
1288
1289        assert!(
1290            matches!(
1291                python,
1292                PythonInstallation {
1293                    source: PythonSource::SearchPath,
1294                    interpreter: _
1295                }
1296            ),
1297            "We should find a python; got {python:?}"
1298        );
1299        assert_eq!(
1300            &python.interpreter().python_full_version().to_string(),
1301            "3.11.2",
1302            "We should find the correct interpreter for the request"
1303        );
1304
1305        Ok(())
1306    }
1307
1308    #[test]
1309    fn find_python_version_patch() -> Result<()> {
1310        let mut context = TestContext::new()?;
1311        context.add_python_versions(&["3.10.1", "3.11.3", "3.11.2", "3.12.3"])?;
1312
1313        let python = context.run(|| {
1314            find_python_installation(
1315                &PythonRequest::parse("3.11.2"),
1316                EnvironmentPreference::Any,
1317                PythonPreference::OnlySystem,
1318                &context.cache,
1319            )
1320        })??;
1321
1322        assert!(
1323            matches!(
1324                python,
1325                PythonInstallation {
1326                    source: PythonSource::SearchPath,
1327                    interpreter: _
1328                }
1329            ),
1330            "We should find a python; got {python:?}"
1331        );
1332        assert_eq!(
1333            &python.interpreter().python_full_version().to_string(),
1334            "3.11.2",
1335            "We should find the correct interpreter for the request"
1336        );
1337
1338        Ok(())
1339    }
1340
1341    #[test]
1342    fn find_python_version_minor_no_match() -> Result<()> {
1343        let mut context = TestContext::new()?;
1344        context.add_python_versions(&["3.10.1", "3.11.2", "3.12.3"])?;
1345
1346        let result = context.run(|| {
1347            find_python_installation(
1348                &PythonRequest::parse("3.9"),
1349                EnvironmentPreference::Any,
1350                PythonPreference::OnlySystem,
1351                &context.cache,
1352            )
1353        })?;
1354        assert!(
1355            matches!(result, Err(PythonNotFound { .. })),
1356            "We should not find a python; got {result:?}"
1357        );
1358
1359        Ok(())
1360    }
1361
1362    #[test]
1363    fn find_python_version_patch_no_match() -> Result<()> {
1364        let mut context = TestContext::new()?;
1365        context.add_python_versions(&["3.10.1", "3.11.2", "3.12.3"])?;
1366
1367        let result = context.run(|| {
1368            find_python_installation(
1369                &PythonRequest::parse("3.11.9"),
1370                EnvironmentPreference::Any,
1371                PythonPreference::OnlySystem,
1372                &context.cache,
1373            )
1374        })?;
1375        assert!(
1376            matches!(result, Err(PythonNotFound { .. })),
1377            "We should not find a python; got {result:?}"
1378        );
1379
1380        Ok(())
1381    }
1382
1383    fn find_best_python_installation_no_download(
1384        request: &PythonRequest,
1385        environments: EnvironmentPreference,
1386        preference: PythonPreference,
1387        cache: &Cache,
1388    ) -> Result<PythonInstallation, crate::Error> {
1389        let client_builder = BaseClientBuilder::default();
1390        tokio::runtime::Builder::new_current_thread()
1391            .enable_all()
1392            .build()
1393            .expect("Failed to build runtime")
1394            .block_on(find_best_python_installation(
1395                request,
1396                environments,
1397                preference,
1398                false,
1399                &client_builder,
1400                cache,
1401                None,
1402                None,
1403                None,
1404                None,
1405            ))
1406    }
1407
1408    #[test]
1409    fn find_best_python_version_patch_exact() -> Result<()> {
1410        let mut context = TestContext::new()?;
1411        context.add_python_versions(&["3.10.1", "3.11.2", "3.11.4", "3.11.3", "3.12.5"])?;
1412
1413        let python = context.run(|| {
1414            find_best_python_installation_no_download(
1415                &PythonRequest::parse("3.11.3"),
1416                EnvironmentPreference::Any,
1417                PythonPreference::OnlySystem,
1418                &context.cache,
1419            )
1420        })?;
1421
1422        assert!(
1423            matches!(
1424                python,
1425                PythonInstallation {
1426                    source: PythonSource::SearchPath,
1427                    interpreter: _
1428                }
1429            ),
1430            "We should find a python; got {python:?}"
1431        );
1432        assert_eq!(
1433            &python.interpreter().python_full_version().to_string(),
1434            "3.11.3",
1435            "We should prefer the exact request"
1436        );
1437
1438        Ok(())
1439    }
1440
1441    #[test]
1442    fn find_best_python_version_patch_fallback() -> Result<()> {
1443        let mut context = TestContext::new()?;
1444        context.add_python_versions(&["3.10.1", "3.11.2", "3.11.4", "3.11.3", "3.12.5"])?;
1445
1446        let python = context.run(|| {
1447            find_best_python_installation_no_download(
1448                &PythonRequest::parse("3.11.11"),
1449                EnvironmentPreference::Any,
1450                PythonPreference::OnlySystem,
1451                &context.cache,
1452            )
1453        })?;
1454
1455        assert!(
1456            matches!(
1457                python,
1458                PythonInstallation {
1459                    source: PythonSource::SearchPath,
1460                    interpreter: _
1461                }
1462            ),
1463            "We should find a python; got {python:?}"
1464        );
1465        assert_eq!(
1466            &python.interpreter().python_full_version().to_string(),
1467            "3.11.2",
1468            "We should fallback to the first matching minor"
1469        );
1470
1471        Ok(())
1472    }
1473
1474    #[test]
1475    fn find_best_python_skips_source_without_match() -> Result<()> {
1476        let mut context = TestContext::new()?;
1477        let venv = context.tempdir.child(".venv");
1478        TestContext::mock_venv(&venv, "3.12.0")?;
1479        context.add_python_versions(&["3.10.1"])?;
1480
1481        let python =
1482            context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
1483                find_best_python_installation_no_download(
1484                    &PythonRequest::parse("3.10"),
1485                    EnvironmentPreference::Any,
1486                    PythonPreference::OnlySystem,
1487                    &context.cache,
1488                )
1489            })?;
1490        assert!(
1491            matches!(
1492                python,
1493                PythonInstallation {
1494                    source: PythonSource::SearchPathFirst,
1495                    interpreter: _
1496                }
1497            ),
1498            "We should skip the active environment in favor of the requested version; got {python:?}"
1499        );
1500
1501        Ok(())
1502    }
1503
1504    #[test]
1505    fn find_best_python_returns_to_earlier_source_on_fallback() -> Result<()> {
1506        let mut context = TestContext::new()?;
1507        let venv = context.tempdir.child(".venv");
1508        TestContext::mock_venv(&venv, "3.10.1")?;
1509        context.add_python_versions(&["3.10.3"])?;
1510
1511        let python =
1512            context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
1513                find_best_python_installation_no_download(
1514                    &PythonRequest::parse("3.10.2"),
1515                    EnvironmentPreference::Any,
1516                    PythonPreference::OnlySystem,
1517                    &context.cache,
1518                )
1519            })?;
1520        assert!(
1521            matches!(
1522                python,
1523                PythonInstallation {
1524                    source: PythonSource::ActiveEnvironment,
1525                    interpreter: _
1526                }
1527            ),
1528            "We should prefer the active environment after relaxing; got {python:?}"
1529        );
1530        assert_eq!(
1531            python.interpreter().python_full_version().to_string(),
1532            "3.10.1",
1533            "We should prefer the active environment"
1534        );
1535
1536        Ok(())
1537    }
1538
1539    #[test]
1540    fn find_python_from_active_python() -> Result<()> {
1541        let context = TestContext::new()?;
1542        let venv = context.tempdir.child("some-venv");
1543        TestContext::mock_venv(&venv, "3.12.0")?;
1544
1545        let python =
1546            context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
1547                find_python_installation(
1548                    &PythonRequest::Default,
1549                    EnvironmentPreference::Any,
1550                    PythonPreference::OnlySystem,
1551                    &context.cache,
1552                )
1553            })??;
1554        assert_eq!(
1555            python.interpreter().python_full_version().to_string(),
1556            "3.12.0",
1557            "We should prefer the active environment"
1558        );
1559
1560        Ok(())
1561    }
1562
1563    #[test]
1564    fn find_python_from_active_python_prerelease() -> Result<()> {
1565        let mut context = TestContext::new()?;
1566        context.add_python_versions(&["3.12.0"])?;
1567        let venv = context.tempdir.child("some-venv");
1568        TestContext::mock_venv(&venv, "3.13.0rc1")?;
1569
1570        let python =
1571            context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
1572                find_python_installation(
1573                    &PythonRequest::Default,
1574                    EnvironmentPreference::Any,
1575                    PythonPreference::OnlySystem,
1576                    &context.cache,
1577                )
1578            })??;
1579        assert_eq!(
1580            python.interpreter().python_full_version().to_string(),
1581            "3.13.0rc1",
1582            "We should prefer the active environment"
1583        );
1584
1585        Ok(())
1586    }
1587
1588    #[test]
1589    fn find_python_from_conda_prefix() -> Result<()> {
1590        let context = TestContext::new()?;
1591        let condaenv = context.tempdir.child("condaenv");
1592        TestContext::mock_conda_prefix(&condaenv, "3.12.0")?;
1593
1594        let python = context
1595            .run_with_vars(
1596                &[(EnvVars::CONDA_PREFIX, Some(condaenv.as_os_str()))],
1597                || {
1598                    // Note this python is not treated as a system interpreter
1599                    find_python_installation(
1600                        &PythonRequest::Default,
1601                        EnvironmentPreference::OnlyVirtual,
1602                        PythonPreference::OnlySystem,
1603                        &context.cache,
1604                    )
1605                },
1606            )?
1607            .unwrap();
1608        assert_eq!(
1609            python.interpreter().python_full_version().to_string(),
1610            "3.12.0",
1611            "We should allow the active conda python"
1612        );
1613
1614        let baseenv = context.tempdir.child("conda");
1615        TestContext::mock_conda_prefix(&baseenv, "3.12.1")?;
1616
1617        // But not if it's a base environment
1618        let result = context.run_with_vars_and_preview(
1619            &[
1620                (EnvVars::CONDA_PREFIX, Some(baseenv.as_os_str())),
1621                (EnvVars::CONDA_DEFAULT_ENV, Some(&OsString::from("base"))),
1622                (EnvVars::CONDA_ROOT, None),
1623            ],
1624            &[],
1625            || {
1626                find_python_installation(
1627                    &PythonRequest::Default,
1628                    EnvironmentPreference::OnlyVirtual,
1629                    PythonPreference::OnlySystem,
1630                    &context.cache,
1631                )
1632            },
1633        )?;
1634
1635        assert!(
1636            matches!(result, Err(PythonNotFound { .. })),
1637            "We should not allow the non-virtual environment; got {result:?}"
1638        );
1639
1640        // Unless, system interpreters are included...
1641        let python = context
1642            .run_with_vars_and_preview(
1643                &[
1644                    (EnvVars::CONDA_PREFIX, Some(baseenv.as_os_str())),
1645                    (EnvVars::CONDA_DEFAULT_ENV, Some(&OsString::from("base"))),
1646                    (EnvVars::CONDA_ROOT, None),
1647                ],
1648                &[],
1649                || {
1650                    find_python_installation(
1651                        &PythonRequest::Default,
1652                        EnvironmentPreference::OnlySystem,
1653                        PythonPreference::OnlySystem,
1654                        &context.cache,
1655                    )
1656                },
1657            )?
1658            .unwrap();
1659
1660        assert_eq!(
1661            python.interpreter().python_full_version().to_string(),
1662            "3.12.1",
1663            "We should find the base conda environment"
1664        );
1665
1666        // If the environment name doesn't match the default, we should not treat it as system
1667        let python = context
1668            .run_with_vars_and_preview(
1669                &[
1670                    (EnvVars::CONDA_PREFIX, Some(condaenv.as_os_str())),
1671                    (
1672                        EnvVars::CONDA_DEFAULT_ENV,
1673                        Some(&OsString::from("condaenv")),
1674                    ),
1675                ],
1676                &[],
1677                || {
1678                    find_python_installation(
1679                        &PythonRequest::Default,
1680                        EnvironmentPreference::OnlyVirtual,
1681                        PythonPreference::OnlySystem,
1682                        &context.cache,
1683                    )
1684                },
1685            )?
1686            .unwrap();
1687
1688        assert_eq!(
1689            python.interpreter().python_full_version().to_string(),
1690            "3.12.0",
1691            "We should find the conda environment when name matches"
1692        );
1693
1694        // When CONDA_DEFAULT_ENV is "base", it should always be treated as base environment
1695        let result = context.run_with_vars_and_preview(
1696            &[
1697                (EnvVars::CONDA_PREFIX, Some(condaenv.as_os_str())),
1698                (EnvVars::CONDA_DEFAULT_ENV, Some(&OsString::from("base"))),
1699            ],
1700            &[],
1701            || {
1702                find_python_installation(
1703                    &PythonRequest::Default,
1704                    EnvironmentPreference::OnlyVirtual,
1705                    PythonPreference::OnlySystem,
1706                    &context.cache,
1707                )
1708            },
1709        )?;
1710
1711        assert!(
1712            matches!(result, Err(PythonNotFound { .. })),
1713            "We should not allow the base environment when looking for virtual environments"
1714        );
1715
1716        // With the `special-conda-env-names` preview feature, "base" is not special-cased
1717        // and uses path-based heuristics instead. When the directory name matches the env name,
1718        // it should be treated as a child environment.
1719        let base_dir = context.tempdir.child("base");
1720        TestContext::mock_conda_prefix(&base_dir, "3.12.6")?;
1721        let python = context
1722            .run_with_vars_and_preview(
1723                &[
1724                    (EnvVars::CONDA_PREFIX, Some(base_dir.as_os_str())),
1725                    (EnvVars::CONDA_DEFAULT_ENV, Some(&OsString::from("base"))),
1726                    (EnvVars::CONDA_ROOT, None),
1727                ],
1728                &[PreviewFeature::SpecialCondaEnvNames],
1729                || {
1730                    find_python_installation(
1731                        &PythonRequest::Default,
1732                        EnvironmentPreference::OnlyVirtual,
1733                        PythonPreference::OnlySystem,
1734                        &context.cache,
1735                    )
1736                },
1737            )?
1738            .unwrap();
1739
1740        assert_eq!(
1741            python.interpreter().python_full_version().to_string(),
1742            "3.12.6",
1743            "With special-conda-env-names preview, 'base' named env in matching dir should be treated as child"
1744        );
1745
1746        // When environment name matches directory name, it should be treated as a child environment
1747        let myenv_dir = context.tempdir.child("myenv");
1748        TestContext::mock_conda_prefix(&myenv_dir, "3.12.5")?;
1749        let python = context
1750            .run_with_vars_and_preview(
1751                &[
1752                    (EnvVars::CONDA_PREFIX, Some(myenv_dir.as_os_str())),
1753                    (EnvVars::CONDA_DEFAULT_ENV, Some(&OsString::from("myenv"))),
1754                ],
1755                &[],
1756                || {
1757                    find_python_installation(
1758                        &PythonRequest::Default,
1759                        EnvironmentPreference::OnlyVirtual,
1760                        PythonPreference::OnlySystem,
1761                        &context.cache,
1762                    )
1763                },
1764            )?
1765            .unwrap();
1766
1767        assert_eq!(
1768            python.interpreter().python_full_version().to_string(),
1769            "3.12.5",
1770            "We should find the child conda environment"
1771        );
1772
1773        // Test _CONDA_ROOT detection of base environment
1774        let conda_root_env = context.tempdir.child("conda-root");
1775        TestContext::mock_conda_prefix(&conda_root_env, "3.12.2")?;
1776
1777        // When _CONDA_ROOT matches CONDA_PREFIX, it should be treated as a base environment
1778        let result = context.run_with_vars(
1779            &[
1780                (EnvVars::CONDA_PREFIX, Some(conda_root_env.as_os_str())),
1781                (EnvVars::CONDA_ROOT, Some(conda_root_env.as_os_str())),
1782                (
1783                    EnvVars::CONDA_DEFAULT_ENV,
1784                    Some(&OsString::from("custom-name")),
1785                ),
1786            ],
1787            || {
1788                find_python_installation(
1789                    &PythonRequest::Default,
1790                    EnvironmentPreference::OnlyVirtual,
1791                    PythonPreference::OnlySystem,
1792                    &context.cache,
1793                )
1794            },
1795        )?;
1796
1797        assert!(
1798            matches!(result, Err(PythonNotFound { .. })),
1799            "Base environment detected via _CONDA_ROOT should be excluded from virtual environments; got {result:?}"
1800        );
1801
1802        // When _CONDA_ROOT doesn't match CONDA_PREFIX, it should be treated as a regular conda environment
1803        let other_conda_env = context.tempdir.child("other-conda");
1804        TestContext::mock_conda_prefix(&other_conda_env, "3.12.3")?;
1805
1806        let python = context
1807            .run_with_vars_and_preview(
1808                &[
1809                    (EnvVars::CONDA_PREFIX, Some(other_conda_env.as_os_str())),
1810                    (EnvVars::CONDA_ROOT, Some(conda_root_env.as_os_str())),
1811                    (
1812                        EnvVars::CONDA_DEFAULT_ENV,
1813                        Some(&OsString::from("other-conda")),
1814                    ),
1815                ],
1816                &[],
1817                || {
1818                    find_python_installation(
1819                        &PythonRequest::Default,
1820                        EnvironmentPreference::OnlyVirtual,
1821                        PythonPreference::OnlySystem,
1822                        &context.cache,
1823                    )
1824                },
1825            )?
1826            .unwrap();
1827
1828        assert_eq!(
1829            python.interpreter().python_full_version().to_string(),
1830            "3.12.3",
1831            "Non-base conda environment should be available for virtual environment preference"
1832        );
1833
1834        // When CONDA_PREFIX equals CONDA_DEFAULT_ENV, it should be treated as a virtual environment
1835        let unnamed_env = context.tempdir.child("my-conda-env");
1836        TestContext::mock_conda_prefix(&unnamed_env, "3.12.4")?;
1837        let unnamed_env_path = unnamed_env.to_string_lossy().to_string();
1838
1839        let python = context.run_with_vars(
1840            &[
1841                (EnvVars::CONDA_PREFIX, Some(unnamed_env.as_os_str())),
1842                (
1843                    EnvVars::CONDA_DEFAULT_ENV,
1844                    Some(&OsString::from(&unnamed_env_path)),
1845                ),
1846            ],
1847            || {
1848                find_python_installation(
1849                    &PythonRequest::Default,
1850                    EnvironmentPreference::OnlyVirtual,
1851                    PythonPreference::OnlySystem,
1852                    &context.cache,
1853                )
1854            },
1855        )??;
1856
1857        assert_eq!(
1858            python.interpreter().python_full_version().to_string(),
1859            "3.12.4",
1860            "We should find the unnamed conda environment"
1861        );
1862
1863        Ok(())
1864    }
1865
1866    #[test]
1867    fn find_python_from_conda_prefix_and_virtualenv() -> Result<()> {
1868        let context = TestContext::new()?;
1869        let venv = context.tempdir.child(".venv");
1870        TestContext::mock_venv(&venv, "3.12.0")?;
1871        let condaenv = context.tempdir.child("condaenv");
1872        TestContext::mock_conda_prefix(&condaenv, "3.12.1")?;
1873
1874        let python = context.run_with_vars(
1875            &[
1876                (EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())),
1877                (EnvVars::CONDA_PREFIX, Some(condaenv.as_os_str())),
1878            ],
1879            || {
1880                find_python_installation(
1881                    &PythonRequest::Default,
1882                    EnvironmentPreference::Any,
1883                    PythonPreference::OnlySystem,
1884                    &context.cache,
1885                )
1886            },
1887        )??;
1888        assert_eq!(
1889            python.interpreter().python_full_version().to_string(),
1890            "3.12.0",
1891            "We should prefer the non-conda python"
1892        );
1893
1894        // Put a virtual environment in the working directory
1895        let venv = context.workdir.child(".venv");
1896        TestContext::mock_venv(venv, "3.12.2")?;
1897        let python = context.run_with_vars(
1898            &[(EnvVars::CONDA_PREFIX, Some(condaenv.as_os_str()))],
1899            || {
1900                find_python_installation(
1901                    &PythonRequest::Default,
1902                    EnvironmentPreference::Any,
1903                    PythonPreference::OnlySystem,
1904                    &context.cache,
1905                )
1906            },
1907        )??;
1908        assert_eq!(
1909            python.interpreter().python_full_version().to_string(),
1910            "3.12.1",
1911            "We should prefer the conda python over inactive virtual environments"
1912        );
1913
1914        Ok(())
1915    }
1916
1917    #[test]
1918    fn find_python_from_discovered_python() -> Result<()> {
1919        let mut context = TestContext::new()?;
1920
1921        // Create a virtual environment in a parent of the workdir
1922        let venv = context.tempdir.child(".venv");
1923        TestContext::mock_venv(venv, "3.12.0")?;
1924
1925        let python = context.run(|| {
1926            find_python_installation(
1927                &PythonRequest::Default,
1928                EnvironmentPreference::Any,
1929                PythonPreference::OnlySystem,
1930                &context.cache,
1931            )
1932        })??;
1933
1934        assert_eq!(
1935            python.interpreter().python_full_version().to_string(),
1936            "3.12.0",
1937            "We should find the python"
1938        );
1939
1940        // Add some system versions to ensure we don't use those
1941        context.add_python_versions(&["3.12.1", "3.12.2"])?;
1942        let python = context.run(|| {
1943            find_python_installation(
1944                &PythonRequest::Default,
1945                EnvironmentPreference::Any,
1946                PythonPreference::OnlySystem,
1947                &context.cache,
1948            )
1949        })??;
1950
1951        assert_eq!(
1952            python.interpreter().python_full_version().to_string(),
1953            "3.12.0",
1954            "We should prefer the discovered virtual environment over available system versions"
1955        );
1956
1957        Ok(())
1958    }
1959
1960    #[test]
1961    fn find_python_skips_broken_active_python() -> Result<()> {
1962        let context = TestContext::new()?;
1963        let venv = context.tempdir.child(".venv");
1964        TestContext::mock_venv(&venv, "3.12.0")?;
1965
1966        // Delete the pyvenv cfg to break the virtualenv
1967        fs_err::remove_file(venv.join("pyvenv.cfg"))?;
1968
1969        let python =
1970            context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
1971                find_python_installation(
1972                    &PythonRequest::Default,
1973                    EnvironmentPreference::Any,
1974                    PythonPreference::OnlySystem,
1975                    &context.cache,
1976                )
1977            })??;
1978        assert_eq!(
1979            python.interpreter().python_full_version().to_string(),
1980            "3.12.0",
1981            // TODO(zanieb): We should skip this python, why don't we?
1982            "We should prefer the active environment"
1983        );
1984
1985        Ok(())
1986    }
1987
1988    #[test]
1989    fn find_python_from_parent_interpreter() -> Result<()> {
1990        let mut context = TestContext::new()?;
1991
1992        let parent = context.tempdir.child("python").to_path_buf();
1993        TestContext::create_mock_interpreter(
1994            &parent,
1995            &PythonVersion::from_str("3.12.0").unwrap(),
1996            ImplementationName::CPython,
1997            // Note we mark this as a system interpreter instead of a virtual environment
1998            true,
1999            false,
2000        )?;
2001
2002        let python = context.run_with_vars(
2003            &[(
2004                EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
2005                Some(parent.as_os_str()),
2006            )],
2007            || {
2008                find_python_installation(
2009                    &PythonRequest::Default,
2010                    EnvironmentPreference::Any,
2011                    PythonPreference::OnlySystem,
2012                    &context.cache,
2013                )
2014            },
2015        )??;
2016        assert_eq!(
2017            python.interpreter().python_full_version().to_string(),
2018            "3.12.0",
2019            "We should find the parent interpreter"
2020        );
2021
2022        // Parent interpreters are preferred over virtual environments and system interpreters
2023        let venv = context.tempdir.child(".venv");
2024        TestContext::mock_venv(&venv, "3.12.2")?;
2025        context.add_python_versions(&["3.12.3"])?;
2026        let python = context.run_with_vars(
2027            &[
2028                (
2029                    EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
2030                    Some(parent.as_os_str()),
2031                ),
2032                (EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())),
2033            ],
2034            || {
2035                find_python_installation(
2036                    &PythonRequest::Default,
2037                    EnvironmentPreference::Any,
2038                    PythonPreference::OnlySystem,
2039                    &context.cache,
2040                )
2041            },
2042        )??;
2043        assert_eq!(
2044            python.interpreter().python_full_version().to_string(),
2045            "3.12.0",
2046            "We should prefer the parent interpreter"
2047        );
2048
2049        // Test with `EnvironmentPreference::ExplicitSystem`
2050        let python = context.run_with_vars(
2051            &[
2052                (
2053                    EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
2054                    Some(parent.as_os_str()),
2055                ),
2056                (EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())),
2057            ],
2058            || {
2059                find_python_installation(
2060                    &PythonRequest::Default,
2061                    EnvironmentPreference::ExplicitSystem,
2062                    PythonPreference::OnlySystem,
2063                    &context.cache,
2064                )
2065            },
2066        )??;
2067        assert_eq!(
2068            python.interpreter().python_full_version().to_string(),
2069            "3.12.0",
2070            "We should prefer the parent interpreter"
2071        );
2072
2073        // Test with `EnvironmentPreference::OnlySystem`
2074        let python = context.run_with_vars(
2075            &[
2076                (
2077                    EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
2078                    Some(parent.as_os_str()),
2079                ),
2080                (EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())),
2081            ],
2082            || {
2083                find_python_installation(
2084                    &PythonRequest::Default,
2085                    EnvironmentPreference::OnlySystem,
2086                    PythonPreference::OnlySystem,
2087                    &context.cache,
2088                )
2089            },
2090        )??;
2091        assert_eq!(
2092            python.interpreter().python_full_version().to_string(),
2093            "3.12.0",
2094            "We should prefer the parent interpreter since it's not virtual"
2095        );
2096
2097        // Test with `EnvironmentPreference::OnlyVirtual`
2098        let python = context.run_with_vars(
2099            &[
2100                (
2101                    EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
2102                    Some(parent.as_os_str()),
2103                ),
2104                (EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())),
2105            ],
2106            || {
2107                find_python_installation(
2108                    &PythonRequest::Default,
2109                    EnvironmentPreference::OnlyVirtual,
2110                    PythonPreference::OnlySystem,
2111                    &context.cache,
2112                )
2113            },
2114        )??;
2115        assert_eq!(
2116            python.interpreter().python_full_version().to_string(),
2117            "3.12.2",
2118            "We find the virtual environment Python because a system is explicitly not allowed"
2119        );
2120
2121        Ok(())
2122    }
2123
2124    #[test]
2125    fn find_python_from_parent_interpreter_prerelease() -> Result<()> {
2126        let mut context = TestContext::new()?;
2127        context.add_python_versions(&["3.12.0"])?;
2128        let parent = context.tempdir.child("python").to_path_buf();
2129        TestContext::create_mock_interpreter(
2130            &parent,
2131            &PythonVersion::from_str("3.13.0rc2").unwrap(),
2132            ImplementationName::CPython,
2133            // Note we mark this as a system interpreter instead of a virtual environment
2134            true,
2135            false,
2136        )?;
2137
2138        let python = context.run_with_vars(
2139            &[(
2140                EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
2141                Some(parent.as_os_str()),
2142            )],
2143            || {
2144                find_python_installation(
2145                    &PythonRequest::Default,
2146                    EnvironmentPreference::Any,
2147                    PythonPreference::OnlySystem,
2148                    &context.cache,
2149                )
2150            },
2151        )??;
2152        assert_eq!(
2153            python.interpreter().python_full_version().to_string(),
2154            "3.13.0rc2",
2155            "We should find the parent interpreter"
2156        );
2157
2158        Ok(())
2159    }
2160
2161    #[test]
2162    fn find_python_active_python_skipped_if_system_required() -> Result<()> {
2163        let mut context = TestContext::new()?;
2164        let venv = context.tempdir.child(".venv");
2165        TestContext::mock_venv(&venv, "3.9.0")?;
2166        context.add_python_versions(&["3.10.0", "3.11.1", "3.12.2"])?;
2167
2168        // Without a specific request
2169        let python =
2170            context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
2171                find_python_installation(
2172                    &PythonRequest::Default,
2173                    EnvironmentPreference::OnlySystem,
2174                    PythonPreference::OnlySystem,
2175                    &context.cache,
2176                )
2177            })??;
2178        assert_eq!(
2179            python.interpreter().python_full_version().to_string(),
2180            "3.10.0",
2181            "We should skip the active environment"
2182        );
2183
2184        // With a requested minor version
2185        let python =
2186            context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
2187                find_python_installation(
2188                    &PythonRequest::parse("3.12"),
2189                    EnvironmentPreference::OnlySystem,
2190                    PythonPreference::OnlySystem,
2191                    &context.cache,
2192                )
2193            })??;
2194        assert_eq!(
2195            python.interpreter().python_full_version().to_string(),
2196            "3.12.2",
2197            "We should skip the active environment"
2198        );
2199
2200        // With a patch version that cannot be python
2201        let result =
2202            context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
2203                find_python_installation(
2204                    &PythonRequest::parse("3.12.3"),
2205                    EnvironmentPreference::OnlySystem,
2206                    PythonPreference::OnlySystem,
2207                    &context.cache,
2208                )
2209            })?;
2210        assert!(
2211            result.is_err(),
2212            "We should not find an python; got {result:?}"
2213        );
2214
2215        Ok(())
2216    }
2217
2218    #[test]
2219    fn find_python_fails_if_no_virtualenv_and_system_not_allowed() -> Result<()> {
2220        let mut context = TestContext::new()?;
2221        context.add_python_versions(&["3.10.1", "3.11.2"])?;
2222
2223        let result = context.run(|| {
2224            find_python_installation(
2225                &PythonRequest::Default,
2226                EnvironmentPreference::OnlyVirtual,
2227                PythonPreference::OnlySystem,
2228                &context.cache,
2229            )
2230        })?;
2231        assert!(
2232            matches!(result, Err(PythonNotFound { .. })),
2233            "We should not find an python; got {result:?}"
2234        );
2235
2236        // With an invalid virtual environment variable
2237        let result = context.run_with_vars(
2238            &[(EnvVars::VIRTUAL_ENV, Some(context.tempdir.as_os_str()))],
2239            || {
2240                find_python_installation(
2241                    &PythonRequest::parse("3.12.3"),
2242                    EnvironmentPreference::OnlySystem,
2243                    PythonPreference::OnlySystem,
2244                    &context.cache,
2245                )
2246            },
2247        )?;
2248        assert!(
2249            matches!(result, Err(PythonNotFound { .. })),
2250            "We should not find an python; got {result:?}"
2251        );
2252        Ok(())
2253    }
2254
2255    #[test]
2256    fn find_python_allows_name_in_working_directory() -> Result<()> {
2257        let context = TestContext::new()?;
2258        context.add_python_to_workdir("foobar", "3.10.0")?;
2259
2260        let python = context.run(|| {
2261            find_python_installation(
2262                &PythonRequest::parse("foobar"),
2263                EnvironmentPreference::Any,
2264                PythonPreference::OnlySystem,
2265                &context.cache,
2266            )
2267        })??;
2268        assert_eq!(
2269            python.interpreter().python_full_version().to_string(),
2270            "3.10.0",
2271            "We should find the named executable"
2272        );
2273
2274        let result = context.run(|| {
2275            find_python_installation(
2276                &PythonRequest::Default,
2277                EnvironmentPreference::Any,
2278                PythonPreference::OnlySystem,
2279                &context.cache,
2280            )
2281        })?;
2282        assert!(
2283            matches!(result, Err(PythonNotFound { .. })),
2284            "We should not find it without a specific request"
2285        );
2286
2287        let result = context.run(|| {
2288            find_python_installation(
2289                &PythonRequest::parse("3.10.0"),
2290                EnvironmentPreference::Any,
2291                PythonPreference::OnlySystem,
2292                &context.cache,
2293            )
2294        })?;
2295        assert!(
2296            matches!(result, Err(PythonNotFound { .. })),
2297            "We should not find it via a matching version request"
2298        );
2299
2300        Ok(())
2301    }
2302
2303    #[test]
2304    fn find_python_allows_relative_file_path() -> Result<()> {
2305        let mut context = TestContext::new()?;
2306        let python = context.workdir.child("foo").join("bar");
2307        TestContext::create_mock_interpreter(
2308            &python,
2309            &PythonVersion::from_str("3.10.0").unwrap(),
2310            ImplementationName::default(),
2311            true,
2312            false,
2313        )?;
2314
2315        let python = context.run(|| {
2316            find_python_installation(
2317                &PythonRequest::parse("./foo/bar"),
2318                EnvironmentPreference::Any,
2319                PythonPreference::OnlySystem,
2320                &context.cache,
2321            )
2322        })??;
2323        assert_eq!(
2324            python.interpreter().python_full_version().to_string(),
2325            "3.10.0",
2326            "We should find the `bar` executable"
2327        );
2328
2329        context.add_python_versions(&["3.11.1"])?;
2330        let python = context.run(|| {
2331            find_python_installation(
2332                &PythonRequest::parse("./foo/bar"),
2333                EnvironmentPreference::Any,
2334                PythonPreference::OnlySystem,
2335                &context.cache,
2336            )
2337        })??;
2338        assert_eq!(
2339            python.interpreter().python_full_version().to_string(),
2340            "3.10.0",
2341            "We should prefer the `bar` executable over the system and virtualenvs"
2342        );
2343
2344        Ok(())
2345    }
2346
2347    #[test]
2348    fn find_python_allows_absolute_file_path() -> Result<()> {
2349        let mut context = TestContext::new()?;
2350        let python_path = context.tempdir.child("foo").join("bar");
2351        TestContext::create_mock_interpreter(
2352            &python_path,
2353            &PythonVersion::from_str("3.10.0").unwrap(),
2354            ImplementationName::default(),
2355            true,
2356            false,
2357        )?;
2358
2359        let python = context.run(|| {
2360            find_python_installation(
2361                &PythonRequest::parse(python_path.to_str().unwrap()),
2362                EnvironmentPreference::Any,
2363                PythonPreference::OnlySystem,
2364                &context.cache,
2365            )
2366        })??;
2367        assert_eq!(
2368            python.interpreter().python_full_version().to_string(),
2369            "3.10.0",
2370            "We should find the `bar` executable"
2371        );
2372
2373        // With `EnvironmentPreference::ExplicitSystem`
2374        let python = context.run(|| {
2375            find_python_installation(
2376                &PythonRequest::parse(python_path.to_str().unwrap()),
2377                EnvironmentPreference::ExplicitSystem,
2378                PythonPreference::OnlySystem,
2379                &context.cache,
2380            )
2381        })??;
2382        assert_eq!(
2383            python.interpreter().python_full_version().to_string(),
2384            "3.10.0",
2385            "We should allow the `bar` executable with explicit system"
2386        );
2387
2388        // With `EnvironmentPreference::OnlyVirtual`
2389        let python = context.run(|| {
2390            find_python_installation(
2391                &PythonRequest::parse(python_path.to_str().unwrap()),
2392                EnvironmentPreference::OnlyVirtual,
2393                PythonPreference::OnlySystem,
2394                &context.cache,
2395            )
2396        })??;
2397        assert_eq!(
2398            python.interpreter().python_full_version().to_string(),
2399            "3.10.0",
2400            "We should allow the `bar` executable and verify it is virtual"
2401        );
2402
2403        context.add_python_versions(&["3.11.1"])?;
2404        let python = context.run(|| {
2405            find_python_installation(
2406                &PythonRequest::parse(python_path.to_str().unwrap()),
2407                EnvironmentPreference::Any,
2408                PythonPreference::OnlySystem,
2409                &context.cache,
2410            )
2411        })??;
2412        assert_eq!(
2413            python.interpreter().python_full_version().to_string(),
2414            "3.10.0",
2415            "We should prefer the `bar` executable over the system and virtualenvs"
2416        );
2417
2418        Ok(())
2419    }
2420
2421    #[test]
2422    fn find_python_allows_venv_directory_path() -> Result<()> {
2423        let mut context = TestContext::new()?;
2424
2425        let venv = context.tempdir.child("foo").child(".venv");
2426        TestContext::mock_venv(&venv, "3.10.0")?;
2427        let python = context.run(|| {
2428            find_python_installation(
2429                &PythonRequest::parse("../foo/.venv"),
2430                EnvironmentPreference::Any,
2431                PythonPreference::OnlySystem,
2432                &context.cache,
2433            )
2434        })??;
2435        assert_eq!(
2436            python.interpreter().python_full_version().to_string(),
2437            "3.10.0",
2438            "We should find the relative venv path"
2439        );
2440
2441        let python = context.run(|| {
2442            find_python_installation(
2443                &PythonRequest::parse(venv.to_str().unwrap()),
2444                EnvironmentPreference::Any,
2445                PythonPreference::OnlySystem,
2446                &context.cache,
2447            )
2448        })??;
2449        assert_eq!(
2450            python.interpreter().python_full_version().to_string(),
2451            "3.10.0",
2452            "We should find the absolute venv path"
2453        );
2454
2455        // We should allow it to be a directory that _looks_ like a virtual environment.
2456        let python_path = context.tempdir.child("bar").join("bin").join("python");
2457        TestContext::create_mock_interpreter(
2458            &python_path,
2459            &PythonVersion::from_str("3.10.0").unwrap(),
2460            ImplementationName::default(),
2461            true,
2462            false,
2463        )?;
2464        let python = context.run(|| {
2465            find_python_installation(
2466                &PythonRequest::parse(context.tempdir.child("bar").to_str().unwrap()),
2467                EnvironmentPreference::Any,
2468                PythonPreference::OnlySystem,
2469                &context.cache,
2470            )
2471        })??;
2472        assert_eq!(
2473            python.interpreter().python_full_version().to_string(),
2474            "3.10.0",
2475            "We should find the executable in the directory"
2476        );
2477
2478        let other_venv = context.tempdir.child("foobar").child(".venv");
2479        TestContext::mock_venv(&other_venv, "3.11.1")?;
2480        context.add_python_versions(&["3.12.2"])?;
2481        let python = context.run_with_vars(
2482            &[(EnvVars::VIRTUAL_ENV, Some(other_venv.as_os_str()))],
2483            || {
2484                find_python_installation(
2485                    &PythonRequest::parse(venv.to_str().unwrap()),
2486                    EnvironmentPreference::Any,
2487                    PythonPreference::OnlySystem,
2488                    &context.cache,
2489                )
2490            },
2491        )??;
2492        assert_eq!(
2493            python.interpreter().python_full_version().to_string(),
2494            "3.10.0",
2495            "We should prefer the requested directory over the system and active virtual environments"
2496        );
2497
2498        Ok(())
2499    }
2500
2501    #[test]
2502    fn find_python_venv_symlink() -> Result<()> {
2503        let context = TestContext::new()?;
2504
2505        let venv = context.tempdir.child("target").child("env");
2506        TestContext::mock_venv(&venv, "3.10.6")?;
2507        let symlink = context.tempdir.child("proj").child(".venv");
2508        context.tempdir.child("proj").create_dir_all()?;
2509        symlink.symlink_to_dir(venv)?;
2510
2511        let python = context.run(|| {
2512            find_python_installation(
2513                &PythonRequest::parse("../proj/.venv"),
2514                EnvironmentPreference::Any,
2515                PythonPreference::OnlySystem,
2516                &context.cache,
2517            )
2518        })??;
2519        assert_eq!(
2520            python.interpreter().python_full_version().to_string(),
2521            "3.10.6",
2522            "We should find the symlinked venv"
2523        );
2524        Ok(())
2525    }
2526
2527    #[test]
2528    fn find_python_treats_missing_file_path_as_file() -> Result<()> {
2529        let context = TestContext::new()?;
2530        context.workdir.child("foo").create_dir_all()?;
2531
2532        let result = context.run(|| {
2533            find_python_installation(
2534                &PythonRequest::parse("./foo/bar"),
2535                EnvironmentPreference::Any,
2536                PythonPreference::OnlySystem,
2537                &context.cache,
2538            )
2539        })?;
2540        assert!(
2541            matches!(result, Err(PythonNotFound { .. })),
2542            "We should not find the file; got {result:?}"
2543        );
2544
2545        Ok(())
2546    }
2547
2548    #[test]
2549    fn find_python_executable_name_in_search_path() -> Result<()> {
2550        let mut context = TestContext::new()?;
2551        let python = context.tempdir.child("foo").join("bar");
2552        TestContext::create_mock_interpreter(
2553            &python,
2554            &PythonVersion::from_str("3.10.0").unwrap(),
2555            ImplementationName::default(),
2556            true,
2557            false,
2558        )?;
2559        context.add_to_search_path(context.tempdir.child("foo").to_path_buf());
2560
2561        let python = context.run(|| {
2562            find_python_installation(
2563                &PythonRequest::parse("bar"),
2564                EnvironmentPreference::Any,
2565                PythonPreference::OnlySystem,
2566                &context.cache,
2567            )
2568        })??;
2569        assert_eq!(
2570            python.interpreter().python_full_version().to_string(),
2571            "3.10.0",
2572            "We should find the `bar` executable"
2573        );
2574
2575        // With [`EnvironmentPreference::OnlyVirtual`], we should not allow the interpreter
2576        let result = context.run(|| {
2577            find_python_installation(
2578                &PythonRequest::parse("bar"),
2579                EnvironmentPreference::ExplicitSystem,
2580                PythonPreference::OnlySystem,
2581                &context.cache,
2582            )
2583        })?;
2584        assert!(
2585            matches!(result, Err(PythonNotFound { .. })),
2586            "We should not allow a system interpreter; got {result:?}"
2587        );
2588
2589        // Unless it's a virtual environment interpreter
2590        let mut context = TestContext::new()?;
2591        let python = context.tempdir.child("foo").join("bar");
2592        TestContext::create_mock_interpreter(
2593            &python,
2594            &PythonVersion::from_str("3.10.0").unwrap(),
2595            ImplementationName::default(),
2596            false, // Not a system interpreter
2597            false,
2598        )?;
2599        context.add_to_search_path(context.tempdir.child("foo").to_path_buf());
2600
2601        let python = context
2602            .run(|| {
2603                find_python_installation(
2604                    &PythonRequest::parse("bar"),
2605                    EnvironmentPreference::ExplicitSystem,
2606                    PythonPreference::OnlySystem,
2607                    &context.cache,
2608                )
2609            })
2610            .unwrap()
2611            .unwrap();
2612        assert_eq!(
2613            python.interpreter().python_full_version().to_string(),
2614            "3.10.0",
2615            "We should find the `bar` executable"
2616        );
2617
2618        Ok(())
2619    }
2620
2621    #[test]
2622    fn find_python_pypy() -> Result<()> {
2623        let mut context = TestContext::new()?;
2624
2625        context.add_python_interpreters(&[(true, ImplementationName::PyPy, "pypy", "3.10.0")])?;
2626        let result = context.run(|| {
2627            find_python_installation(
2628                &PythonRequest::Default,
2629                EnvironmentPreference::Any,
2630                PythonPreference::OnlySystem,
2631                &context.cache,
2632            )
2633        })?;
2634        assert!(
2635            matches!(result, Err(PythonNotFound { .. })),
2636            "We should not find the pypy interpreter if not named `python` or requested; got {result:?}"
2637        );
2638
2639        // But we should find it
2640        context.reset_search_path();
2641        context.add_python_interpreters(&[(true, ImplementationName::PyPy, "python", "3.10.1")])?;
2642        let python = context.run(|| {
2643            find_python_installation(
2644                &PythonRequest::Default,
2645                EnvironmentPreference::Any,
2646                PythonPreference::OnlySystem,
2647                &context.cache,
2648            )
2649        })??;
2650        assert_eq!(
2651            python.interpreter().python_full_version().to_string(),
2652            "3.10.1",
2653            "We should find the pypy interpreter if it's the only one"
2654        );
2655
2656        let python = context.run(|| {
2657            find_python_installation(
2658                &PythonRequest::parse("pypy"),
2659                EnvironmentPreference::Any,
2660                PythonPreference::OnlySystem,
2661                &context.cache,
2662            )
2663        })??;
2664        assert_eq!(
2665            python.interpreter().python_full_version().to_string(),
2666            "3.10.1",
2667            "We should find the pypy interpreter if it's requested"
2668        );
2669
2670        Ok(())
2671    }
2672
2673    #[test]
2674    fn find_python_pypy_request_ignores_cpython() -> Result<()> {
2675        let mut context = TestContext::new()?;
2676        context.add_python_interpreters(&[
2677            (true, ImplementationName::CPython, "python", "3.10.0"),
2678            (true, ImplementationName::PyPy, "pypy", "3.10.1"),
2679        ])?;
2680
2681        let python = context.run(|| {
2682            find_python_installation(
2683                &PythonRequest::parse("pypy"),
2684                EnvironmentPreference::Any,
2685                PythonPreference::OnlySystem,
2686                &context.cache,
2687            )
2688        })??;
2689        assert_eq!(
2690            python.interpreter().python_full_version().to_string(),
2691            "3.10.1",
2692            "We should skip the CPython interpreter"
2693        );
2694
2695        let python = context.run(|| {
2696            find_python_installation(
2697                &PythonRequest::Default,
2698                EnvironmentPreference::Any,
2699                PythonPreference::OnlySystem,
2700                &context.cache,
2701            )
2702        })??;
2703        assert_eq!(
2704            python.interpreter().python_full_version().to_string(),
2705            "3.10.0",
2706            "We should take the first interpreter without a specific request"
2707        );
2708
2709        Ok(())
2710    }
2711
2712    #[test]
2713    fn find_python_pypy_request_skips_wrong_versions() -> Result<()> {
2714        let mut context = TestContext::new()?;
2715        context.add_python_interpreters(&[
2716            (true, ImplementationName::PyPy, "pypy", "3.9"),
2717            (true, ImplementationName::PyPy, "pypy", "3.10.1"),
2718        ])?;
2719
2720        let python = context.run(|| {
2721            find_python_installation(
2722                &PythonRequest::parse("pypy3.10"),
2723                EnvironmentPreference::Any,
2724                PythonPreference::OnlySystem,
2725                &context.cache,
2726            )
2727        })??;
2728        assert_eq!(
2729            python.interpreter().python_full_version().to_string(),
2730            "3.10.1",
2731            "We should skip the first interpreter"
2732        );
2733
2734        Ok(())
2735    }
2736
2737    #[test]
2738    fn find_python_pypy_finds_executable_with_version_name() -> Result<()> {
2739        let mut context = TestContext::new()?;
2740        context.add_python_interpreters(&[
2741            (true, ImplementationName::PyPy, "pypy3.9", "3.10.0"), // We don't consider this one because of the executable name
2742            (true, ImplementationName::PyPy, "pypy3.10", "3.10.1"),
2743            (true, ImplementationName::PyPy, "pypy", "3.10.2"),
2744        ])?;
2745
2746        let python = context.run(|| {
2747            find_python_installation(
2748                &PythonRequest::parse("pypy@3.10"),
2749                EnvironmentPreference::Any,
2750                PythonPreference::OnlySystem,
2751                &context.cache,
2752            )
2753        })??;
2754        assert_eq!(
2755            python.interpreter().python_full_version().to_string(),
2756            "3.10.1",
2757            "We should find the requested interpreter version"
2758        );
2759
2760        Ok(())
2761    }
2762
2763    #[test]
2764    fn find_python_all_minors() -> Result<()> {
2765        let mut context = TestContext::new()?;
2766        context.add_python_interpreters(&[
2767            (true, ImplementationName::CPython, "python", "3.10.0"),
2768            (true, ImplementationName::CPython, "python3", "3.10.0"),
2769            (true, ImplementationName::CPython, "python3.12", "3.12.0"),
2770        ])?;
2771
2772        let python = context.run(|| {
2773            find_python_installation(
2774                &PythonRequest::parse(">= 3.11"),
2775                EnvironmentPreference::Any,
2776                PythonPreference::OnlySystem,
2777                &context.cache,
2778            )
2779        })??;
2780        assert_eq!(
2781            python.interpreter().python_full_version().to_string(),
2782            "3.12.0",
2783            "We should find matching minor version even if they aren't called `python` or `python3`"
2784        );
2785
2786        Ok(())
2787    }
2788
2789    #[test]
2790    fn find_python_all_minors_prerelease() -> Result<()> {
2791        let mut context = TestContext::new()?;
2792        context.add_python_interpreters(&[
2793            (true, ImplementationName::CPython, "python", "3.10.0"),
2794            (true, ImplementationName::CPython, "python3", "3.10.0"),
2795            (true, ImplementationName::CPython, "python3.11", "3.11.0b0"),
2796        ])?;
2797
2798        let python = context.run(|| {
2799            find_python_installation(
2800                &PythonRequest::parse(">= 3.11"),
2801                EnvironmentPreference::Any,
2802                PythonPreference::OnlySystem,
2803                &context.cache,
2804            )
2805        })??;
2806        assert_eq!(
2807            python.interpreter().python_full_version().to_string(),
2808            "3.11.0b0",
2809            "We should find the 3.11 prerelease even though >=3.11 would normally exclude prereleases"
2810        );
2811
2812        Ok(())
2813    }
2814
2815    #[test]
2816    fn find_python_all_minors_prerelease_next() -> Result<()> {
2817        let mut context = TestContext::new()?;
2818        context.add_python_interpreters(&[
2819            (true, ImplementationName::CPython, "python", "3.10.0"),
2820            (true, ImplementationName::CPython, "python3", "3.10.0"),
2821            (true, ImplementationName::CPython, "python3.12", "3.12.0b0"),
2822        ])?;
2823
2824        let python = context.run(|| {
2825            find_python_installation(
2826                &PythonRequest::parse(">= 3.11"),
2827                EnvironmentPreference::Any,
2828                PythonPreference::OnlySystem,
2829                &context.cache,
2830            )
2831        })??;
2832        assert_eq!(
2833            python.interpreter().python_full_version().to_string(),
2834            "3.12.0b0",
2835            "We should find the 3.12 prerelease"
2836        );
2837
2838        Ok(())
2839    }
2840
2841    #[test]
2842    fn find_python_graalpy() -> Result<()> {
2843        let mut context = TestContext::new()?;
2844
2845        context.add_python_interpreters(&[(
2846            true,
2847            ImplementationName::GraalPy,
2848            "graalpy",
2849            "3.10.0",
2850        )])?;
2851        let result = context.run(|| {
2852            find_python_installation(
2853                &PythonRequest::Default,
2854                EnvironmentPreference::Any,
2855                PythonPreference::OnlySystem,
2856                &context.cache,
2857            )
2858        })?;
2859        assert!(
2860            matches!(result, Err(PythonNotFound { .. })),
2861            "We should not the graalpy interpreter if not named `python` or requested; got {result:?}"
2862        );
2863
2864        // But we should find it
2865        context.reset_search_path();
2866        context.add_python_interpreters(&[(
2867            true,
2868            ImplementationName::GraalPy,
2869            "python",
2870            "3.10.1",
2871        )])?;
2872        let python = context.run(|| {
2873            find_python_installation(
2874                &PythonRequest::Default,
2875                EnvironmentPreference::Any,
2876                PythonPreference::OnlySystem,
2877                &context.cache,
2878            )
2879        })??;
2880        assert_eq!(
2881            python.interpreter().python_full_version().to_string(),
2882            "3.10.1",
2883            "We should find the graalpy interpreter if it's the only one"
2884        );
2885
2886        let python = context.run(|| {
2887            find_python_installation(
2888                &PythonRequest::parse("graalpy"),
2889                EnvironmentPreference::Any,
2890                PythonPreference::OnlySystem,
2891                &context.cache,
2892            )
2893        })??;
2894        assert_eq!(
2895            python.interpreter().python_full_version().to_string(),
2896            "3.10.1",
2897            "We should find the graalpy interpreter if it's requested"
2898        );
2899
2900        Ok(())
2901    }
2902
2903    #[test]
2904    fn find_python_graalpy_request_ignores_cpython() -> Result<()> {
2905        let mut context = TestContext::new()?;
2906        context.add_python_interpreters(&[
2907            (true, ImplementationName::CPython, "python", "3.10.0"),
2908            (true, ImplementationName::GraalPy, "graalpy", "3.10.1"),
2909        ])?;
2910
2911        let python = context.run(|| {
2912            find_python_installation(
2913                &PythonRequest::parse("graalpy"),
2914                EnvironmentPreference::Any,
2915                PythonPreference::OnlySystem,
2916                &context.cache,
2917            )
2918        })??;
2919        assert_eq!(
2920            python.interpreter().python_full_version().to_string(),
2921            "3.10.1",
2922            "We should skip the CPython interpreter"
2923        );
2924
2925        let python = context.run(|| {
2926            find_python_installation(
2927                &PythonRequest::Default,
2928                EnvironmentPreference::Any,
2929                PythonPreference::OnlySystem,
2930                &context.cache,
2931            )
2932        })??;
2933        assert_eq!(
2934            python.interpreter().python_full_version().to_string(),
2935            "3.10.0",
2936            "We should take the first interpreter without a specific request"
2937        );
2938
2939        Ok(())
2940    }
2941
2942    #[test]
2943    fn find_python_executable_name_preference() -> Result<()> {
2944        let mut context = TestContext::new()?;
2945        TestContext::create_mock_interpreter(
2946            &context.tempdir.join("pypy3.10"),
2947            &PythonVersion::from_str("3.10.0").unwrap(),
2948            ImplementationName::PyPy,
2949            true,
2950            false,
2951        )?;
2952        TestContext::create_mock_interpreter(
2953            &context.tempdir.join("pypy"),
2954            &PythonVersion::from_str("3.10.1").unwrap(),
2955            ImplementationName::PyPy,
2956            true,
2957            false,
2958        )?;
2959        context.add_to_search_path(context.tempdir.to_path_buf());
2960
2961        let python = context
2962            .run(|| {
2963                find_python_installation(
2964                    &PythonRequest::parse("pypy@3.10"),
2965                    EnvironmentPreference::Any,
2966                    PythonPreference::OnlySystem,
2967                    &context.cache,
2968                )
2969            })
2970            .unwrap()
2971            .unwrap();
2972        assert_eq!(
2973            python.interpreter().python_full_version().to_string(),
2974            "3.10.0",
2975            "We should prefer the versioned one when a version is requested"
2976        );
2977
2978        let python = context
2979            .run(|| {
2980                find_python_installation(
2981                    &PythonRequest::parse("pypy"),
2982                    EnvironmentPreference::Any,
2983                    PythonPreference::OnlySystem,
2984                    &context.cache,
2985                )
2986            })
2987            .unwrap()
2988            .unwrap();
2989        assert_eq!(
2990            python.interpreter().python_full_version().to_string(),
2991            "3.10.1",
2992            "We should prefer the generic one when no version is requested"
2993        );
2994
2995        let mut context = TestContext::new()?;
2996        TestContext::create_mock_interpreter(
2997            &context.tempdir.join("python3.10"),
2998            &PythonVersion::from_str("3.10.0").unwrap(),
2999            ImplementationName::PyPy,
3000            true,
3001            false,
3002        )?;
3003        TestContext::create_mock_interpreter(
3004            &context.tempdir.join("pypy"),
3005            &PythonVersion::from_str("3.10.1").unwrap(),
3006            ImplementationName::PyPy,
3007            true,
3008            false,
3009        )?;
3010        TestContext::create_mock_interpreter(
3011            &context.tempdir.join("python"),
3012            &PythonVersion::from_str("3.10.2").unwrap(),
3013            ImplementationName::PyPy,
3014            true,
3015            false,
3016        )?;
3017        context.add_to_search_path(context.tempdir.to_path_buf());
3018
3019        let python = context
3020            .run(|| {
3021                find_python_installation(
3022                    &PythonRequest::parse("pypy@3.10"),
3023                    EnvironmentPreference::Any,
3024                    PythonPreference::OnlySystem,
3025                    &context.cache,
3026                )
3027            })
3028            .unwrap()
3029            .unwrap();
3030        assert_eq!(
3031            python.interpreter().python_full_version().to_string(),
3032            "3.10.1",
3033            "We should prefer the implementation name over the generic name"
3034        );
3035
3036        let python = context
3037            .run(|| {
3038                find_python_installation(
3039                    &PythonRequest::parse("default"),
3040                    EnvironmentPreference::Any,
3041                    PythonPreference::OnlySystem,
3042                    &context.cache,
3043                )
3044            })
3045            .unwrap()
3046            .unwrap();
3047        assert_eq!(
3048            python.interpreter().python_full_version().to_string(),
3049            "3.10.2",
3050            "We should prefer the generic name over the implementation name, but not the versioned name"
3051        );
3052
3053        // We prefer `python` executables over `graalpy` executables in the same directory
3054        // if they are both GraalPy
3055        let mut context = TestContext::new()?;
3056        TestContext::create_mock_interpreter(
3057            &context.tempdir.join("python"),
3058            &PythonVersion::from_str("3.10.0").unwrap(),
3059            ImplementationName::GraalPy,
3060            true,
3061            false,
3062        )?;
3063        TestContext::create_mock_interpreter(
3064            &context.tempdir.join("graalpy"),
3065            &PythonVersion::from_str("3.10.1").unwrap(),
3066            ImplementationName::GraalPy,
3067            true,
3068            false,
3069        )?;
3070        context.add_to_search_path(context.tempdir.to_path_buf());
3071
3072        let python = context
3073            .run(|| {
3074                find_python_installation(
3075                    &PythonRequest::parse("graalpy@3.10"),
3076                    EnvironmentPreference::Any,
3077                    PythonPreference::OnlySystem,
3078                    &context.cache,
3079                )
3080            })
3081            .unwrap()
3082            .unwrap();
3083        assert_eq!(
3084            python.interpreter().python_full_version().to_string(),
3085            "3.10.1",
3086        );
3087
3088        // And `python` executables earlier in the search path will take precedence
3089        context.reset_search_path();
3090        context.add_python_interpreters(&[
3091            (true, ImplementationName::GraalPy, "python", "3.10.2"),
3092            (true, ImplementationName::GraalPy, "graalpy", "3.10.3"),
3093        ])?;
3094        let python = context
3095            .run(|| {
3096                find_python_installation(
3097                    &PythonRequest::parse("graalpy@3.10"),
3098                    EnvironmentPreference::Any,
3099                    PythonPreference::OnlySystem,
3100                    &context.cache,
3101                )
3102            })
3103            .unwrap()
3104            .unwrap();
3105        assert_eq!(
3106            python.interpreter().python_full_version().to_string(),
3107            "3.10.2",
3108        );
3109
3110        // And `graalpy` executables earlier in the search path will take precedence
3111        context.reset_search_path();
3112        context.add_python_interpreters(&[
3113            (true, ImplementationName::GraalPy, "graalpy", "3.10.3"),
3114            (true, ImplementationName::GraalPy, "python", "3.10.2"),
3115        ])?;
3116        let python = context
3117            .run(|| {
3118                find_python_installation(
3119                    &PythonRequest::parse("graalpy@3.10"),
3120                    EnvironmentPreference::Any,
3121                    PythonPreference::OnlySystem,
3122                    &context.cache,
3123                )
3124            })
3125            .unwrap()
3126            .unwrap();
3127        assert_eq!(
3128            python.interpreter().python_full_version().to_string(),
3129            "3.10.3",
3130        );
3131
3132        Ok(())
3133    }
3134
3135    #[test]
3136    fn find_python_version_free_threaded() -> Result<()> {
3137        let mut context = TestContext::new()?;
3138
3139        TestContext::create_mock_interpreter(
3140            &context.tempdir.join("python"),
3141            &PythonVersion::from_str("3.13.1").unwrap(),
3142            ImplementationName::CPython,
3143            true,
3144            false,
3145        )?;
3146        TestContext::create_mock_interpreter(
3147            &context.tempdir.join("python3.13t"),
3148            &PythonVersion::from_str("3.13.0").unwrap(),
3149            ImplementationName::CPython,
3150            true,
3151            true,
3152        )?;
3153        context.add_to_search_path(context.tempdir.to_path_buf());
3154
3155        let python = context.run(|| {
3156            find_python_installation(
3157                &PythonRequest::parse("3.13t"),
3158                EnvironmentPreference::Any,
3159                PythonPreference::OnlySystem,
3160                &context.cache,
3161            )
3162        })??;
3163
3164        assert!(
3165            matches!(
3166                python,
3167                PythonInstallation {
3168                    source: PythonSource::SearchPathFirst,
3169                    interpreter: _
3170                }
3171            ),
3172            "We should find a python; got {python:?}"
3173        );
3174        assert_eq!(
3175            &python.interpreter().python_full_version().to_string(),
3176            "3.13.0",
3177            "We should find the correct interpreter for the request"
3178        );
3179        assert!(
3180            &python.interpreter().gil_disabled(),
3181            "We should find a python without the GIL"
3182        );
3183
3184        Ok(())
3185    }
3186
3187    #[test]
3188    fn find_python_version_prefer_non_free_threaded() -> Result<()> {
3189        let mut context = TestContext::new()?;
3190
3191        TestContext::create_mock_interpreter(
3192            &context.tempdir.join("python"),
3193            &PythonVersion::from_str("3.13.0").unwrap(),
3194            ImplementationName::CPython,
3195            true,
3196            false,
3197        )?;
3198        TestContext::create_mock_interpreter(
3199            &context.tempdir.join("python3.13t"),
3200            &PythonVersion::from_str("3.13.0").unwrap(),
3201            ImplementationName::CPython,
3202            true,
3203            true,
3204        )?;
3205        context.add_to_search_path(context.tempdir.to_path_buf());
3206
3207        let python = context.run(|| {
3208            find_python_installation(
3209                &PythonRequest::parse("3.13"),
3210                EnvironmentPreference::Any,
3211                PythonPreference::OnlySystem,
3212                &context.cache,
3213            )
3214        })??;
3215
3216        assert!(
3217            matches!(
3218                python,
3219                PythonInstallation {
3220                    source: PythonSource::SearchPathFirst,
3221                    interpreter: _
3222                }
3223            ),
3224            "We should find a python; got {python:?}"
3225        );
3226        assert_eq!(
3227            &python.interpreter().python_full_version().to_string(),
3228            "3.13.0",
3229            "We should find the correct interpreter for the request"
3230        );
3231        assert!(
3232            !&python.interpreter().gil_disabled(),
3233            "We should prefer a python with the GIL"
3234        );
3235
3236        Ok(())
3237    }
3238
3239    #[test]
3240    fn find_python_pyodide() -> Result<()> {
3241        let mut context = TestContext::new()?;
3242
3243        context.add_pyodide_version("3.13.2")?;
3244
3245        // We should not find the Pyodide interpreter by default
3246        let result = context.run(|| {
3247            find_python_installation(
3248                &PythonRequest::Default,
3249                EnvironmentPreference::Any,
3250                PythonPreference::OnlySystem,
3251                &context.cache,
3252            )
3253        })?;
3254        assert!(
3255            result.is_err(),
3256            "We should not find an python; got {result:?}"
3257        );
3258
3259        // With `Any`, it should be discoverable
3260        let python = context.run(|| {
3261            find_python_installation(
3262                &PythonRequest::Any,
3263                EnvironmentPreference::Any,
3264                PythonPreference::OnlySystem,
3265                &context.cache,
3266            )
3267        })??;
3268        assert_eq!(
3269            python.interpreter().python_full_version().to_string(),
3270            "3.13.2"
3271        );
3272
3273        // We should prefer the native Python to the Pyodide Python
3274        context.add_python_versions(&["3.15.7"])?;
3275
3276        let python = context.run(|| {
3277            find_python_installation(
3278                &PythonRequest::Default,
3279                EnvironmentPreference::Any,
3280                PythonPreference::OnlySystem,
3281                &context.cache,
3282            )
3283        })??;
3284        assert_eq!(
3285            python.interpreter().python_full_version().to_string(),
3286            "3.15.7"
3287        );
3288
3289        Ok(())
3290    }
3291}