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;
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 myenv_dir = context.tempdir.child("myenv");
1340 TestContext::mock_conda_prefix(&myenv_dir, "3.12.5")?;
1341 let python = context
1342 .run_with_vars(
1343 &[
1344 (EnvVars::CONDA_PREFIX, Some(myenv_dir.as_os_str())),
1345 (EnvVars::CONDA_DEFAULT_ENV, Some(&OsString::from("myenv"))),
1346 ],
1347 || {
1348 find_python_installation(
1349 &PythonRequest::Default,
1350 EnvironmentPreference::OnlyVirtual,
1351 PythonPreference::OnlySystem,
1352 &context.cache,
1353 Preview::default(),
1354 )
1355 },
1356 )?
1357 .unwrap();
1358
1359 assert_eq!(
1360 python.interpreter().python_full_version().to_string(),
1361 "3.12.5",
1362 "We should find the child conda environment"
1363 );
1364
1365 let conda_root_env = context.tempdir.child("conda-root");
1367 TestContext::mock_conda_prefix(&conda_root_env, "3.12.2")?;
1368
1369 let result = context.run_with_vars(
1371 &[
1372 (EnvVars::CONDA_PREFIX, Some(conda_root_env.as_os_str())),
1373 (EnvVars::CONDA_ROOT, Some(conda_root_env.as_os_str())),
1374 (
1375 EnvVars::CONDA_DEFAULT_ENV,
1376 Some(&OsString::from("custom-name")),
1377 ),
1378 ],
1379 || {
1380 find_python_installation(
1381 &PythonRequest::Default,
1382 EnvironmentPreference::OnlyVirtual,
1383 PythonPreference::OnlySystem,
1384 &context.cache,
1385 Preview::default(),
1386 )
1387 },
1388 )?;
1389
1390 assert!(
1391 matches!(result, Err(PythonNotFound { .. })),
1392 "Base environment detected via _CONDA_ROOT should be excluded from virtual environments; got {result:?}"
1393 );
1394
1395 let other_conda_env = context.tempdir.child("other-conda");
1397 TestContext::mock_conda_prefix(&other_conda_env, "3.12.3")?;
1398
1399 let python = context
1400 .run_with_vars(
1401 &[
1402 (EnvVars::CONDA_PREFIX, Some(other_conda_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("other-conda")),
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 .unwrap();
1420
1421 assert_eq!(
1422 python.interpreter().python_full_version().to_string(),
1423 "3.12.3",
1424 "Non-base conda environment should be available for virtual environment preference"
1425 );
1426
1427 let unnamed_env = context.tempdir.child("my-conda-env");
1429 TestContext::mock_conda_prefix(&unnamed_env, "3.12.4")?;
1430 let unnamed_env_path = unnamed_env.to_string_lossy().to_string();
1431
1432 let python = context.run_with_vars(
1433 &[
1434 (EnvVars::CONDA_PREFIX, Some(unnamed_env.as_os_str())),
1435 (
1436 EnvVars::CONDA_DEFAULT_ENV,
1437 Some(&OsString::from(&unnamed_env_path)),
1438 ),
1439 ],
1440 || {
1441 find_python_installation(
1442 &PythonRequest::Default,
1443 EnvironmentPreference::OnlyVirtual,
1444 PythonPreference::OnlySystem,
1445 &context.cache,
1446 Preview::default(),
1447 )
1448 },
1449 )??;
1450
1451 assert_eq!(
1452 python.interpreter().python_full_version().to_string(),
1453 "3.12.4",
1454 "We should find the unnamed conda environment"
1455 );
1456
1457 Ok(())
1458 }
1459
1460 #[test]
1461 fn find_python_from_conda_prefix_and_virtualenv() -> Result<()> {
1462 let context = TestContext::new()?;
1463 let venv = context.tempdir.child(".venv");
1464 TestContext::mock_venv(&venv, "3.12.0")?;
1465 let condaenv = context.tempdir.child("condaenv");
1466 TestContext::mock_conda_prefix(&condaenv, "3.12.1")?;
1467
1468 let python = context.run_with_vars(
1469 &[
1470 (EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())),
1471 (EnvVars::CONDA_PREFIX, Some(condaenv.as_os_str())),
1472 ],
1473 || {
1474 find_python_installation(
1475 &PythonRequest::Default,
1476 EnvironmentPreference::Any,
1477 PythonPreference::OnlySystem,
1478 &context.cache,
1479 Preview::default(),
1480 )
1481 },
1482 )??;
1483 assert_eq!(
1484 python.interpreter().python_full_version().to_string(),
1485 "3.12.0",
1486 "We should prefer the non-conda python"
1487 );
1488
1489 let venv = context.workdir.child(".venv");
1491 TestContext::mock_venv(venv, "3.12.2")?;
1492 let python = context.run_with_vars(
1493 &[(EnvVars::CONDA_PREFIX, Some(condaenv.as_os_str()))],
1494 || {
1495 find_python_installation(
1496 &PythonRequest::Default,
1497 EnvironmentPreference::Any,
1498 PythonPreference::OnlySystem,
1499 &context.cache,
1500 Preview::default(),
1501 )
1502 },
1503 )??;
1504 assert_eq!(
1505 python.interpreter().python_full_version().to_string(),
1506 "3.12.1",
1507 "We should prefer the conda python over inactive virtual environments"
1508 );
1509
1510 Ok(())
1511 }
1512
1513 #[test]
1514 fn find_python_from_discovered_python() -> Result<()> {
1515 let mut context = TestContext::new()?;
1516
1517 let venv = context.tempdir.child(".venv");
1519 TestContext::mock_venv(venv, "3.12.0")?;
1520
1521 let python = context.run(|| {
1522 find_python_installation(
1523 &PythonRequest::Default,
1524 EnvironmentPreference::Any,
1525 PythonPreference::OnlySystem,
1526 &context.cache,
1527 Preview::default(),
1528 )
1529 })??;
1530
1531 assert_eq!(
1532 python.interpreter().python_full_version().to_string(),
1533 "3.12.0",
1534 "We should find the python"
1535 );
1536
1537 context.add_python_versions(&["3.12.1", "3.12.2"])?;
1539 let python = context.run(|| {
1540 find_python_installation(
1541 &PythonRequest::Default,
1542 EnvironmentPreference::Any,
1543 PythonPreference::OnlySystem,
1544 &context.cache,
1545 Preview::default(),
1546 )
1547 })??;
1548
1549 assert_eq!(
1550 python.interpreter().python_full_version().to_string(),
1551 "3.12.0",
1552 "We should prefer the discovered virtual environment over available system versions"
1553 );
1554
1555 Ok(())
1556 }
1557
1558 #[test]
1559 fn find_python_skips_broken_active_python() -> Result<()> {
1560 let context = TestContext::new()?;
1561 let venv = context.tempdir.child(".venv");
1562 TestContext::mock_venv(&venv, "3.12.0")?;
1563
1564 fs_err::remove_file(venv.join("pyvenv.cfg"))?;
1566
1567 let python =
1568 context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
1569 find_python_installation(
1570 &PythonRequest::Default,
1571 EnvironmentPreference::Any,
1572 PythonPreference::OnlySystem,
1573 &context.cache,
1574 Preview::default(),
1575 )
1576 })??;
1577 assert_eq!(
1578 python.interpreter().python_full_version().to_string(),
1579 "3.12.0",
1580 "We should prefer the active environment"
1582 );
1583
1584 Ok(())
1585 }
1586
1587 #[test]
1588 fn find_python_from_parent_interpreter() -> Result<()> {
1589 let mut context = TestContext::new()?;
1590
1591 let parent = context.tempdir.child("python").to_path_buf();
1592 TestContext::create_mock_interpreter(
1593 &parent,
1594 &PythonVersion::from_str("3.12.0").unwrap(),
1595 ImplementationName::CPython,
1596 true,
1598 false,
1599 )?;
1600
1601 let python = context.run_with_vars(
1602 &[(
1603 EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
1604 Some(parent.as_os_str()),
1605 )],
1606 || {
1607 find_python_installation(
1608 &PythonRequest::Default,
1609 EnvironmentPreference::Any,
1610 PythonPreference::OnlySystem,
1611 &context.cache,
1612 Preview::default(),
1613 )
1614 },
1615 )??;
1616 assert_eq!(
1617 python.interpreter().python_full_version().to_string(),
1618 "3.12.0",
1619 "We should find the parent interpreter"
1620 );
1621
1622 let venv = context.tempdir.child(".venv");
1624 TestContext::mock_venv(&venv, "3.12.2")?;
1625 context.add_python_versions(&["3.12.3"])?;
1626 let python = context.run_with_vars(
1627 &[
1628 (
1629 EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
1630 Some(parent.as_os_str()),
1631 ),
1632 (EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())),
1633 ],
1634 || {
1635 find_python_installation(
1636 &PythonRequest::Default,
1637 EnvironmentPreference::Any,
1638 PythonPreference::OnlySystem,
1639 &context.cache,
1640 Preview::default(),
1641 )
1642 },
1643 )??;
1644 assert_eq!(
1645 python.interpreter().python_full_version().to_string(),
1646 "3.12.0",
1647 "We should prefer the parent interpreter"
1648 );
1649
1650 let python = context.run_with_vars(
1652 &[
1653 (
1654 EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
1655 Some(parent.as_os_str()),
1656 ),
1657 (EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())),
1658 ],
1659 || {
1660 find_python_installation(
1661 &PythonRequest::Default,
1662 EnvironmentPreference::ExplicitSystem,
1663 PythonPreference::OnlySystem,
1664 &context.cache,
1665 Preview::default(),
1666 )
1667 },
1668 )??;
1669 assert_eq!(
1670 python.interpreter().python_full_version().to_string(),
1671 "3.12.0",
1672 "We should prefer the parent interpreter"
1673 );
1674
1675 let python = context.run_with_vars(
1677 &[
1678 (
1679 EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
1680 Some(parent.as_os_str()),
1681 ),
1682 (EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())),
1683 ],
1684 || {
1685 find_python_installation(
1686 &PythonRequest::Default,
1687 EnvironmentPreference::OnlySystem,
1688 PythonPreference::OnlySystem,
1689 &context.cache,
1690 Preview::default(),
1691 )
1692 },
1693 )??;
1694 assert_eq!(
1695 python.interpreter().python_full_version().to_string(),
1696 "3.12.0",
1697 "We should prefer the parent interpreter since it's not virtual"
1698 );
1699
1700 let python = context.run_with_vars(
1702 &[
1703 (
1704 EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
1705 Some(parent.as_os_str()),
1706 ),
1707 (EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())),
1708 ],
1709 || {
1710 find_python_installation(
1711 &PythonRequest::Default,
1712 EnvironmentPreference::OnlyVirtual,
1713 PythonPreference::OnlySystem,
1714 &context.cache,
1715 Preview::default(),
1716 )
1717 },
1718 )??;
1719 assert_eq!(
1720 python.interpreter().python_full_version().to_string(),
1721 "3.12.2",
1722 "We find the virtual environment Python because a system is explicitly not allowed"
1723 );
1724
1725 Ok(())
1726 }
1727
1728 #[test]
1729 fn find_python_from_parent_interpreter_prerelease() -> Result<()> {
1730 let mut context = TestContext::new()?;
1731 context.add_python_versions(&["3.12.0"])?;
1732 let parent = context.tempdir.child("python").to_path_buf();
1733 TestContext::create_mock_interpreter(
1734 &parent,
1735 &PythonVersion::from_str("3.13.0rc2").unwrap(),
1736 ImplementationName::CPython,
1737 true,
1739 false,
1740 )?;
1741
1742 let python = context.run_with_vars(
1743 &[(
1744 EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
1745 Some(parent.as_os_str()),
1746 )],
1747 || {
1748 find_python_installation(
1749 &PythonRequest::Default,
1750 EnvironmentPreference::Any,
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.13.0rc2",
1760 "We should find the parent interpreter"
1761 );
1762
1763 Ok(())
1764 }
1765
1766 #[test]
1767 fn find_python_active_python_skipped_if_system_required() -> Result<()> {
1768 let mut context = TestContext::new()?;
1769 let venv = context.tempdir.child(".venv");
1770 TestContext::mock_venv(&venv, "3.9.0")?;
1771 context.add_python_versions(&["3.10.0", "3.11.1", "3.12.2"])?;
1772
1773 let python =
1775 context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
1776 find_python_installation(
1777 &PythonRequest::Default,
1778 EnvironmentPreference::OnlySystem,
1779 PythonPreference::OnlySystem,
1780 &context.cache,
1781 Preview::default(),
1782 )
1783 })??;
1784 assert_eq!(
1785 python.interpreter().python_full_version().to_string(),
1786 "3.10.0",
1787 "We should skip the active environment"
1788 );
1789
1790 let python =
1792 context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
1793 find_python_installation(
1794 &PythonRequest::parse("3.12"),
1795 EnvironmentPreference::OnlySystem,
1796 PythonPreference::OnlySystem,
1797 &context.cache,
1798 Preview::default(),
1799 )
1800 })??;
1801 assert_eq!(
1802 python.interpreter().python_full_version().to_string(),
1803 "3.12.2",
1804 "We should skip the active environment"
1805 );
1806
1807 let result =
1809 context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
1810 find_python_installation(
1811 &PythonRequest::parse("3.12.3"),
1812 EnvironmentPreference::OnlySystem,
1813 PythonPreference::OnlySystem,
1814 &context.cache,
1815 Preview::default(),
1816 )
1817 })?;
1818 assert!(
1819 result.is_err(),
1820 "We should not find an python; got {result:?}"
1821 );
1822
1823 Ok(())
1824 }
1825
1826 #[test]
1827 fn find_python_fails_if_no_virtualenv_and_system_not_allowed() -> Result<()> {
1828 let mut context = TestContext::new()?;
1829 context.add_python_versions(&["3.10.1", "3.11.2"])?;
1830
1831 let result = context.run(|| {
1832 find_python_installation(
1833 &PythonRequest::Default,
1834 EnvironmentPreference::OnlyVirtual,
1835 PythonPreference::OnlySystem,
1836 &context.cache,
1837 Preview::default(),
1838 )
1839 })?;
1840 assert!(
1841 matches!(result, Err(PythonNotFound { .. })),
1842 "We should not find an python; got {result:?}"
1843 );
1844
1845 let result = context.run_with_vars(
1847 &[(EnvVars::VIRTUAL_ENV, Some(context.tempdir.as_os_str()))],
1848 || {
1849 find_python_installation(
1850 &PythonRequest::parse("3.12.3"),
1851 EnvironmentPreference::OnlySystem,
1852 PythonPreference::OnlySystem,
1853 &context.cache,
1854 Preview::default(),
1855 )
1856 },
1857 )?;
1858 assert!(
1859 matches!(result, Err(PythonNotFound { .. })),
1860 "We should not find an python; got {result:?}"
1861 );
1862 Ok(())
1863 }
1864
1865 #[test]
1866 fn find_python_allows_name_in_working_directory() -> Result<()> {
1867 let context = TestContext::new()?;
1868 context.add_python_to_workdir("foobar", "3.10.0")?;
1869
1870 let python = context.run(|| {
1871 find_python_installation(
1872 &PythonRequest::parse("foobar"),
1873 EnvironmentPreference::Any,
1874 PythonPreference::OnlySystem,
1875 &context.cache,
1876 Preview::default(),
1877 )
1878 })??;
1879 assert_eq!(
1880 python.interpreter().python_full_version().to_string(),
1881 "3.10.0",
1882 "We should find the named executable"
1883 );
1884
1885 let result = context.run(|| {
1886 find_python_installation(
1887 &PythonRequest::Default,
1888 EnvironmentPreference::Any,
1889 PythonPreference::OnlySystem,
1890 &context.cache,
1891 Preview::default(),
1892 )
1893 })?;
1894 assert!(
1895 matches!(result, Err(PythonNotFound { .. })),
1896 "We should not find it without a specific request"
1897 );
1898
1899 let result = context.run(|| {
1900 find_python_installation(
1901 &PythonRequest::parse("3.10.0"),
1902 EnvironmentPreference::Any,
1903 PythonPreference::OnlySystem,
1904 &context.cache,
1905 Preview::default(),
1906 )
1907 })?;
1908 assert!(
1909 matches!(result, Err(PythonNotFound { .. })),
1910 "We should not find it via a matching version request"
1911 );
1912
1913 Ok(())
1914 }
1915
1916 #[test]
1917 fn find_python_allows_relative_file_path() -> Result<()> {
1918 let mut context = TestContext::new()?;
1919 let python = context.workdir.child("foo").join("bar");
1920 TestContext::create_mock_interpreter(
1921 &python,
1922 &PythonVersion::from_str("3.10.0").unwrap(),
1923 ImplementationName::default(),
1924 true,
1925 false,
1926 )?;
1927
1928 let python = context.run(|| {
1929 find_python_installation(
1930 &PythonRequest::parse("./foo/bar"),
1931 EnvironmentPreference::Any,
1932 PythonPreference::OnlySystem,
1933 &context.cache,
1934 Preview::default(),
1935 )
1936 })??;
1937 assert_eq!(
1938 python.interpreter().python_full_version().to_string(),
1939 "3.10.0",
1940 "We should find the `bar` executable"
1941 );
1942
1943 context.add_python_versions(&["3.11.1"])?;
1944 let python = context.run(|| {
1945 find_python_installation(
1946 &PythonRequest::parse("./foo/bar"),
1947 EnvironmentPreference::Any,
1948 PythonPreference::OnlySystem,
1949 &context.cache,
1950 Preview::default(),
1951 )
1952 })??;
1953 assert_eq!(
1954 python.interpreter().python_full_version().to_string(),
1955 "3.10.0",
1956 "We should prefer the `bar` executable over the system and virtualenvs"
1957 );
1958
1959 Ok(())
1960 }
1961
1962 #[test]
1963 fn find_python_allows_absolute_file_path() -> Result<()> {
1964 let mut context = TestContext::new()?;
1965 let python_path = context.tempdir.child("foo").join("bar");
1966 TestContext::create_mock_interpreter(
1967 &python_path,
1968 &PythonVersion::from_str("3.10.0").unwrap(),
1969 ImplementationName::default(),
1970 true,
1971 false,
1972 )?;
1973
1974 let python = context.run(|| {
1975 find_python_installation(
1976 &PythonRequest::parse(python_path.to_str().unwrap()),
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 find the `bar` executable"
1987 );
1988
1989 let python = context.run(|| {
1991 find_python_installation(
1992 &PythonRequest::parse(python_path.to_str().unwrap()),
1993 EnvironmentPreference::ExplicitSystem,
1994 PythonPreference::OnlySystem,
1995 &context.cache,
1996 Preview::default(),
1997 )
1998 })??;
1999 assert_eq!(
2000 python.interpreter().python_full_version().to_string(),
2001 "3.10.0",
2002 "We should allow the `bar` executable with explicit system"
2003 );
2004
2005 let python = context.run(|| {
2007 find_python_installation(
2008 &PythonRequest::parse(python_path.to_str().unwrap()),
2009 EnvironmentPreference::OnlyVirtual,
2010 PythonPreference::OnlySystem,
2011 &context.cache,
2012 Preview::default(),
2013 )
2014 })??;
2015 assert_eq!(
2016 python.interpreter().python_full_version().to_string(),
2017 "3.10.0",
2018 "We should allow the `bar` executable and verify it is virtual"
2019 );
2020
2021 context.add_python_versions(&["3.11.1"])?;
2022 let python = context.run(|| {
2023 find_python_installation(
2024 &PythonRequest::parse(python_path.to_str().unwrap()),
2025 EnvironmentPreference::Any,
2026 PythonPreference::OnlySystem,
2027 &context.cache,
2028 Preview::default(),
2029 )
2030 })??;
2031 assert_eq!(
2032 python.interpreter().python_full_version().to_string(),
2033 "3.10.0",
2034 "We should prefer the `bar` executable over the system and virtualenvs"
2035 );
2036
2037 Ok(())
2038 }
2039
2040 #[test]
2041 fn find_python_allows_venv_directory_path() -> Result<()> {
2042 let mut context = TestContext::new()?;
2043
2044 let venv = context.tempdir.child("foo").child(".venv");
2045 TestContext::mock_venv(&venv, "3.10.0")?;
2046 let python = context.run(|| {
2047 find_python_installation(
2048 &PythonRequest::parse("../foo/.venv"),
2049 EnvironmentPreference::Any,
2050 PythonPreference::OnlySystem,
2051 &context.cache,
2052 Preview::default(),
2053 )
2054 })??;
2055 assert_eq!(
2056 python.interpreter().python_full_version().to_string(),
2057 "3.10.0",
2058 "We should find the relative venv path"
2059 );
2060
2061 let python = context.run(|| {
2062 find_python_installation(
2063 &PythonRequest::parse(venv.to_str().unwrap()),
2064 EnvironmentPreference::Any,
2065 PythonPreference::OnlySystem,
2066 &context.cache,
2067 Preview::default(),
2068 )
2069 })??;
2070 assert_eq!(
2071 python.interpreter().python_full_version().to_string(),
2072 "3.10.0",
2073 "We should find the absolute venv path"
2074 );
2075
2076 let python_path = context.tempdir.child("bar").join("bin").join("python");
2078 TestContext::create_mock_interpreter(
2079 &python_path,
2080 &PythonVersion::from_str("3.10.0").unwrap(),
2081 ImplementationName::default(),
2082 true,
2083 false,
2084 )?;
2085 let python = context.run(|| {
2086 find_python_installation(
2087 &PythonRequest::parse(context.tempdir.child("bar").to_str().unwrap()),
2088 EnvironmentPreference::Any,
2089 PythonPreference::OnlySystem,
2090 &context.cache,
2091 Preview::default(),
2092 )
2093 })??;
2094 assert_eq!(
2095 python.interpreter().python_full_version().to_string(),
2096 "3.10.0",
2097 "We should find the executable in the directory"
2098 );
2099
2100 let other_venv = context.tempdir.child("foobar").child(".venv");
2101 TestContext::mock_venv(&other_venv, "3.11.1")?;
2102 context.add_python_versions(&["3.12.2"])?;
2103 let python = context.run_with_vars(
2104 &[(EnvVars::VIRTUAL_ENV, Some(other_venv.as_os_str()))],
2105 || {
2106 find_python_installation(
2107 &PythonRequest::parse(venv.to_str().unwrap()),
2108 EnvironmentPreference::Any,
2109 PythonPreference::OnlySystem,
2110 &context.cache,
2111 Preview::default(),
2112 )
2113 },
2114 )??;
2115 assert_eq!(
2116 python.interpreter().python_full_version().to_string(),
2117 "3.10.0",
2118 "We should prefer the requested directory over the system and active virtual environments"
2119 );
2120
2121 Ok(())
2122 }
2123
2124 #[test]
2125 fn find_python_venv_symlink() -> Result<()> {
2126 let context = TestContext::new()?;
2127
2128 let venv = context.tempdir.child("target").child("env");
2129 TestContext::mock_venv(&venv, "3.10.6")?;
2130 let symlink = context.tempdir.child("proj").child(".venv");
2131 context.tempdir.child("proj").create_dir_all()?;
2132 symlink.symlink_to_dir(venv)?;
2133
2134 let python = context.run(|| {
2135 find_python_installation(
2136 &PythonRequest::parse("../proj/.venv"),
2137 EnvironmentPreference::Any,
2138 PythonPreference::OnlySystem,
2139 &context.cache,
2140 Preview::default(),
2141 )
2142 })??;
2143 assert_eq!(
2144 python.interpreter().python_full_version().to_string(),
2145 "3.10.6",
2146 "We should find the symlinked venv"
2147 );
2148 Ok(())
2149 }
2150
2151 #[test]
2152 fn find_python_treats_missing_file_path_as_file() -> Result<()> {
2153 let context = TestContext::new()?;
2154 context.workdir.child("foo").create_dir_all()?;
2155
2156 let result = context.run(|| {
2157 find_python_installation(
2158 &PythonRequest::parse("./foo/bar"),
2159 EnvironmentPreference::Any,
2160 PythonPreference::OnlySystem,
2161 &context.cache,
2162 Preview::default(),
2163 )
2164 })?;
2165 assert!(
2166 matches!(result, Err(PythonNotFound { .. })),
2167 "We should not find the file; got {result:?}"
2168 );
2169
2170 Ok(())
2171 }
2172
2173 #[test]
2174 fn find_python_executable_name_in_search_path() -> Result<()> {
2175 let mut context = TestContext::new()?;
2176 let python = context.tempdir.child("foo").join("bar");
2177 TestContext::create_mock_interpreter(
2178 &python,
2179 &PythonVersion::from_str("3.10.0").unwrap(),
2180 ImplementationName::default(),
2181 true,
2182 false,
2183 )?;
2184 context.add_to_search_path(context.tempdir.child("foo").to_path_buf());
2185
2186 let python = context.run(|| {
2187 find_python_installation(
2188 &PythonRequest::parse("bar"),
2189 EnvironmentPreference::Any,
2190 PythonPreference::OnlySystem,
2191 &context.cache,
2192 Preview::default(),
2193 )
2194 })??;
2195 assert_eq!(
2196 python.interpreter().python_full_version().to_string(),
2197 "3.10.0",
2198 "We should find the `bar` executable"
2199 );
2200
2201 let result = context.run(|| {
2203 find_python_installation(
2204 &PythonRequest::parse("bar"),
2205 EnvironmentPreference::ExplicitSystem,
2206 PythonPreference::OnlySystem,
2207 &context.cache,
2208 Preview::default(),
2209 )
2210 })?;
2211 assert!(
2212 matches!(result, Err(PythonNotFound { .. })),
2213 "We should not allow a system interpreter; got {result:?}"
2214 );
2215
2216 let mut context = TestContext::new()?;
2218 let python = context.tempdir.child("foo").join("bar");
2219 TestContext::create_mock_interpreter(
2220 &python,
2221 &PythonVersion::from_str("3.10.0").unwrap(),
2222 ImplementationName::default(),
2223 false, false,
2225 )?;
2226 context.add_to_search_path(context.tempdir.child("foo").to_path_buf());
2227
2228 let python = context
2229 .run(|| {
2230 find_python_installation(
2231 &PythonRequest::parse("bar"),
2232 EnvironmentPreference::ExplicitSystem,
2233 PythonPreference::OnlySystem,
2234 &context.cache,
2235 Preview::default(),
2236 )
2237 })
2238 .unwrap()
2239 .unwrap();
2240 assert_eq!(
2241 python.interpreter().python_full_version().to_string(),
2242 "3.10.0",
2243 "We should find the `bar` executable"
2244 );
2245
2246 Ok(())
2247 }
2248
2249 #[test]
2250 fn find_python_pypy() -> Result<()> {
2251 let mut context = TestContext::new()?;
2252
2253 context.add_python_interpreters(&[(true, ImplementationName::PyPy, "pypy", "3.10.0")])?;
2254 let result = context.run(|| {
2255 find_python_installation(
2256 &PythonRequest::Default,
2257 EnvironmentPreference::Any,
2258 PythonPreference::OnlySystem,
2259 &context.cache,
2260 Preview::default(),
2261 )
2262 })?;
2263 assert!(
2264 matches!(result, Err(PythonNotFound { .. })),
2265 "We should not find the pypy interpreter if not named `python` or requested; got {result:?}"
2266 );
2267
2268 context.reset_search_path();
2270 context.add_python_interpreters(&[(true, ImplementationName::PyPy, "python", "3.10.1")])?;
2271 let python = context.run(|| {
2272 find_python_installation(
2273 &PythonRequest::Default,
2274 EnvironmentPreference::Any,
2275 PythonPreference::OnlySystem,
2276 &context.cache,
2277 Preview::default(),
2278 )
2279 })??;
2280 assert_eq!(
2281 python.interpreter().python_full_version().to_string(),
2282 "3.10.1",
2283 "We should find the pypy interpreter if it's the only one"
2284 );
2285
2286 let python = context.run(|| {
2287 find_python_installation(
2288 &PythonRequest::parse("pypy"),
2289 EnvironmentPreference::Any,
2290 PythonPreference::OnlySystem,
2291 &context.cache,
2292 Preview::default(),
2293 )
2294 })??;
2295 assert_eq!(
2296 python.interpreter().python_full_version().to_string(),
2297 "3.10.1",
2298 "We should find the pypy interpreter if it's requested"
2299 );
2300
2301 Ok(())
2302 }
2303
2304 #[test]
2305 fn find_python_pypy_request_ignores_cpython() -> Result<()> {
2306 let mut context = TestContext::new()?;
2307 context.add_python_interpreters(&[
2308 (true, ImplementationName::CPython, "python", "3.10.0"),
2309 (true, ImplementationName::PyPy, "pypy", "3.10.1"),
2310 ])?;
2311
2312 let python = context.run(|| {
2313 find_python_installation(
2314 &PythonRequest::parse("pypy"),
2315 EnvironmentPreference::Any,
2316 PythonPreference::OnlySystem,
2317 &context.cache,
2318 Preview::default(),
2319 )
2320 })??;
2321 assert_eq!(
2322 python.interpreter().python_full_version().to_string(),
2323 "3.10.1",
2324 "We should skip the CPython interpreter"
2325 );
2326
2327 let python = context.run(|| {
2328 find_python_installation(
2329 &PythonRequest::Default,
2330 EnvironmentPreference::Any,
2331 PythonPreference::OnlySystem,
2332 &context.cache,
2333 Preview::default(),
2334 )
2335 })??;
2336 assert_eq!(
2337 python.interpreter().python_full_version().to_string(),
2338 "3.10.0",
2339 "We should take the first interpreter without a specific request"
2340 );
2341
2342 Ok(())
2343 }
2344
2345 #[test]
2346 fn find_python_pypy_request_skips_wrong_versions() -> Result<()> {
2347 let mut context = TestContext::new()?;
2348 context.add_python_interpreters(&[
2349 (true, ImplementationName::PyPy, "pypy", "3.9"),
2350 (true, ImplementationName::PyPy, "pypy", "3.10.1"),
2351 ])?;
2352
2353 let python = context.run(|| {
2354 find_python_installation(
2355 &PythonRequest::parse("pypy3.10"),
2356 EnvironmentPreference::Any,
2357 PythonPreference::OnlySystem,
2358 &context.cache,
2359 Preview::default(),
2360 )
2361 })??;
2362 assert_eq!(
2363 python.interpreter().python_full_version().to_string(),
2364 "3.10.1",
2365 "We should skip the first interpreter"
2366 );
2367
2368 Ok(())
2369 }
2370
2371 #[test]
2372 fn find_python_pypy_finds_executable_with_version_name() -> Result<()> {
2373 let mut context = TestContext::new()?;
2374 context.add_python_interpreters(&[
2375 (true, ImplementationName::PyPy, "pypy3.9", "3.10.0"), (true, ImplementationName::PyPy, "pypy3.10", "3.10.1"),
2377 (true, ImplementationName::PyPy, "pypy", "3.10.2"),
2378 ])?;
2379
2380 let python = context.run(|| {
2381 find_python_installation(
2382 &PythonRequest::parse("pypy@3.10"),
2383 EnvironmentPreference::Any,
2384 PythonPreference::OnlySystem,
2385 &context.cache,
2386 Preview::default(),
2387 )
2388 })??;
2389 assert_eq!(
2390 python.interpreter().python_full_version().to_string(),
2391 "3.10.1",
2392 "We should find the requested interpreter version"
2393 );
2394
2395 Ok(())
2396 }
2397
2398 #[test]
2399 fn find_python_all_minors() -> Result<()> {
2400 let mut context = TestContext::new()?;
2401 context.add_python_interpreters(&[
2402 (true, ImplementationName::CPython, "python", "3.10.0"),
2403 (true, ImplementationName::CPython, "python3", "3.10.0"),
2404 (true, ImplementationName::CPython, "python3.12", "3.12.0"),
2405 ])?;
2406
2407 let python = context.run(|| {
2408 find_python_installation(
2409 &PythonRequest::parse(">= 3.11"),
2410 EnvironmentPreference::Any,
2411 PythonPreference::OnlySystem,
2412 &context.cache,
2413 Preview::default(),
2414 )
2415 })??;
2416 assert_eq!(
2417 python.interpreter().python_full_version().to_string(),
2418 "3.12.0",
2419 "We should find matching minor version even if they aren't called `python` or `python3`"
2420 );
2421
2422 Ok(())
2423 }
2424
2425 #[test]
2426 fn find_python_all_minors_prerelease() -> Result<()> {
2427 let mut context = TestContext::new()?;
2428 context.add_python_interpreters(&[
2429 (true, ImplementationName::CPython, "python", "3.10.0"),
2430 (true, ImplementationName::CPython, "python3", "3.10.0"),
2431 (true, ImplementationName::CPython, "python3.11", "3.11.0b0"),
2432 ])?;
2433
2434 let python = context.run(|| {
2435 find_python_installation(
2436 &PythonRequest::parse(">= 3.11"),
2437 EnvironmentPreference::Any,
2438 PythonPreference::OnlySystem,
2439 &context.cache,
2440 Preview::default(),
2441 )
2442 })??;
2443 assert_eq!(
2444 python.interpreter().python_full_version().to_string(),
2445 "3.11.0b0",
2446 "We should find the 3.11 prerelease even though >=3.11 would normally exclude prereleases"
2447 );
2448
2449 Ok(())
2450 }
2451
2452 #[test]
2453 fn find_python_all_minors_prerelease_next() -> Result<()> {
2454 let mut context = TestContext::new()?;
2455 context.add_python_interpreters(&[
2456 (true, ImplementationName::CPython, "python", "3.10.0"),
2457 (true, ImplementationName::CPython, "python3", "3.10.0"),
2458 (true, ImplementationName::CPython, "python3.12", "3.12.0b0"),
2459 ])?;
2460
2461 let python = context.run(|| {
2462 find_python_installation(
2463 &PythonRequest::parse(">= 3.11"),
2464 EnvironmentPreference::Any,
2465 PythonPreference::OnlySystem,
2466 &context.cache,
2467 Preview::default(),
2468 )
2469 })??;
2470 assert_eq!(
2471 python.interpreter().python_full_version().to_string(),
2472 "3.12.0b0",
2473 "We should find the 3.12 prerelease"
2474 );
2475
2476 Ok(())
2477 }
2478
2479 #[test]
2480 fn find_python_graalpy() -> Result<()> {
2481 let mut context = TestContext::new()?;
2482
2483 context.add_python_interpreters(&[(
2484 true,
2485 ImplementationName::GraalPy,
2486 "graalpy",
2487 "3.10.0",
2488 )])?;
2489 let result = context.run(|| {
2490 find_python_installation(
2491 &PythonRequest::Default,
2492 EnvironmentPreference::Any,
2493 PythonPreference::OnlySystem,
2494 &context.cache,
2495 Preview::default(),
2496 )
2497 })?;
2498 assert!(
2499 matches!(result, Err(PythonNotFound { .. })),
2500 "We should not the graalpy interpreter if not named `python` or requested; got {result:?}"
2501 );
2502
2503 context.reset_search_path();
2505 context.add_python_interpreters(&[(
2506 true,
2507 ImplementationName::GraalPy,
2508 "python",
2509 "3.10.1",
2510 )])?;
2511 let python = context.run(|| {
2512 find_python_installation(
2513 &PythonRequest::Default,
2514 EnvironmentPreference::Any,
2515 PythonPreference::OnlySystem,
2516 &context.cache,
2517 Preview::default(),
2518 )
2519 })??;
2520 assert_eq!(
2521 python.interpreter().python_full_version().to_string(),
2522 "3.10.1",
2523 "We should find the graalpy interpreter if it's the only one"
2524 );
2525
2526 let python = context.run(|| {
2527 find_python_installation(
2528 &PythonRequest::parse("graalpy"),
2529 EnvironmentPreference::Any,
2530 PythonPreference::OnlySystem,
2531 &context.cache,
2532 Preview::default(),
2533 )
2534 })??;
2535 assert_eq!(
2536 python.interpreter().python_full_version().to_string(),
2537 "3.10.1",
2538 "We should find the graalpy interpreter if it's requested"
2539 );
2540
2541 Ok(())
2542 }
2543
2544 #[test]
2545 fn find_python_graalpy_request_ignores_cpython() -> Result<()> {
2546 let mut context = TestContext::new()?;
2547 context.add_python_interpreters(&[
2548 (true, ImplementationName::CPython, "python", "3.10.0"),
2549 (true, ImplementationName::GraalPy, "graalpy", "3.10.1"),
2550 ])?;
2551
2552 let python = context.run(|| {
2553 find_python_installation(
2554 &PythonRequest::parse("graalpy"),
2555 EnvironmentPreference::Any,
2556 PythonPreference::OnlySystem,
2557 &context.cache,
2558 Preview::default(),
2559 )
2560 })??;
2561 assert_eq!(
2562 python.interpreter().python_full_version().to_string(),
2563 "3.10.1",
2564 "We should skip the CPython interpreter"
2565 );
2566
2567 let python = context.run(|| {
2568 find_python_installation(
2569 &PythonRequest::Default,
2570 EnvironmentPreference::Any,
2571 PythonPreference::OnlySystem,
2572 &context.cache,
2573 Preview::default(),
2574 )
2575 })??;
2576 assert_eq!(
2577 python.interpreter().python_full_version().to_string(),
2578 "3.10.0",
2579 "We should take the first interpreter without a specific request"
2580 );
2581
2582 Ok(())
2583 }
2584
2585 #[test]
2586 fn find_python_executable_name_preference() -> Result<()> {
2587 let mut context = TestContext::new()?;
2588 TestContext::create_mock_interpreter(
2589 &context.tempdir.join("pypy3.10"),
2590 &PythonVersion::from_str("3.10.0").unwrap(),
2591 ImplementationName::PyPy,
2592 true,
2593 false,
2594 )?;
2595 TestContext::create_mock_interpreter(
2596 &context.tempdir.join("pypy"),
2597 &PythonVersion::from_str("3.10.1").unwrap(),
2598 ImplementationName::PyPy,
2599 true,
2600 false,
2601 )?;
2602 context.add_to_search_path(context.tempdir.to_path_buf());
2603
2604 let python = context
2605 .run(|| {
2606 find_python_installation(
2607 &PythonRequest::parse("pypy@3.10"),
2608 EnvironmentPreference::Any,
2609 PythonPreference::OnlySystem,
2610 &context.cache,
2611 Preview::default(),
2612 )
2613 })
2614 .unwrap()
2615 .unwrap();
2616 assert_eq!(
2617 python.interpreter().python_full_version().to_string(),
2618 "3.10.0",
2619 "We should prefer the versioned one when a version is requested"
2620 );
2621
2622 let python = context
2623 .run(|| {
2624 find_python_installation(
2625 &PythonRequest::parse("pypy"),
2626 EnvironmentPreference::Any,
2627 PythonPreference::OnlySystem,
2628 &context.cache,
2629 Preview::default(),
2630 )
2631 })
2632 .unwrap()
2633 .unwrap();
2634 assert_eq!(
2635 python.interpreter().python_full_version().to_string(),
2636 "3.10.1",
2637 "We should prefer the generic one when no version is requested"
2638 );
2639
2640 let mut context = TestContext::new()?;
2641 TestContext::create_mock_interpreter(
2642 &context.tempdir.join("python3.10"),
2643 &PythonVersion::from_str("3.10.0").unwrap(),
2644 ImplementationName::PyPy,
2645 true,
2646 false,
2647 )?;
2648 TestContext::create_mock_interpreter(
2649 &context.tempdir.join("pypy"),
2650 &PythonVersion::from_str("3.10.1").unwrap(),
2651 ImplementationName::PyPy,
2652 true,
2653 false,
2654 )?;
2655 TestContext::create_mock_interpreter(
2656 &context.tempdir.join("python"),
2657 &PythonVersion::from_str("3.10.2").unwrap(),
2658 ImplementationName::PyPy,
2659 true,
2660 false,
2661 )?;
2662 context.add_to_search_path(context.tempdir.to_path_buf());
2663
2664 let python = context
2665 .run(|| {
2666 find_python_installation(
2667 &PythonRequest::parse("pypy@3.10"),
2668 EnvironmentPreference::Any,
2669 PythonPreference::OnlySystem,
2670 &context.cache,
2671 Preview::default(),
2672 )
2673 })
2674 .unwrap()
2675 .unwrap();
2676 assert_eq!(
2677 python.interpreter().python_full_version().to_string(),
2678 "3.10.1",
2679 "We should prefer the implementation name over the generic name"
2680 );
2681
2682 let python = context
2683 .run(|| {
2684 find_python_installation(
2685 &PythonRequest::parse("default"),
2686 EnvironmentPreference::Any,
2687 PythonPreference::OnlySystem,
2688 &context.cache,
2689 Preview::default(),
2690 )
2691 })
2692 .unwrap()
2693 .unwrap();
2694 assert_eq!(
2695 python.interpreter().python_full_version().to_string(),
2696 "3.10.2",
2697 "We should prefer the generic name over the implementation name, but not the versioned name"
2698 );
2699
2700 let mut context = TestContext::new()?;
2703 TestContext::create_mock_interpreter(
2704 &context.tempdir.join("python"),
2705 &PythonVersion::from_str("3.10.0").unwrap(),
2706 ImplementationName::GraalPy,
2707 true,
2708 false,
2709 )?;
2710 TestContext::create_mock_interpreter(
2711 &context.tempdir.join("graalpy"),
2712 &PythonVersion::from_str("3.10.1").unwrap(),
2713 ImplementationName::GraalPy,
2714 true,
2715 false,
2716 )?;
2717 context.add_to_search_path(context.tempdir.to_path_buf());
2718
2719 let python = context
2720 .run(|| {
2721 find_python_installation(
2722 &PythonRequest::parse("graalpy@3.10"),
2723 EnvironmentPreference::Any,
2724 PythonPreference::OnlySystem,
2725 &context.cache,
2726 Preview::default(),
2727 )
2728 })
2729 .unwrap()
2730 .unwrap();
2731 assert_eq!(
2732 python.interpreter().python_full_version().to_string(),
2733 "3.10.1",
2734 );
2735
2736 context.reset_search_path();
2738 context.add_python_interpreters(&[
2739 (true, ImplementationName::GraalPy, "python", "3.10.2"),
2740 (true, ImplementationName::GraalPy, "graalpy", "3.10.3"),
2741 ])?;
2742 let python = context
2743 .run(|| {
2744 find_python_installation(
2745 &PythonRequest::parse("graalpy@3.10"),
2746 EnvironmentPreference::Any,
2747 PythonPreference::OnlySystem,
2748 &context.cache,
2749 Preview::default(),
2750 )
2751 })
2752 .unwrap()
2753 .unwrap();
2754 assert_eq!(
2755 python.interpreter().python_full_version().to_string(),
2756 "3.10.2",
2757 );
2758
2759 context.reset_search_path();
2761 context.add_python_interpreters(&[
2762 (true, ImplementationName::GraalPy, "graalpy", "3.10.3"),
2763 (true, ImplementationName::GraalPy, "python", "3.10.2"),
2764 ])?;
2765 let python = context
2766 .run(|| {
2767 find_python_installation(
2768 &PythonRequest::parse("graalpy@3.10"),
2769 EnvironmentPreference::Any,
2770 PythonPreference::OnlySystem,
2771 &context.cache,
2772 Preview::default(),
2773 )
2774 })
2775 .unwrap()
2776 .unwrap();
2777 assert_eq!(
2778 python.interpreter().python_full_version().to_string(),
2779 "3.10.3",
2780 );
2781
2782 Ok(())
2783 }
2784
2785 #[test]
2786 fn find_python_version_free_threaded() -> Result<()> {
2787 let mut context = TestContext::new()?;
2788
2789 TestContext::create_mock_interpreter(
2790 &context.tempdir.join("python"),
2791 &PythonVersion::from_str("3.13.1").unwrap(),
2792 ImplementationName::CPython,
2793 true,
2794 false,
2795 )?;
2796 TestContext::create_mock_interpreter(
2797 &context.tempdir.join("python3.13t"),
2798 &PythonVersion::from_str("3.13.0").unwrap(),
2799 ImplementationName::CPython,
2800 true,
2801 true,
2802 )?;
2803 context.add_to_search_path(context.tempdir.to_path_buf());
2804
2805 let python = context.run(|| {
2806 find_python_installation(
2807 &PythonRequest::parse("3.13t"),
2808 EnvironmentPreference::Any,
2809 PythonPreference::OnlySystem,
2810 &context.cache,
2811 Preview::default(),
2812 )
2813 })??;
2814
2815 assert!(
2816 matches!(
2817 python,
2818 PythonInstallation {
2819 source: PythonSource::SearchPathFirst,
2820 interpreter: _
2821 }
2822 ),
2823 "We should find a python; got {python:?}"
2824 );
2825 assert_eq!(
2826 &python.interpreter().python_full_version().to_string(),
2827 "3.13.0",
2828 "We should find the correct interpreter for the request"
2829 );
2830 assert!(
2831 &python.interpreter().gil_disabled(),
2832 "We should find a python without the GIL"
2833 );
2834
2835 Ok(())
2836 }
2837
2838 #[test]
2839 fn find_python_version_prefer_non_free_threaded() -> Result<()> {
2840 let mut context = TestContext::new()?;
2841
2842 TestContext::create_mock_interpreter(
2843 &context.tempdir.join("python"),
2844 &PythonVersion::from_str("3.13.0").unwrap(),
2845 ImplementationName::CPython,
2846 true,
2847 false,
2848 )?;
2849 TestContext::create_mock_interpreter(
2850 &context.tempdir.join("python3.13t"),
2851 &PythonVersion::from_str("3.13.0").unwrap(),
2852 ImplementationName::CPython,
2853 true,
2854 true,
2855 )?;
2856 context.add_to_search_path(context.tempdir.to_path_buf());
2857
2858 let python = context.run(|| {
2859 find_python_installation(
2860 &PythonRequest::parse("3.13"),
2861 EnvironmentPreference::Any,
2862 PythonPreference::OnlySystem,
2863 &context.cache,
2864 Preview::default(),
2865 )
2866 })??;
2867
2868 assert!(
2869 matches!(
2870 python,
2871 PythonInstallation {
2872 source: PythonSource::SearchPathFirst,
2873 interpreter: _
2874 }
2875 ),
2876 "We should find a python; got {python:?}"
2877 );
2878 assert_eq!(
2879 &python.interpreter().python_full_version().to_string(),
2880 "3.13.0",
2881 "We should find the correct interpreter for the request"
2882 );
2883 assert!(
2884 !&python.interpreter().gil_disabled(),
2885 "We should prefer a python with the GIL"
2886 );
2887
2888 Ok(())
2889 }
2890
2891 #[test]
2892 fn find_python_pyodide() -> Result<()> {
2893 let mut context = TestContext::new()?;
2894
2895 context.add_pyodide_version("3.13.2")?;
2896
2897 let result = context.run(|| {
2899 find_python_installation(
2900 &PythonRequest::Default,
2901 EnvironmentPreference::Any,
2902 PythonPreference::OnlySystem,
2903 &context.cache,
2904 Preview::default(),
2905 )
2906 })?;
2907 assert!(
2908 result.is_err(),
2909 "We should not find an python; got {result:?}"
2910 );
2911
2912 let python = context.run(|| {
2914 find_python_installation(
2915 &PythonRequest::Any,
2916 EnvironmentPreference::Any,
2917 PythonPreference::OnlySystem,
2918 &context.cache,
2919 Preview::default(),
2920 )
2921 })??;
2922 assert_eq!(
2923 python.interpreter().python_full_version().to_string(),
2924 "3.13.2"
2925 );
2926
2927 context.add_python_versions(&["3.15.7"])?;
2929
2930 let python = context.run(|| {
2931 find_python_installation(
2932 &PythonRequest::Default,
2933 EnvironmentPreference::Any,
2934 PythonPreference::OnlySystem,
2935 &context.cache,
2936 Preview::default(),
2937 )
2938 })??;
2939 assert_eq!(
2940 python.interpreter().python_full_version().to_string(),
2941 "3.15.7"
2942 );
2943
2944 Ok(())
2945 }
2946}