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