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