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;
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        // When environment name matches directory name, it should be treated as a child environment
1339        let myenv_dir = context.tempdir.child("myenv");
1340        TestContext::mock_conda_prefix(&myenv_dir, "3.12.5")?;
1341        let python = context
1342            .run_with_vars(
1343                &[
1344                    (EnvVars::CONDA_PREFIX, Some(myenv_dir.as_os_str())),
1345                    (EnvVars::CONDA_DEFAULT_ENV, Some(&OsString::from("myenv"))),
1346                ],
1347                || {
1348                    find_python_installation(
1349                        &PythonRequest::Default,
1350                        EnvironmentPreference::OnlyVirtual,
1351                        PythonPreference::OnlySystem,
1352                        &context.cache,
1353                        Preview::default(),
1354                    )
1355                },
1356            )?
1357            .unwrap();
1358
1359        assert_eq!(
1360            python.interpreter().python_full_version().to_string(),
1361            "3.12.5",
1362            "We should find the child conda environment"
1363        );
1364
1365        // Test _CONDA_ROOT detection of base environment
1366        let conda_root_env = context.tempdir.child("conda-root");
1367        TestContext::mock_conda_prefix(&conda_root_env, "3.12.2")?;
1368
1369        // When _CONDA_ROOT matches CONDA_PREFIX, it should be treated as a base environment
1370        let result = context.run_with_vars(
1371            &[
1372                (EnvVars::CONDA_PREFIX, Some(conda_root_env.as_os_str())),
1373                (EnvVars::CONDA_ROOT, Some(conda_root_env.as_os_str())),
1374                (
1375                    EnvVars::CONDA_DEFAULT_ENV,
1376                    Some(&OsString::from("custom-name")),
1377                ),
1378            ],
1379            || {
1380                find_python_installation(
1381                    &PythonRequest::Default,
1382                    EnvironmentPreference::OnlyVirtual,
1383                    PythonPreference::OnlySystem,
1384                    &context.cache,
1385                    Preview::default(),
1386                )
1387            },
1388        )?;
1389
1390        assert!(
1391            matches!(result, Err(PythonNotFound { .. })),
1392            "Base environment detected via _CONDA_ROOT should be excluded from virtual environments; got {result:?}"
1393        );
1394
1395        // When _CONDA_ROOT doesn't match CONDA_PREFIX, it should be treated as a regular conda environment
1396        let other_conda_env = context.tempdir.child("other-conda");
1397        TestContext::mock_conda_prefix(&other_conda_env, "3.12.3")?;
1398
1399        let python = context
1400            .run_with_vars(
1401                &[
1402                    (EnvVars::CONDA_PREFIX, Some(other_conda_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("other-conda")),
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            .unwrap();
1420
1421        assert_eq!(
1422            python.interpreter().python_full_version().to_string(),
1423            "3.12.3",
1424            "Non-base conda environment should be available for virtual environment preference"
1425        );
1426
1427        // When CONDA_PREFIX equals CONDA_DEFAULT_ENV, it should be treated as a virtual environment
1428        let unnamed_env = context.tempdir.child("my-conda-env");
1429        TestContext::mock_conda_prefix(&unnamed_env, "3.12.4")?;
1430        let unnamed_env_path = unnamed_env.to_string_lossy().to_string();
1431
1432        let python = context.run_with_vars(
1433            &[
1434                (EnvVars::CONDA_PREFIX, Some(unnamed_env.as_os_str())),
1435                (
1436                    EnvVars::CONDA_DEFAULT_ENV,
1437                    Some(&OsString::from(&unnamed_env_path)),
1438                ),
1439            ],
1440            || {
1441                find_python_installation(
1442                    &PythonRequest::Default,
1443                    EnvironmentPreference::OnlyVirtual,
1444                    PythonPreference::OnlySystem,
1445                    &context.cache,
1446                    Preview::default(),
1447                )
1448            },
1449        )??;
1450
1451        assert_eq!(
1452            python.interpreter().python_full_version().to_string(),
1453            "3.12.4",
1454            "We should find the unnamed conda environment"
1455        );
1456
1457        Ok(())
1458    }
1459
1460    #[test]
1461    fn find_python_from_conda_prefix_and_virtualenv() -> Result<()> {
1462        let context = TestContext::new()?;
1463        let venv = context.tempdir.child(".venv");
1464        TestContext::mock_venv(&venv, "3.12.0")?;
1465        let condaenv = context.tempdir.child("condaenv");
1466        TestContext::mock_conda_prefix(&condaenv, "3.12.1")?;
1467
1468        let python = context.run_with_vars(
1469            &[
1470                (EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())),
1471                (EnvVars::CONDA_PREFIX, Some(condaenv.as_os_str())),
1472            ],
1473            || {
1474                find_python_installation(
1475                    &PythonRequest::Default,
1476                    EnvironmentPreference::Any,
1477                    PythonPreference::OnlySystem,
1478                    &context.cache,
1479                    Preview::default(),
1480                )
1481            },
1482        )??;
1483        assert_eq!(
1484            python.interpreter().python_full_version().to_string(),
1485            "3.12.0",
1486            "We should prefer the non-conda python"
1487        );
1488
1489        // Put a virtual environment in the working directory
1490        let venv = context.workdir.child(".venv");
1491        TestContext::mock_venv(venv, "3.12.2")?;
1492        let python = context.run_with_vars(
1493            &[(EnvVars::CONDA_PREFIX, Some(condaenv.as_os_str()))],
1494            || {
1495                find_python_installation(
1496                    &PythonRequest::Default,
1497                    EnvironmentPreference::Any,
1498                    PythonPreference::OnlySystem,
1499                    &context.cache,
1500                    Preview::default(),
1501                )
1502            },
1503        )??;
1504        assert_eq!(
1505            python.interpreter().python_full_version().to_string(),
1506            "3.12.1",
1507            "We should prefer the conda python over inactive virtual environments"
1508        );
1509
1510        Ok(())
1511    }
1512
1513    #[test]
1514    fn find_python_from_discovered_python() -> Result<()> {
1515        let mut context = TestContext::new()?;
1516
1517        // Create a virtual environment in a parent of the workdir
1518        let venv = context.tempdir.child(".venv");
1519        TestContext::mock_venv(venv, "3.12.0")?;
1520
1521        let python = context.run(|| {
1522            find_python_installation(
1523                &PythonRequest::Default,
1524                EnvironmentPreference::Any,
1525                PythonPreference::OnlySystem,
1526                &context.cache,
1527                Preview::default(),
1528            )
1529        })??;
1530
1531        assert_eq!(
1532            python.interpreter().python_full_version().to_string(),
1533            "3.12.0",
1534            "We should find the python"
1535        );
1536
1537        // Add some system versions to ensure we don't use those
1538        context.add_python_versions(&["3.12.1", "3.12.2"])?;
1539        let python = context.run(|| {
1540            find_python_installation(
1541                &PythonRequest::Default,
1542                EnvironmentPreference::Any,
1543                PythonPreference::OnlySystem,
1544                &context.cache,
1545                Preview::default(),
1546            )
1547        })??;
1548
1549        assert_eq!(
1550            python.interpreter().python_full_version().to_string(),
1551            "3.12.0",
1552            "We should prefer the discovered virtual environment over available system versions"
1553        );
1554
1555        Ok(())
1556    }
1557
1558    #[test]
1559    fn find_python_skips_broken_active_python() -> Result<()> {
1560        let context = TestContext::new()?;
1561        let venv = context.tempdir.child(".venv");
1562        TestContext::mock_venv(&venv, "3.12.0")?;
1563
1564        // Delete the pyvenv cfg to break the virtualenv
1565        fs_err::remove_file(venv.join("pyvenv.cfg"))?;
1566
1567        let python =
1568            context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
1569                find_python_installation(
1570                    &PythonRequest::Default,
1571                    EnvironmentPreference::Any,
1572                    PythonPreference::OnlySystem,
1573                    &context.cache,
1574                    Preview::default(),
1575                )
1576            })??;
1577        assert_eq!(
1578            python.interpreter().python_full_version().to_string(),
1579            "3.12.0",
1580            // TODO(zanieb): We should skip this python, why don't we?
1581            "We should prefer the active environment"
1582        );
1583
1584        Ok(())
1585    }
1586
1587    #[test]
1588    fn find_python_from_parent_interpreter() -> Result<()> {
1589        let mut context = TestContext::new()?;
1590
1591        let parent = context.tempdir.child("python").to_path_buf();
1592        TestContext::create_mock_interpreter(
1593            &parent,
1594            &PythonVersion::from_str("3.12.0").unwrap(),
1595            ImplementationName::CPython,
1596            // Note we mark this as a system interpreter instead of a virtual environment
1597            true,
1598            false,
1599        )?;
1600
1601        let python = context.run_with_vars(
1602            &[(
1603                EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
1604                Some(parent.as_os_str()),
1605            )],
1606            || {
1607                find_python_installation(
1608                    &PythonRequest::Default,
1609                    EnvironmentPreference::Any,
1610                    PythonPreference::OnlySystem,
1611                    &context.cache,
1612                    Preview::default(),
1613                )
1614            },
1615        )??;
1616        assert_eq!(
1617            python.interpreter().python_full_version().to_string(),
1618            "3.12.0",
1619            "We should find the parent interpreter"
1620        );
1621
1622        // Parent interpreters are preferred over virtual environments and system interpreters
1623        let venv = context.tempdir.child(".venv");
1624        TestContext::mock_venv(&venv, "3.12.2")?;
1625        context.add_python_versions(&["3.12.3"])?;
1626        let python = context.run_with_vars(
1627            &[
1628                (
1629                    EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
1630                    Some(parent.as_os_str()),
1631                ),
1632                (EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())),
1633            ],
1634            || {
1635                find_python_installation(
1636                    &PythonRequest::Default,
1637                    EnvironmentPreference::Any,
1638                    PythonPreference::OnlySystem,
1639                    &context.cache,
1640                    Preview::default(),
1641                )
1642            },
1643        )??;
1644        assert_eq!(
1645            python.interpreter().python_full_version().to_string(),
1646            "3.12.0",
1647            "We should prefer the parent interpreter"
1648        );
1649
1650        // Test with `EnvironmentPreference::ExplicitSystem`
1651        let python = context.run_with_vars(
1652            &[
1653                (
1654                    EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
1655                    Some(parent.as_os_str()),
1656                ),
1657                (EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())),
1658            ],
1659            || {
1660                find_python_installation(
1661                    &PythonRequest::Default,
1662                    EnvironmentPreference::ExplicitSystem,
1663                    PythonPreference::OnlySystem,
1664                    &context.cache,
1665                    Preview::default(),
1666                )
1667            },
1668        )??;
1669        assert_eq!(
1670            python.interpreter().python_full_version().to_string(),
1671            "3.12.0",
1672            "We should prefer the parent interpreter"
1673        );
1674
1675        // Test with `EnvironmentPreference::OnlySystem`
1676        let python = context.run_with_vars(
1677            &[
1678                (
1679                    EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
1680                    Some(parent.as_os_str()),
1681                ),
1682                (EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())),
1683            ],
1684            || {
1685                find_python_installation(
1686                    &PythonRequest::Default,
1687                    EnvironmentPreference::OnlySystem,
1688                    PythonPreference::OnlySystem,
1689                    &context.cache,
1690                    Preview::default(),
1691                )
1692            },
1693        )??;
1694        assert_eq!(
1695            python.interpreter().python_full_version().to_string(),
1696            "3.12.0",
1697            "We should prefer the parent interpreter since it's not virtual"
1698        );
1699
1700        // Test with `EnvironmentPreference::OnlyVirtual`
1701        let python = context.run_with_vars(
1702            &[
1703                (
1704                    EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
1705                    Some(parent.as_os_str()),
1706                ),
1707                (EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())),
1708            ],
1709            || {
1710                find_python_installation(
1711                    &PythonRequest::Default,
1712                    EnvironmentPreference::OnlyVirtual,
1713                    PythonPreference::OnlySystem,
1714                    &context.cache,
1715                    Preview::default(),
1716                )
1717            },
1718        )??;
1719        assert_eq!(
1720            python.interpreter().python_full_version().to_string(),
1721            "3.12.2",
1722            "We find the virtual environment Python because a system is explicitly not allowed"
1723        );
1724
1725        Ok(())
1726    }
1727
1728    #[test]
1729    fn find_python_from_parent_interpreter_prerelease() -> Result<()> {
1730        let mut context = TestContext::new()?;
1731        context.add_python_versions(&["3.12.0"])?;
1732        let parent = context.tempdir.child("python").to_path_buf();
1733        TestContext::create_mock_interpreter(
1734            &parent,
1735            &PythonVersion::from_str("3.13.0rc2").unwrap(),
1736            ImplementationName::CPython,
1737            // Note we mark this as a system interpreter instead of a virtual environment
1738            true,
1739            false,
1740        )?;
1741
1742        let python = context.run_with_vars(
1743            &[(
1744                EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
1745                Some(parent.as_os_str()),
1746            )],
1747            || {
1748                find_python_installation(
1749                    &PythonRequest::Default,
1750                    EnvironmentPreference::Any,
1751                    PythonPreference::OnlySystem,
1752                    &context.cache,
1753                    Preview::default(),
1754                )
1755            },
1756        )??;
1757        assert_eq!(
1758            python.interpreter().python_full_version().to_string(),
1759            "3.13.0rc2",
1760            "We should find the parent interpreter"
1761        );
1762
1763        Ok(())
1764    }
1765
1766    #[test]
1767    fn find_python_active_python_skipped_if_system_required() -> Result<()> {
1768        let mut context = TestContext::new()?;
1769        let venv = context.tempdir.child(".venv");
1770        TestContext::mock_venv(&venv, "3.9.0")?;
1771        context.add_python_versions(&["3.10.0", "3.11.1", "3.12.2"])?;
1772
1773        // Without a specific request
1774        let python =
1775            context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
1776                find_python_installation(
1777                    &PythonRequest::Default,
1778                    EnvironmentPreference::OnlySystem,
1779                    PythonPreference::OnlySystem,
1780                    &context.cache,
1781                    Preview::default(),
1782                )
1783            })??;
1784        assert_eq!(
1785            python.interpreter().python_full_version().to_string(),
1786            "3.10.0",
1787            "We should skip the active environment"
1788        );
1789
1790        // With a requested minor version
1791        let python =
1792            context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
1793                find_python_installation(
1794                    &PythonRequest::parse("3.12"),
1795                    EnvironmentPreference::OnlySystem,
1796                    PythonPreference::OnlySystem,
1797                    &context.cache,
1798                    Preview::default(),
1799                )
1800            })??;
1801        assert_eq!(
1802            python.interpreter().python_full_version().to_string(),
1803            "3.12.2",
1804            "We should skip the active environment"
1805        );
1806
1807        // With a patch version that cannot be python
1808        let result =
1809            context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
1810                find_python_installation(
1811                    &PythonRequest::parse("3.12.3"),
1812                    EnvironmentPreference::OnlySystem,
1813                    PythonPreference::OnlySystem,
1814                    &context.cache,
1815                    Preview::default(),
1816                )
1817            })?;
1818        assert!(
1819            result.is_err(),
1820            "We should not find an python; got {result:?}"
1821        );
1822
1823        Ok(())
1824    }
1825
1826    #[test]
1827    fn find_python_fails_if_no_virtualenv_and_system_not_allowed() -> Result<()> {
1828        let mut context = TestContext::new()?;
1829        context.add_python_versions(&["3.10.1", "3.11.2"])?;
1830
1831        let result = context.run(|| {
1832            find_python_installation(
1833                &PythonRequest::Default,
1834                EnvironmentPreference::OnlyVirtual,
1835                PythonPreference::OnlySystem,
1836                &context.cache,
1837                Preview::default(),
1838            )
1839        })?;
1840        assert!(
1841            matches!(result, Err(PythonNotFound { .. })),
1842            "We should not find an python; got {result:?}"
1843        );
1844
1845        // With an invalid virtual environment variable
1846        let result = context.run_with_vars(
1847            &[(EnvVars::VIRTUAL_ENV, Some(context.tempdir.as_os_str()))],
1848            || {
1849                find_python_installation(
1850                    &PythonRequest::parse("3.12.3"),
1851                    EnvironmentPreference::OnlySystem,
1852                    PythonPreference::OnlySystem,
1853                    &context.cache,
1854                    Preview::default(),
1855                )
1856            },
1857        )?;
1858        assert!(
1859            matches!(result, Err(PythonNotFound { .. })),
1860            "We should not find an python; got {result:?}"
1861        );
1862        Ok(())
1863    }
1864
1865    #[test]
1866    fn find_python_allows_name_in_working_directory() -> Result<()> {
1867        let context = TestContext::new()?;
1868        context.add_python_to_workdir("foobar", "3.10.0")?;
1869
1870        let python = context.run(|| {
1871            find_python_installation(
1872                &PythonRequest::parse("foobar"),
1873                EnvironmentPreference::Any,
1874                PythonPreference::OnlySystem,
1875                &context.cache,
1876                Preview::default(),
1877            )
1878        })??;
1879        assert_eq!(
1880            python.interpreter().python_full_version().to_string(),
1881            "3.10.0",
1882            "We should find the named executable"
1883        );
1884
1885        let result = context.run(|| {
1886            find_python_installation(
1887                &PythonRequest::Default,
1888                EnvironmentPreference::Any,
1889                PythonPreference::OnlySystem,
1890                &context.cache,
1891                Preview::default(),
1892            )
1893        })?;
1894        assert!(
1895            matches!(result, Err(PythonNotFound { .. })),
1896            "We should not find it without a specific request"
1897        );
1898
1899        let result = context.run(|| {
1900            find_python_installation(
1901                &PythonRequest::parse("3.10.0"),
1902                EnvironmentPreference::Any,
1903                PythonPreference::OnlySystem,
1904                &context.cache,
1905                Preview::default(),
1906            )
1907        })?;
1908        assert!(
1909            matches!(result, Err(PythonNotFound { .. })),
1910            "We should not find it via a matching version request"
1911        );
1912
1913        Ok(())
1914    }
1915
1916    #[test]
1917    fn find_python_allows_relative_file_path() -> Result<()> {
1918        let mut context = TestContext::new()?;
1919        let python = context.workdir.child("foo").join("bar");
1920        TestContext::create_mock_interpreter(
1921            &python,
1922            &PythonVersion::from_str("3.10.0").unwrap(),
1923            ImplementationName::default(),
1924            true,
1925            false,
1926        )?;
1927
1928        let python = context.run(|| {
1929            find_python_installation(
1930                &PythonRequest::parse("./foo/bar"),
1931                EnvironmentPreference::Any,
1932                PythonPreference::OnlySystem,
1933                &context.cache,
1934                Preview::default(),
1935            )
1936        })??;
1937        assert_eq!(
1938            python.interpreter().python_full_version().to_string(),
1939            "3.10.0",
1940            "We should find the `bar` executable"
1941        );
1942
1943        context.add_python_versions(&["3.11.1"])?;
1944        let python = context.run(|| {
1945            find_python_installation(
1946                &PythonRequest::parse("./foo/bar"),
1947                EnvironmentPreference::Any,
1948                PythonPreference::OnlySystem,
1949                &context.cache,
1950                Preview::default(),
1951            )
1952        })??;
1953        assert_eq!(
1954            python.interpreter().python_full_version().to_string(),
1955            "3.10.0",
1956            "We should prefer the `bar` executable over the system and virtualenvs"
1957        );
1958
1959        Ok(())
1960    }
1961
1962    #[test]
1963    fn find_python_allows_absolute_file_path() -> Result<()> {
1964        let mut context = TestContext::new()?;
1965        let python_path = context.tempdir.child("foo").join("bar");
1966        TestContext::create_mock_interpreter(
1967            &python_path,
1968            &PythonVersion::from_str("3.10.0").unwrap(),
1969            ImplementationName::default(),
1970            true,
1971            false,
1972        )?;
1973
1974        let python = context.run(|| {
1975            find_python_installation(
1976                &PythonRequest::parse(python_path.to_str().unwrap()),
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 find the `bar` executable"
1987        );
1988
1989        // With `EnvironmentPreference::ExplicitSystem`
1990        let python = context.run(|| {
1991            find_python_installation(
1992                &PythonRequest::parse(python_path.to_str().unwrap()),
1993                EnvironmentPreference::ExplicitSystem,
1994                PythonPreference::OnlySystem,
1995                &context.cache,
1996                Preview::default(),
1997            )
1998        })??;
1999        assert_eq!(
2000            python.interpreter().python_full_version().to_string(),
2001            "3.10.0",
2002            "We should allow the `bar` executable with explicit system"
2003        );
2004
2005        // With `EnvironmentPreference::OnlyVirtual`
2006        let python = context.run(|| {
2007            find_python_installation(
2008                &PythonRequest::parse(python_path.to_str().unwrap()),
2009                EnvironmentPreference::OnlyVirtual,
2010                PythonPreference::OnlySystem,
2011                &context.cache,
2012                Preview::default(),
2013            )
2014        })??;
2015        assert_eq!(
2016            python.interpreter().python_full_version().to_string(),
2017            "3.10.0",
2018            "We should allow the `bar` executable and verify it is virtual"
2019        );
2020
2021        context.add_python_versions(&["3.11.1"])?;
2022        let python = context.run(|| {
2023            find_python_installation(
2024                &PythonRequest::parse(python_path.to_str().unwrap()),
2025                EnvironmentPreference::Any,
2026                PythonPreference::OnlySystem,
2027                &context.cache,
2028                Preview::default(),
2029            )
2030        })??;
2031        assert_eq!(
2032            python.interpreter().python_full_version().to_string(),
2033            "3.10.0",
2034            "We should prefer the `bar` executable over the system and virtualenvs"
2035        );
2036
2037        Ok(())
2038    }
2039
2040    #[test]
2041    fn find_python_allows_venv_directory_path() -> Result<()> {
2042        let mut context = TestContext::new()?;
2043
2044        let venv = context.tempdir.child("foo").child(".venv");
2045        TestContext::mock_venv(&venv, "3.10.0")?;
2046        let python = context.run(|| {
2047            find_python_installation(
2048                &PythonRequest::parse("../foo/.venv"),
2049                EnvironmentPreference::Any,
2050                PythonPreference::OnlySystem,
2051                &context.cache,
2052                Preview::default(),
2053            )
2054        })??;
2055        assert_eq!(
2056            python.interpreter().python_full_version().to_string(),
2057            "3.10.0",
2058            "We should find the relative venv path"
2059        );
2060
2061        let python = context.run(|| {
2062            find_python_installation(
2063                &PythonRequest::parse(venv.to_str().unwrap()),
2064                EnvironmentPreference::Any,
2065                PythonPreference::OnlySystem,
2066                &context.cache,
2067                Preview::default(),
2068            )
2069        })??;
2070        assert_eq!(
2071            python.interpreter().python_full_version().to_string(),
2072            "3.10.0",
2073            "We should find the absolute venv path"
2074        );
2075
2076        // We should allow it to be a directory that _looks_ like a virtual environment.
2077        let python_path = context.tempdir.child("bar").join("bin").join("python");
2078        TestContext::create_mock_interpreter(
2079            &python_path,
2080            &PythonVersion::from_str("3.10.0").unwrap(),
2081            ImplementationName::default(),
2082            true,
2083            false,
2084        )?;
2085        let python = context.run(|| {
2086            find_python_installation(
2087                &PythonRequest::parse(context.tempdir.child("bar").to_str().unwrap()),
2088                EnvironmentPreference::Any,
2089                PythonPreference::OnlySystem,
2090                &context.cache,
2091                Preview::default(),
2092            )
2093        })??;
2094        assert_eq!(
2095            python.interpreter().python_full_version().to_string(),
2096            "3.10.0",
2097            "We should find the executable in the directory"
2098        );
2099
2100        let other_venv = context.tempdir.child("foobar").child(".venv");
2101        TestContext::mock_venv(&other_venv, "3.11.1")?;
2102        context.add_python_versions(&["3.12.2"])?;
2103        let python = context.run_with_vars(
2104            &[(EnvVars::VIRTUAL_ENV, Some(other_venv.as_os_str()))],
2105            || {
2106                find_python_installation(
2107                    &PythonRequest::parse(venv.to_str().unwrap()),
2108                    EnvironmentPreference::Any,
2109                    PythonPreference::OnlySystem,
2110                    &context.cache,
2111                    Preview::default(),
2112                )
2113            },
2114        )??;
2115        assert_eq!(
2116            python.interpreter().python_full_version().to_string(),
2117            "3.10.0",
2118            "We should prefer the requested directory over the system and active virtual environments"
2119        );
2120
2121        Ok(())
2122    }
2123
2124    #[test]
2125    fn find_python_venv_symlink() -> Result<()> {
2126        let context = TestContext::new()?;
2127
2128        let venv = context.tempdir.child("target").child("env");
2129        TestContext::mock_venv(&venv, "3.10.6")?;
2130        let symlink = context.tempdir.child("proj").child(".venv");
2131        context.tempdir.child("proj").create_dir_all()?;
2132        symlink.symlink_to_dir(venv)?;
2133
2134        let python = context.run(|| {
2135            find_python_installation(
2136                &PythonRequest::parse("../proj/.venv"),
2137                EnvironmentPreference::Any,
2138                PythonPreference::OnlySystem,
2139                &context.cache,
2140                Preview::default(),
2141            )
2142        })??;
2143        assert_eq!(
2144            python.interpreter().python_full_version().to_string(),
2145            "3.10.6",
2146            "We should find the symlinked venv"
2147        );
2148        Ok(())
2149    }
2150
2151    #[test]
2152    fn find_python_treats_missing_file_path_as_file() -> Result<()> {
2153        let context = TestContext::new()?;
2154        context.workdir.child("foo").create_dir_all()?;
2155
2156        let result = context.run(|| {
2157            find_python_installation(
2158                &PythonRequest::parse("./foo/bar"),
2159                EnvironmentPreference::Any,
2160                PythonPreference::OnlySystem,
2161                &context.cache,
2162                Preview::default(),
2163            )
2164        })?;
2165        assert!(
2166            matches!(result, Err(PythonNotFound { .. })),
2167            "We should not find the file; got {result:?}"
2168        );
2169
2170        Ok(())
2171    }
2172
2173    #[test]
2174    fn find_python_executable_name_in_search_path() -> Result<()> {
2175        let mut context = TestContext::new()?;
2176        let python = context.tempdir.child("foo").join("bar");
2177        TestContext::create_mock_interpreter(
2178            &python,
2179            &PythonVersion::from_str("3.10.0").unwrap(),
2180            ImplementationName::default(),
2181            true,
2182            false,
2183        )?;
2184        context.add_to_search_path(context.tempdir.child("foo").to_path_buf());
2185
2186        let python = context.run(|| {
2187            find_python_installation(
2188                &PythonRequest::parse("bar"),
2189                EnvironmentPreference::Any,
2190                PythonPreference::OnlySystem,
2191                &context.cache,
2192                Preview::default(),
2193            )
2194        })??;
2195        assert_eq!(
2196            python.interpreter().python_full_version().to_string(),
2197            "3.10.0",
2198            "We should find the `bar` executable"
2199        );
2200
2201        // With [`EnvironmentPreference::OnlyVirtual`], we should not allow the interpreter
2202        let result = context.run(|| {
2203            find_python_installation(
2204                &PythonRequest::parse("bar"),
2205                EnvironmentPreference::ExplicitSystem,
2206                PythonPreference::OnlySystem,
2207                &context.cache,
2208                Preview::default(),
2209            )
2210        })?;
2211        assert!(
2212            matches!(result, Err(PythonNotFound { .. })),
2213            "We should not allow a system interpreter; got {result:?}"
2214        );
2215
2216        // Unless it's a virtual environment interpreter
2217        let mut context = TestContext::new()?;
2218        let python = context.tempdir.child("foo").join("bar");
2219        TestContext::create_mock_interpreter(
2220            &python,
2221            &PythonVersion::from_str("3.10.0").unwrap(),
2222            ImplementationName::default(),
2223            false, // Not a system interpreter
2224            false,
2225        )?;
2226        context.add_to_search_path(context.tempdir.child("foo").to_path_buf());
2227
2228        let python = context
2229            .run(|| {
2230                find_python_installation(
2231                    &PythonRequest::parse("bar"),
2232                    EnvironmentPreference::ExplicitSystem,
2233                    PythonPreference::OnlySystem,
2234                    &context.cache,
2235                    Preview::default(),
2236                )
2237            })
2238            .unwrap()
2239            .unwrap();
2240        assert_eq!(
2241            python.interpreter().python_full_version().to_string(),
2242            "3.10.0",
2243            "We should find the `bar` executable"
2244        );
2245
2246        Ok(())
2247    }
2248
2249    #[test]
2250    fn find_python_pypy() -> Result<()> {
2251        let mut context = TestContext::new()?;
2252
2253        context.add_python_interpreters(&[(true, ImplementationName::PyPy, "pypy", "3.10.0")])?;
2254        let result = context.run(|| {
2255            find_python_installation(
2256                &PythonRequest::Default,
2257                EnvironmentPreference::Any,
2258                PythonPreference::OnlySystem,
2259                &context.cache,
2260                Preview::default(),
2261            )
2262        })?;
2263        assert!(
2264            matches!(result, Err(PythonNotFound { .. })),
2265            "We should not find the pypy interpreter if not named `python` or requested; got {result:?}"
2266        );
2267
2268        // But we should find it
2269        context.reset_search_path();
2270        context.add_python_interpreters(&[(true, ImplementationName::PyPy, "python", "3.10.1")])?;
2271        let python = context.run(|| {
2272            find_python_installation(
2273                &PythonRequest::Default,
2274                EnvironmentPreference::Any,
2275                PythonPreference::OnlySystem,
2276                &context.cache,
2277                Preview::default(),
2278            )
2279        })??;
2280        assert_eq!(
2281            python.interpreter().python_full_version().to_string(),
2282            "3.10.1",
2283            "We should find the pypy interpreter if it's the only one"
2284        );
2285
2286        let python = context.run(|| {
2287            find_python_installation(
2288                &PythonRequest::parse("pypy"),
2289                EnvironmentPreference::Any,
2290                PythonPreference::OnlySystem,
2291                &context.cache,
2292                Preview::default(),
2293            )
2294        })??;
2295        assert_eq!(
2296            python.interpreter().python_full_version().to_string(),
2297            "3.10.1",
2298            "We should find the pypy interpreter if it's requested"
2299        );
2300
2301        Ok(())
2302    }
2303
2304    #[test]
2305    fn find_python_pypy_request_ignores_cpython() -> Result<()> {
2306        let mut context = TestContext::new()?;
2307        context.add_python_interpreters(&[
2308            (true, ImplementationName::CPython, "python", "3.10.0"),
2309            (true, ImplementationName::PyPy, "pypy", "3.10.1"),
2310        ])?;
2311
2312        let python = context.run(|| {
2313            find_python_installation(
2314                &PythonRequest::parse("pypy"),
2315                EnvironmentPreference::Any,
2316                PythonPreference::OnlySystem,
2317                &context.cache,
2318                Preview::default(),
2319            )
2320        })??;
2321        assert_eq!(
2322            python.interpreter().python_full_version().to_string(),
2323            "3.10.1",
2324            "We should skip the CPython interpreter"
2325        );
2326
2327        let python = context.run(|| {
2328            find_python_installation(
2329                &PythonRequest::Default,
2330                EnvironmentPreference::Any,
2331                PythonPreference::OnlySystem,
2332                &context.cache,
2333                Preview::default(),
2334            )
2335        })??;
2336        assert_eq!(
2337            python.interpreter().python_full_version().to_string(),
2338            "3.10.0",
2339            "We should take the first interpreter without a specific request"
2340        );
2341
2342        Ok(())
2343    }
2344
2345    #[test]
2346    fn find_python_pypy_request_skips_wrong_versions() -> Result<()> {
2347        let mut context = TestContext::new()?;
2348        context.add_python_interpreters(&[
2349            (true, ImplementationName::PyPy, "pypy", "3.9"),
2350            (true, ImplementationName::PyPy, "pypy", "3.10.1"),
2351        ])?;
2352
2353        let python = context.run(|| {
2354            find_python_installation(
2355                &PythonRequest::parse("pypy3.10"),
2356                EnvironmentPreference::Any,
2357                PythonPreference::OnlySystem,
2358                &context.cache,
2359                Preview::default(),
2360            )
2361        })??;
2362        assert_eq!(
2363            python.interpreter().python_full_version().to_string(),
2364            "3.10.1",
2365            "We should skip the first interpreter"
2366        );
2367
2368        Ok(())
2369    }
2370
2371    #[test]
2372    fn find_python_pypy_finds_executable_with_version_name() -> Result<()> {
2373        let mut context = TestContext::new()?;
2374        context.add_python_interpreters(&[
2375            (true, ImplementationName::PyPy, "pypy3.9", "3.10.0"), // We don't consider this one because of the executable name
2376            (true, ImplementationName::PyPy, "pypy3.10", "3.10.1"),
2377            (true, ImplementationName::PyPy, "pypy", "3.10.2"),
2378        ])?;
2379
2380        let python = context.run(|| {
2381            find_python_installation(
2382                &PythonRequest::parse("pypy@3.10"),
2383                EnvironmentPreference::Any,
2384                PythonPreference::OnlySystem,
2385                &context.cache,
2386                Preview::default(),
2387            )
2388        })??;
2389        assert_eq!(
2390            python.interpreter().python_full_version().to_string(),
2391            "3.10.1",
2392            "We should find the requested interpreter version"
2393        );
2394
2395        Ok(())
2396    }
2397
2398    #[test]
2399    fn find_python_all_minors() -> Result<()> {
2400        let mut context = TestContext::new()?;
2401        context.add_python_interpreters(&[
2402            (true, ImplementationName::CPython, "python", "3.10.0"),
2403            (true, ImplementationName::CPython, "python3", "3.10.0"),
2404            (true, ImplementationName::CPython, "python3.12", "3.12.0"),
2405        ])?;
2406
2407        let python = context.run(|| {
2408            find_python_installation(
2409                &PythonRequest::parse(">= 3.11"),
2410                EnvironmentPreference::Any,
2411                PythonPreference::OnlySystem,
2412                &context.cache,
2413                Preview::default(),
2414            )
2415        })??;
2416        assert_eq!(
2417            python.interpreter().python_full_version().to_string(),
2418            "3.12.0",
2419            "We should find matching minor version even if they aren't called `python` or `python3`"
2420        );
2421
2422        Ok(())
2423    }
2424
2425    #[test]
2426    fn find_python_all_minors_prerelease() -> Result<()> {
2427        let mut context = TestContext::new()?;
2428        context.add_python_interpreters(&[
2429            (true, ImplementationName::CPython, "python", "3.10.0"),
2430            (true, ImplementationName::CPython, "python3", "3.10.0"),
2431            (true, ImplementationName::CPython, "python3.11", "3.11.0b0"),
2432        ])?;
2433
2434        let python = context.run(|| {
2435            find_python_installation(
2436                &PythonRequest::parse(">= 3.11"),
2437                EnvironmentPreference::Any,
2438                PythonPreference::OnlySystem,
2439                &context.cache,
2440                Preview::default(),
2441            )
2442        })??;
2443        assert_eq!(
2444            python.interpreter().python_full_version().to_string(),
2445            "3.11.0b0",
2446            "We should find the 3.11 prerelease even though >=3.11 would normally exclude prereleases"
2447        );
2448
2449        Ok(())
2450    }
2451
2452    #[test]
2453    fn find_python_all_minors_prerelease_next() -> Result<()> {
2454        let mut context = TestContext::new()?;
2455        context.add_python_interpreters(&[
2456            (true, ImplementationName::CPython, "python", "3.10.0"),
2457            (true, ImplementationName::CPython, "python3", "3.10.0"),
2458            (true, ImplementationName::CPython, "python3.12", "3.12.0b0"),
2459        ])?;
2460
2461        let python = context.run(|| {
2462            find_python_installation(
2463                &PythonRequest::parse(">= 3.11"),
2464                EnvironmentPreference::Any,
2465                PythonPreference::OnlySystem,
2466                &context.cache,
2467                Preview::default(),
2468            )
2469        })??;
2470        assert_eq!(
2471            python.interpreter().python_full_version().to_string(),
2472            "3.12.0b0",
2473            "We should find the 3.12 prerelease"
2474        );
2475
2476        Ok(())
2477    }
2478
2479    #[test]
2480    fn find_python_graalpy() -> Result<()> {
2481        let mut context = TestContext::new()?;
2482
2483        context.add_python_interpreters(&[(
2484            true,
2485            ImplementationName::GraalPy,
2486            "graalpy",
2487            "3.10.0",
2488        )])?;
2489        let result = context.run(|| {
2490            find_python_installation(
2491                &PythonRequest::Default,
2492                EnvironmentPreference::Any,
2493                PythonPreference::OnlySystem,
2494                &context.cache,
2495                Preview::default(),
2496            )
2497        })?;
2498        assert!(
2499            matches!(result, Err(PythonNotFound { .. })),
2500            "We should not the graalpy interpreter if not named `python` or requested; got {result:?}"
2501        );
2502
2503        // But we should find it
2504        context.reset_search_path();
2505        context.add_python_interpreters(&[(
2506            true,
2507            ImplementationName::GraalPy,
2508            "python",
2509            "3.10.1",
2510        )])?;
2511        let python = context.run(|| {
2512            find_python_installation(
2513                &PythonRequest::Default,
2514                EnvironmentPreference::Any,
2515                PythonPreference::OnlySystem,
2516                &context.cache,
2517                Preview::default(),
2518            )
2519        })??;
2520        assert_eq!(
2521            python.interpreter().python_full_version().to_string(),
2522            "3.10.1",
2523            "We should find the graalpy interpreter if it's the only one"
2524        );
2525
2526        let python = context.run(|| {
2527            find_python_installation(
2528                &PythonRequest::parse("graalpy"),
2529                EnvironmentPreference::Any,
2530                PythonPreference::OnlySystem,
2531                &context.cache,
2532                Preview::default(),
2533            )
2534        })??;
2535        assert_eq!(
2536            python.interpreter().python_full_version().to_string(),
2537            "3.10.1",
2538            "We should find the graalpy interpreter if it's requested"
2539        );
2540
2541        Ok(())
2542    }
2543
2544    #[test]
2545    fn find_python_graalpy_request_ignores_cpython() -> Result<()> {
2546        let mut context = TestContext::new()?;
2547        context.add_python_interpreters(&[
2548            (true, ImplementationName::CPython, "python", "3.10.0"),
2549            (true, ImplementationName::GraalPy, "graalpy", "3.10.1"),
2550        ])?;
2551
2552        let python = context.run(|| {
2553            find_python_installation(
2554                &PythonRequest::parse("graalpy"),
2555                EnvironmentPreference::Any,
2556                PythonPreference::OnlySystem,
2557                &context.cache,
2558                Preview::default(),
2559            )
2560        })??;
2561        assert_eq!(
2562            python.interpreter().python_full_version().to_string(),
2563            "3.10.1",
2564            "We should skip the CPython interpreter"
2565        );
2566
2567        let python = context.run(|| {
2568            find_python_installation(
2569                &PythonRequest::Default,
2570                EnvironmentPreference::Any,
2571                PythonPreference::OnlySystem,
2572                &context.cache,
2573                Preview::default(),
2574            )
2575        })??;
2576        assert_eq!(
2577            python.interpreter().python_full_version().to_string(),
2578            "3.10.0",
2579            "We should take the first interpreter without a specific request"
2580        );
2581
2582        Ok(())
2583    }
2584
2585    #[test]
2586    fn find_python_executable_name_preference() -> Result<()> {
2587        let mut context = TestContext::new()?;
2588        TestContext::create_mock_interpreter(
2589            &context.tempdir.join("pypy3.10"),
2590            &PythonVersion::from_str("3.10.0").unwrap(),
2591            ImplementationName::PyPy,
2592            true,
2593            false,
2594        )?;
2595        TestContext::create_mock_interpreter(
2596            &context.tempdir.join("pypy"),
2597            &PythonVersion::from_str("3.10.1").unwrap(),
2598            ImplementationName::PyPy,
2599            true,
2600            false,
2601        )?;
2602        context.add_to_search_path(context.tempdir.to_path_buf());
2603
2604        let python = context
2605            .run(|| {
2606                find_python_installation(
2607                    &PythonRequest::parse("pypy@3.10"),
2608                    EnvironmentPreference::Any,
2609                    PythonPreference::OnlySystem,
2610                    &context.cache,
2611                    Preview::default(),
2612                )
2613            })
2614            .unwrap()
2615            .unwrap();
2616        assert_eq!(
2617            python.interpreter().python_full_version().to_string(),
2618            "3.10.0",
2619            "We should prefer the versioned one when a version is requested"
2620        );
2621
2622        let python = context
2623            .run(|| {
2624                find_python_installation(
2625                    &PythonRequest::parse("pypy"),
2626                    EnvironmentPreference::Any,
2627                    PythonPreference::OnlySystem,
2628                    &context.cache,
2629                    Preview::default(),
2630                )
2631            })
2632            .unwrap()
2633            .unwrap();
2634        assert_eq!(
2635            python.interpreter().python_full_version().to_string(),
2636            "3.10.1",
2637            "We should prefer the generic one when no version is requested"
2638        );
2639
2640        let mut context = TestContext::new()?;
2641        TestContext::create_mock_interpreter(
2642            &context.tempdir.join("python3.10"),
2643            &PythonVersion::from_str("3.10.0").unwrap(),
2644            ImplementationName::PyPy,
2645            true,
2646            false,
2647        )?;
2648        TestContext::create_mock_interpreter(
2649            &context.tempdir.join("pypy"),
2650            &PythonVersion::from_str("3.10.1").unwrap(),
2651            ImplementationName::PyPy,
2652            true,
2653            false,
2654        )?;
2655        TestContext::create_mock_interpreter(
2656            &context.tempdir.join("python"),
2657            &PythonVersion::from_str("3.10.2").unwrap(),
2658            ImplementationName::PyPy,
2659            true,
2660            false,
2661        )?;
2662        context.add_to_search_path(context.tempdir.to_path_buf());
2663
2664        let python = context
2665            .run(|| {
2666                find_python_installation(
2667                    &PythonRequest::parse("pypy@3.10"),
2668                    EnvironmentPreference::Any,
2669                    PythonPreference::OnlySystem,
2670                    &context.cache,
2671                    Preview::default(),
2672                )
2673            })
2674            .unwrap()
2675            .unwrap();
2676        assert_eq!(
2677            python.interpreter().python_full_version().to_string(),
2678            "3.10.1",
2679            "We should prefer the implementation name over the generic name"
2680        );
2681
2682        let python = context
2683            .run(|| {
2684                find_python_installation(
2685                    &PythonRequest::parse("default"),
2686                    EnvironmentPreference::Any,
2687                    PythonPreference::OnlySystem,
2688                    &context.cache,
2689                    Preview::default(),
2690                )
2691            })
2692            .unwrap()
2693            .unwrap();
2694        assert_eq!(
2695            python.interpreter().python_full_version().to_string(),
2696            "3.10.2",
2697            "We should prefer the generic name over the implementation name, but not the versioned name"
2698        );
2699
2700        // We prefer `python` executables over `graalpy` executables in the same directory
2701        // if they are both GraalPy
2702        let mut context = TestContext::new()?;
2703        TestContext::create_mock_interpreter(
2704            &context.tempdir.join("python"),
2705            &PythonVersion::from_str("3.10.0").unwrap(),
2706            ImplementationName::GraalPy,
2707            true,
2708            false,
2709        )?;
2710        TestContext::create_mock_interpreter(
2711            &context.tempdir.join("graalpy"),
2712            &PythonVersion::from_str("3.10.1").unwrap(),
2713            ImplementationName::GraalPy,
2714            true,
2715            false,
2716        )?;
2717        context.add_to_search_path(context.tempdir.to_path_buf());
2718
2719        let python = context
2720            .run(|| {
2721                find_python_installation(
2722                    &PythonRequest::parse("graalpy@3.10"),
2723                    EnvironmentPreference::Any,
2724                    PythonPreference::OnlySystem,
2725                    &context.cache,
2726                    Preview::default(),
2727                )
2728            })
2729            .unwrap()
2730            .unwrap();
2731        assert_eq!(
2732            python.interpreter().python_full_version().to_string(),
2733            "3.10.1",
2734        );
2735
2736        // And `python` executables earlier in the search path will take precedence
2737        context.reset_search_path();
2738        context.add_python_interpreters(&[
2739            (true, ImplementationName::GraalPy, "python", "3.10.2"),
2740            (true, ImplementationName::GraalPy, "graalpy", "3.10.3"),
2741        ])?;
2742        let python = context
2743            .run(|| {
2744                find_python_installation(
2745                    &PythonRequest::parse("graalpy@3.10"),
2746                    EnvironmentPreference::Any,
2747                    PythonPreference::OnlySystem,
2748                    &context.cache,
2749                    Preview::default(),
2750                )
2751            })
2752            .unwrap()
2753            .unwrap();
2754        assert_eq!(
2755            python.interpreter().python_full_version().to_string(),
2756            "3.10.2",
2757        );
2758
2759        // And `graalpy` executables earlier in the search path will take precedence
2760        context.reset_search_path();
2761        context.add_python_interpreters(&[
2762            (true, ImplementationName::GraalPy, "graalpy", "3.10.3"),
2763            (true, ImplementationName::GraalPy, "python", "3.10.2"),
2764        ])?;
2765        let python = context
2766            .run(|| {
2767                find_python_installation(
2768                    &PythonRequest::parse("graalpy@3.10"),
2769                    EnvironmentPreference::Any,
2770                    PythonPreference::OnlySystem,
2771                    &context.cache,
2772                    Preview::default(),
2773                )
2774            })
2775            .unwrap()
2776            .unwrap();
2777        assert_eq!(
2778            python.interpreter().python_full_version().to_string(),
2779            "3.10.3",
2780        );
2781
2782        Ok(())
2783    }
2784
2785    #[test]
2786    fn find_python_version_free_threaded() -> Result<()> {
2787        let mut context = TestContext::new()?;
2788
2789        TestContext::create_mock_interpreter(
2790            &context.tempdir.join("python"),
2791            &PythonVersion::from_str("3.13.1").unwrap(),
2792            ImplementationName::CPython,
2793            true,
2794            false,
2795        )?;
2796        TestContext::create_mock_interpreter(
2797            &context.tempdir.join("python3.13t"),
2798            &PythonVersion::from_str("3.13.0").unwrap(),
2799            ImplementationName::CPython,
2800            true,
2801            true,
2802        )?;
2803        context.add_to_search_path(context.tempdir.to_path_buf());
2804
2805        let python = context.run(|| {
2806            find_python_installation(
2807                &PythonRequest::parse("3.13t"),
2808                EnvironmentPreference::Any,
2809                PythonPreference::OnlySystem,
2810                &context.cache,
2811                Preview::default(),
2812            )
2813        })??;
2814
2815        assert!(
2816            matches!(
2817                python,
2818                PythonInstallation {
2819                    source: PythonSource::SearchPathFirst,
2820                    interpreter: _
2821                }
2822            ),
2823            "We should find a python; got {python:?}"
2824        );
2825        assert_eq!(
2826            &python.interpreter().python_full_version().to_string(),
2827            "3.13.0",
2828            "We should find the correct interpreter for the request"
2829        );
2830        assert!(
2831            &python.interpreter().gil_disabled(),
2832            "We should find a python without the GIL"
2833        );
2834
2835        Ok(())
2836    }
2837
2838    #[test]
2839    fn find_python_version_prefer_non_free_threaded() -> Result<()> {
2840        let mut context = TestContext::new()?;
2841
2842        TestContext::create_mock_interpreter(
2843            &context.tempdir.join("python"),
2844            &PythonVersion::from_str("3.13.0").unwrap(),
2845            ImplementationName::CPython,
2846            true,
2847            false,
2848        )?;
2849        TestContext::create_mock_interpreter(
2850            &context.tempdir.join("python3.13t"),
2851            &PythonVersion::from_str("3.13.0").unwrap(),
2852            ImplementationName::CPython,
2853            true,
2854            true,
2855        )?;
2856        context.add_to_search_path(context.tempdir.to_path_buf());
2857
2858        let python = context.run(|| {
2859            find_python_installation(
2860                &PythonRequest::parse("3.13"),
2861                EnvironmentPreference::Any,
2862                PythonPreference::OnlySystem,
2863                &context.cache,
2864                Preview::default(),
2865            )
2866        })??;
2867
2868        assert!(
2869            matches!(
2870                python,
2871                PythonInstallation {
2872                    source: PythonSource::SearchPathFirst,
2873                    interpreter: _
2874                }
2875            ),
2876            "We should find a python; got {python:?}"
2877        );
2878        assert_eq!(
2879            &python.interpreter().python_full_version().to_string(),
2880            "3.13.0",
2881            "We should find the correct interpreter for the request"
2882        );
2883        assert!(
2884            !&python.interpreter().gil_disabled(),
2885            "We should prefer a python with the GIL"
2886        );
2887
2888        Ok(())
2889    }
2890
2891    #[test]
2892    fn find_python_pyodide() -> Result<()> {
2893        let mut context = TestContext::new()?;
2894
2895        context.add_pyodide_version("3.13.2")?;
2896
2897        // We should not find the Pyodide interpreter by default
2898        let result = context.run(|| {
2899            find_python_installation(
2900                &PythonRequest::Default,
2901                EnvironmentPreference::Any,
2902                PythonPreference::OnlySystem,
2903                &context.cache,
2904                Preview::default(),
2905            )
2906        })?;
2907        assert!(
2908            result.is_err(),
2909            "We should not find an python; got {result:?}"
2910        );
2911
2912        // With `Any`, it should be discoverable
2913        let python = context.run(|| {
2914            find_python_installation(
2915                &PythonRequest::Any,
2916                EnvironmentPreference::Any,
2917                PythonPreference::OnlySystem,
2918                &context.cache,
2919                Preview::default(),
2920            )
2921        })??;
2922        assert_eq!(
2923            python.interpreter().python_full_version().to_string(),
2924            "3.13.2"
2925        );
2926
2927        // We should prefer the native Python to the Pyodide Python
2928        context.add_python_versions(&["3.15.7"])?;
2929
2930        let python = context.run(|| {
2931            find_python_installation(
2932                &PythonRequest::Default,
2933                EnvironmentPreference::Any,
2934                PythonPreference::OnlySystem,
2935                &context.cache,
2936                Preview::default(),
2937            )
2938        })??;
2939        assert_eq!(
2940            python.interpreter().python_full_version().to_string(),
2941            "3.15.7"
2942        );
2943
2944        Ok(())
2945    }
2946}