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