Skip to main content

uv_python/
lib.rs

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