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