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