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