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