Skip to main content

uv_python/
lib.rs

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