Skip to main content

uv_python/
lib.rs

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