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