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