Skip to main content

uv_python/
lib.rs

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