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