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