Skip to main content

uv_python/
lib.rs

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