Skip to main content

uv_python/
lib.rs

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