1use thiserror::Error;
3
4#[cfg(test)]
5use uv_static::EnvVars;
6
7pub use crate::architecture::PythonArchitecture;
8#[cfg(all(test, unix))]
9use crate::discovery::find_python_installations;
10pub use crate::discovery::{
11 EnvironmentPreference, Error as DiscoveryError, PythonDownloads, PythonNotFound,
12 PythonPreference, PythonRequest, PythonSource, PythonVariant, VersionRequest,
13 find_all_python_installations,
14};
15pub use crate::environment::{InvalidEnvironmentKind, PythonEnvironment};
16pub use crate::implementation::{ImplementationName, LenientImplementationName};
17pub use crate::installation::{
18 PythonInstallation, PythonInstallationKey, PythonInstallationMinorVersionKey,
19};
20pub use crate::interpreter::{
21 BrokenLink, Error as InterpreterError, Interpreter, canonicalize_executable,
22};
23pub use crate::pointer_size::PointerSize;
24pub use crate::prefix::Prefix;
25pub use crate::python_version::{BuildVersionError, PythonVersion};
26pub use crate::target::Target;
27pub use crate::version_files::{
28 ConfigDiscovery, DiscoveryOptions as VersionFileDiscoveryOptions,
29 FilePreference as VersionFilePreference, PYTHON_VERSION_FILENAME, PYTHON_VERSIONS_FILENAME,
30 PythonVersionFile,
31};
32pub use crate::virtualenv::{Error as VirtualEnvError, PyVenvConfiguration, VirtualEnvironment};
33
34mod architecture;
35mod discovery;
36pub mod downloads;
37mod environment;
38mod implementation;
39mod installation;
40mod interpreter;
41pub mod macos_dylib;
42pub mod managed;
43#[cfg(windows)]
44mod microsoft_store;
45mod pointer_size;
46mod prefix;
47mod python_version;
48mod sysconfig;
49mod target;
50mod version_files;
51mod virtualenv;
52#[cfg(windows)]
53pub mod windows_registry;
54
55#[cfg(windows)]
56pub(crate) const COMPANY_KEY: &str = "Astral";
57#[cfg(windows)]
58pub(crate) const COMPANY_DISPLAY_NAME: &str = "Astral Software Inc.";
59
60#[cfg(not(test))]
61fn current_dir() -> Result<std::path::PathBuf, std::io::Error> {
62 std::env::current_dir()
63}
64
65#[cfg(test)]
66fn current_dir() -> Result<std::path::PathBuf, std::io::Error> {
67 std::env::var_os(EnvVars::PWD)
68 .map(std::path::PathBuf::from)
69 .map(Ok)
70 .unwrap_or(std::env::current_dir())
71}
72
73#[derive(Debug, Error)]
74pub enum Error {
75 #[error(transparent)]
76 Io(#[from] std::io::Error),
77
78 #[error(transparent)]
79 VirtualEnv(#[from] virtualenv::Error),
80
81 #[error(transparent)]
82 Query(#[from] interpreter::Error),
83
84 #[error(transparent)]
85 Discovery(#[from] discovery::Error),
86
87 #[error(transparent)]
88 ManagedPython(#[from] managed::Error),
89
90 #[error(transparent)]
91 Download(#[from] downloads::Error),
92
93 #[error(transparent)]
94 ClientBuild(#[from] uv_client::ClientBuildError),
95
96 #[error(transparent)]
98 KeyError(#[from] installation::PythonInstallationKeyError),
99
100 #[error("{}", .0)]
101 MissingPython(PythonNotFound, Option<Box<MissingPythonHint>>),
102
103 #[error(transparent)]
104 MissingEnvironment(#[from] environment::EnvironmentNotFound),
105
106 #[error(transparent)]
107 InvalidEnvironment(#[from] environment::InvalidEnvironment),
108
109 #[error(transparent)]
110 RetryParsing(#[from] uv_client::RetryParsingError),
111}
112
113#[derive(Debug)]
115pub enum MissingPythonHint {
116 RequiresUpdate,
118 DownloadsManual(PythonRequest),
120 DownloadsNever(PythonRequest),
122 PreferenceOnlySystem(PythonRequest),
124 Offline(PythonRequest),
126}
127
128impl MissingPythonHint {
129 fn for_request(request: &PythonRequest) -> String {
130 match request {
131 PythonRequest::Default | PythonRequest::Any => String::new(),
132 _ => format!(" for {request}"),
133 }
134 }
135}
136
137impl std::fmt::Display for MissingPythonHint {
138 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 match self {
140 Self::RequiresUpdate => {
141 write!(
142 f,
143 "uv embeds available Python downloads and may require an update to install new versions. Consider retrying on a newer version of uv."
144 )
145 }
146 Self::DownloadsManual(request) => {
147 write!(
148 f,
149 "A managed Python download is available{}, but Python downloads are set to 'manual', use `uv python install {}` to install the required version",
150 Self::for_request(request),
151 request.to_canonical_string(),
152 )
153 }
154 Self::DownloadsNever(request) => {
155 write!(
156 f,
157 "A managed Python download is available{}, but Python downloads are set to 'never'",
158 Self::for_request(request),
159 )
160 }
161 Self::PreferenceOnlySystem(request) => {
162 write!(
163 f,
164 "A managed Python download is available{}, but the Python preference is set to 'only system'",
165 Self::for_request(request),
166 )
167 }
168 Self::Offline(request) => {
169 write!(
170 f,
171 "A managed Python download is available{}, but uv is set to offline mode",
172 Self::for_request(request),
173 )
174 }
175 }
176 }
177}
178
179impl uv_errors::Hinted for Error {
180 fn hints(&self) -> uv_errors::Hints<'_> {
181 match self {
182 Self::MissingPython(_, Some(hint)) => uv_errors::Hints::from(hint.to_string()),
183 Self::Discovery(err) => err.hints(),
184 _ => uv_errors::Hints::none(),
185 }
186 }
187}
188
189impl Error {
190 fn with_hint(self, hint: MissingPythonHint) -> Self {
191 match self {
192 Self::MissingPython(err, _) => Self::MissingPython(err, Some(Box::new(hint))),
193 _ => self,
194 }
195 }
196}
197
198impl From<PythonNotFound> for Error {
199 fn from(err: PythonNotFound) -> Self {
200 Self::MissingPython(err, None)
201 }
202}
203
204#[cfg(all(test, unix))]
207mod tests {
208 use std::assert_matches;
209 use std::{
210 env,
211 ffi::{OsStr, OsString},
212 fs::Permissions,
213 io,
214 os::unix::fs::PermissionsExt,
215 path::{Path, PathBuf},
216 str::FromStr,
217 };
218
219 use anyhow::Result;
220 use assert_fs::{TempDir, fixture::ChildPath, prelude::*};
221 use indoc::{formatdoc, indoc};
222 use temp_env::with_vars;
223 use test_log::test;
224 use uv_client::BaseClientBuilder;
225 use uv_preview::PreviewFeature;
226 use uv_static::EnvVars;
227
228 use uv_cache::Cache;
229
230 use crate::{
231 PythonArchitecture, PythonDownloads, PythonNotFound, PythonRequest, PythonSource,
232 PythonVersion, find_all_python_installations, find_python_installations,
233 implementation::ImplementationName, installation::PythonInstallation,
234 managed::ManagedPythonInstallations, virtualenv::virtualenv_python_executable,
235 };
236 use crate::{
237 PythonPreference,
238 discovery::{
239 self, EnvironmentPreference, find_best_python_installation, find_python_installation,
240 },
241 };
242
243 struct TestContext {
244 tempdir: TempDir,
245 cache: Cache,
246 installations: ManagedPythonInstallations,
247 search_path: Option<Vec<PathBuf>>,
248 workdir: ChildPath,
249 }
250
251 impl TestContext {
252 fn new() -> Result<Self> {
253 let tempdir = TempDir::new()?;
254 let workdir = tempdir.child("workdir");
255 workdir.create_dir_all()?;
256
257 Ok(Self {
258 tempdir,
259 cache: Cache::temp()?,
260 installations: ManagedPythonInstallations::temp()?,
261 search_path: None,
262 workdir,
263 })
264 }
265
266 fn reset_search_path(&mut self) {
268 self.search_path = None;
269 }
270
271 fn add_to_search_path(&mut self, path: PathBuf) {
273 match self.search_path.as_mut() {
274 Some(paths) => paths.push(path),
275 None => self.search_path = Some(vec![path]),
276 }
277 }
278
279 fn new_search_path_directory(&mut self, name: impl AsRef<Path>) -> Result<ChildPath> {
281 let child = self.tempdir.child(name);
282 child.create_dir_all()?;
283 self.add_to_search_path(child.to_path_buf());
284 Ok(child)
285 }
286
287 fn run<F, R>(&self, closure: F) -> R
288 where
289 F: FnOnce() -> R,
290 {
291 self.run_with_vars(&[], closure)
292 }
293
294 fn run_with_vars<F, R>(&self, vars: &[(&str, Option<&OsStr>)], closure: F) -> R
295 where
296 F: FnOnce() -> R,
297 {
298 let path = self
299 .search_path
300 .as_ref()
301 .map(|paths| env::join_paths(paths).unwrap());
302
303 let mut run_vars: Vec<(&str, Option<&OsStr>)> = EnvVars::all_names()
304 .iter()
305 .copied()
306 .map(|name| (name, None))
307 .collect();
308 run_vars.extend([
309 (EnvVars::UV_PYTHON_NO_REGISTRY, Some(OsStr::new("1"))),
311 (EnvVars::PATH, path.as_deref()),
312 (
314 EnvVars::UV_PYTHON_INSTALL_DIR,
315 Some(self.installations.root().as_os_str()),
316 ),
317 (EnvVars::PWD, Some(self.workdir.path().as_os_str())),
319 ]);
320 run_vars.extend(vars.iter().copied());
321 with_vars(&run_vars, closure)
322 }
323
324 fn run_with_vars_and_preview<F, R>(
325 &self,
326 vars: &[(&str, Option<&OsStr>)],
327 preview_features: &[PreviewFeature],
328 closure: F,
329 ) -> R
330 where
331 F: FnOnce() -> R,
332 {
333 let _preview = uv_preview::test::with_features(preview_features);
334 self.run_with_vars(vars, closure)
335 }
336
337 fn create_mock_interpreter(
340 path: &Path,
341 version: &PythonVersion,
342 implementation: ImplementationName,
343 system: bool,
344 free_threaded: bool,
345 ) -> Result<()> {
346 let json = indoc! {r##"
347 {
348 "result": "success",
349 "platform": {
350 "os": {
351 "name": "manylinux",
352 "major": 2,
353 "minor": 38
354 },
355 "arch": "x86_64"
356 },
357 "manylinux_compatible": true,
358 "standalone": true,
359 "markers": {
360 "implementation_name": "{IMPLEMENTATION}",
361 "implementation_version": "{FULL_VERSION}",
362 "os_name": "posix",
363 "platform_machine": "x86_64",
364 "platform_python_implementation": "{IMPLEMENTATION}",
365 "platform_release": "6.5.0-13-generic",
366 "platform_system": "Linux",
367 "platform_version": "#13-Ubuntu SMP PREEMPT_DYNAMIC Fri Nov 3 12:16:05 UTC 2023",
368 "python_full_version": "{FULL_VERSION}",
369 "python_version": "{VERSION}",
370 "sys_platform": "linux"
371 },
372 "sys_base_exec_prefix": "/home/ferris/.pyenv/versions/{FULL_VERSION}",
373 "sys_base_prefix": "/home/ferris/.pyenv/versions/{FULL_VERSION}",
374 "sys_prefix": "{PREFIX}",
375 "sys_executable": "{PATH}",
376 "sys_path": [
377 "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}/lib/python{VERSION}",
378 "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}/site-packages"
379 ],
380 "site_packages": [
381 "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}/site-packages"
382 ],
383 "stdlib": "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}",
384 "extension_suffixes": [".cpython-{VERSION}-x86_64-linux-gnu.so", ".abi3.so", ".so"],
385 "scheme": {
386 "data": "/home/ferris/.pyenv/versions/{FULL_VERSION}",
387 "include": "/home/ferris/.pyenv/versions/{FULL_VERSION}/include",
388 "platlib": "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}/site-packages",
389 "purelib": "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}/site-packages",
390 "scripts": "/home/ferris/.pyenv/versions/{FULL_VERSION}/bin"
391 },
392 "virtualenv": {
393 "data": "",
394 "include": "include",
395 "platlib": "lib/python{VERSION}/site-packages",
396 "purelib": "lib/python{VERSION}/site-packages",
397 "scripts": "bin"
398 },
399 "pointer_size": "64",
400 "gil_disabled": {FREE_THREADED},
401 "debug_enabled": false
402 }
403 "##};
404
405 let json = if system {
406 json.replace("{PREFIX}", "/home/ferris/.pyenv/versions/{FULL_VERSION}")
407 } else {
408 json.replace("{PREFIX}", "/home/ferris/projects/uv/.venv")
409 };
410
411 let json = json
412 .replace(
413 "{PATH}",
414 path.to_str().expect("Path can be represented as string"),
415 )
416 .replace("{FULL_VERSION}", &version.to_string())
417 .replace(
418 "{VERSION}",
419 &format!("{}.{}", version.major(), version.minor()),
420 )
421 .replace("{FREE_THREADED}", &free_threaded.to_string())
422 .replace("{IMPLEMENTATION}", implementation.long_name());
423
424 fs_err::create_dir_all(path.parent().unwrap())?;
425 fs_err::write(
426 path,
427 formatdoc! {r"
428 #!/bin/sh
429 echo '{json}'
430 "},
431 )?;
432
433 fs_err::set_permissions(path, std::os::unix::fs::PermissionsExt::from_mode(0o770))?;
434
435 Ok(())
436 }
437
438 fn create_mock_pyodide_interpreter(path: &Path, version: &PythonVersion) -> Result<()> {
439 let json = indoc! {r##"
440 {
441 "result": "success",
442 "platform": {
443 "os": {
444 "name": "pyodide",
445 "major": 2025,
446 "minor": 0
447 },
448 "arch": "wasm32"
449 },
450 "manylinux_compatible": false,
451 "standalone": false,
452 "markers": {
453 "implementation_name": "cpython",
454 "implementation_version": "{FULL_VERSION}",
455 "os_name": "posix",
456 "platform_machine": "wasm32",
457 "platform_python_implementation": "CPython",
458 "platform_release": "4.0.9",
459 "platform_system": "Emscripten",
460 "platform_version": "#1",
461 "python_full_version": "{FULL_VERSION}",
462 "python_version": "{VERSION}",
463 "sys_platform": "emscripten"
464 },
465 "sys_base_exec_prefix": "/",
466 "sys_base_prefix": "/",
467 "sys_prefix": "/",
468 "sys_executable": "{PATH}",
469 "sys_path": [
470 "",
471 "/lib/python313.zip",
472 "/lib/python{VERSION}",
473 "/lib/python{VERSION}/lib-dynload",
474 "/lib/python{VERSION}/site-packages"
475 ],
476 "site_packages": [
477 "/lib/python{VERSION}/site-packages"
478 ],
479 "stdlib": "//lib/python{VERSION}",
480 "extension_suffixes": [".cpython-{VERSION}-wasm32-emscripten.so", ".so"],
481 "scheme": {
482 "platlib": "//lib/python{VERSION}/site-packages",
483 "purelib": "//lib/python{VERSION}/site-packages",
484 "include": "//include/python{VERSION}",
485 "scripts": "//bin",
486 "data": "/"
487 },
488 "virtualenv": {
489 "purelib": "lib/python{VERSION}/site-packages",
490 "platlib": "lib/python{VERSION}/site-packages",
491 "include": "include/site/python{VERSION}",
492 "scripts": "bin",
493 "data": ""
494 },
495 "pointer_size": "32",
496 "gil_disabled": false,
497 "debug_enabled": false
498 }
499 "##};
500
501 let json = json
502 .replace(
503 "{PATH}",
504 path.to_str().expect("Path can be represented as string"),
505 )
506 .replace("{FULL_VERSION}", &version.to_string())
507 .replace(
508 "{VERSION}",
509 &format!("{}.{}", version.major(), version.minor()),
510 );
511
512 fs_err::create_dir_all(path.parent().unwrap())?;
513 fs_err::write(
514 path,
515 formatdoc! {r"
516 #!/bin/sh
517 echo '{json}'
518 "},
519 )?;
520
521 fs_err::set_permissions(path, std::os::unix::fs::PermissionsExt::from_mode(0o770))?;
522
523 Ok(())
524 }
525
526 fn create_mock_python2_interpreter(path: &Path) -> Result<()> {
529 let output = indoc! { r"
530 Unknown option: -I
531 usage: /usr/bin/python [option] ... [-c cmd | -m mod | file | -] [arg] ...
532 Try `python -h` for more information.
533 "};
534
535 fs_err::write(
536 path,
537 formatdoc! {r"
538 #!/bin/sh
539 echo '{output}' 1>&2
540 "},
541 )?;
542
543 fs_err::set_permissions(path, std::os::unix::fs::PermissionsExt::from_mode(0o770))?;
544
545 Ok(())
546 }
547
548 fn new_search_path_directories(
550 &mut self,
551 names: &[impl AsRef<Path>],
552 ) -> Result<Vec<ChildPath>> {
553 let paths = names
554 .iter()
555 .map(|name| self.new_search_path_directory(name))
556 .collect::<Result<Vec<_>>>()?;
557 Ok(paths)
558 }
559
560 fn add_python_to_workdir(&self, name: &str, version: &str) -> Result<()> {
564 Self::create_mock_interpreter(
565 self.workdir.child(name).as_ref(),
566 &PythonVersion::from_str(version).expect("Test uses valid version"),
567 ImplementationName::default(),
568 true,
569 false,
570 )
571 }
572
573 fn add_pyodide_version(&mut self, version: &'static str) -> Result<()> {
574 let path = self.new_search_path_directory(format!("pyodide-{version}"))?;
575 let python = format!("pyodide{}", env::consts::EXE_SUFFIX);
576 Self::create_mock_pyodide_interpreter(
577 &path.join(python),
578 &PythonVersion::from_str(version).unwrap(),
579 )?;
580 Ok(())
581 }
582
583 fn add_python_versions(&mut self, versions: &[&'static str]) -> Result<()> {
587 let interpreters: Vec<_> = versions
588 .iter()
589 .map(|version| (true, ImplementationName::default(), "python", *version))
590 .collect();
591 self.add_python_interpreters(interpreters.as_slice())
592 }
593
594 fn add_python_interpreters(
598 &mut self,
599 kinds: &[(bool, ImplementationName, &'static str, &'static str)],
600 ) -> Result<()> {
601 let names: Vec<OsString> = kinds
603 .iter()
604 .map(|(system, implementation, name, version)| {
605 OsString::from_str(&format!("{system}-{implementation}-{name}-{version}"))
606 .unwrap()
607 })
608 .collect();
609 let paths = self.new_search_path_directories(names.as_slice())?;
610 for (path, (system, implementation, executable, version)) in
611 itertools::zip_eq(&paths, kinds)
612 {
613 let python = format!("{executable}{}", env::consts::EXE_SUFFIX);
614 Self::create_mock_interpreter(
615 &path.join(python),
616 &PythonVersion::from_str(version).unwrap(),
617 *implementation,
618 *system,
619 false,
620 )?;
621 }
622 Ok(())
623 }
624
625 fn mock_venv(path: impl AsRef<Path>, version: &'static str) -> Result<()> {
627 let executable = virtualenv_python_executable(path.as_ref());
628 fs_err::create_dir_all(
629 executable
630 .parent()
631 .expect("A Python executable path should always have a parent"),
632 )?;
633 Self::create_mock_interpreter(
634 &executable,
635 &PythonVersion::from_str(version)
636 .expect("A valid Python version is used for tests"),
637 ImplementationName::default(),
638 false,
639 false,
640 )?;
641 ChildPath::new(path.as_ref().join("pyvenv.cfg")).touch()?;
642 Ok(())
643 }
644
645 fn mock_conda_prefix(path: impl AsRef<Path>, version: &'static str) -> Result<()> {
649 let executable = virtualenv_python_executable(&path);
650 fs_err::create_dir_all(
651 executable
652 .parent()
653 .expect("A Python executable path should always have a parent"),
654 )?;
655 Self::create_mock_interpreter(
656 &executable,
657 &PythonVersion::from_str(version)
658 .expect("A valid Python version is used for tests"),
659 ImplementationName::default(),
660 true,
661 false,
662 )?;
663 ChildPath::new(path.as_ref().join("pyvenv.cfg")).touch()?;
664 Ok(())
665 }
666 }
667
668 #[test]
669 fn find_python_default_arch() -> Result<()> {
670 let mut context = TestContext::new()?;
671 let version = PythonVersion::from_str("3.14.1").expect("valid Python version");
672 let x86_64 = context
673 .new_search_path_directory("x86_64")?
674 .join("python3.14");
675 let aarch64 = context
676 .new_search_path_directory("aarch64")?
677 .join("python3.14");
678 for executable in [&x86_64, &aarch64] {
679 TestContext::create_mock_interpreter(
680 executable,
681 &version,
682 ImplementationName::CPython,
683 true,
684 false,
685 )?;
686 }
687 let script = fs_err::read_to_string(&aarch64)?;
688 fs_err::write(&aarch64, script.replace("x86_64", "aarch64"))?;
689 let arch = Some("aarch64".parse()?);
690
691 let installation = context.run(|| {
692 find_python_installation(
693 &PythonRequest::parse("3.14"),
694 EnvironmentPreference::OnlySystem,
695 PythonPreference::OnlySystem,
696 arch,
697 &context.cache,
698 )
699 })??;
700 assert_eq!(installation.interpreter().sys_executable(), aarch64);
701
702 for request in ["3.14.99", "3.99"] {
704 let installation = context.run(|| {
705 find_best_python_installation_no_download(
706 &PythonRequest::parse(request),
707 EnvironmentPreference::OnlySystem,
708 PythonPreference::OnlySystem,
709 arch,
710 &context.cache,
711 )
712 })?;
713 assert_eq!(installation.interpreter().sys_executable(), aarch64);
714 }
715
716 let installations = context.run(|| {
717 find_all_python_installations(
718 &PythonRequest::parse("3.14"),
719 EnvironmentPreference::OnlySystem,
720 PythonPreference::OnlySystem,
721 arch,
722 &context.cache,
723 )
724 })?;
725 assert_eq!(
726 installations
727 .iter()
728 .map(|installation| installation.interpreter().sys_executable())
729 .collect::<Vec<_>>(),
730 vec![aarch64.as_path()]
731 );
732
733 let installation = context.run(|| {
734 find_python_installation(
735 &PythonRequest::parse("cpython-3.14-linux-x86_64-gnu"),
736 EnvironmentPreference::OnlySystem,
737 PythonPreference::OnlySystem,
738 arch,
739 &context.cache,
740 )
741 })??;
742 assert_eq!(installation.interpreter().sys_executable(), x86_64);
743
744 let installation = context.run(|| {
745 find_python_installation(
746 &PythonRequest::File(x86_64.clone()),
747 EnvironmentPreference::OnlySystem,
748 PythonPreference::OnlySystem,
749 arch,
750 &context.cache,
751 )
752 })??;
753 assert_eq!(installation.interpreter().sys_executable(), x86_64);
754 Ok(())
755 }
756
757 #[test]
758 fn find_python_empty_path() -> Result<()> {
759 let mut context = TestContext::new()?;
760
761 context.search_path = Some(vec![]);
762 let result = context.run(|| {
763 find_python_installation(
764 &PythonRequest::Default,
765 EnvironmentPreference::OnlySystem,
766 PythonPreference::default(),
767 None,
768 &context.cache,
769 )
770 });
771 assert_matches!(
772 result,
773 Ok(Err(PythonNotFound { .. })),
774 "With an empty path, no Python installation should be detected got {result:?}"
775 );
776
777 context.search_path = None;
778 let result = context.run(|| {
779 find_python_installation(
780 &PythonRequest::Default,
781 EnvironmentPreference::OnlySystem,
782 PythonPreference::default(),
783 None,
784 &context.cache,
785 )
786 });
787 assert_matches!(
788 result,
789 Ok(Err(PythonNotFound { .. })),
790 "With an unset path, no Python installation should be detected got {result:?}"
791 );
792
793 Ok(())
794 }
795
796 #[test]
797 fn find_python_unexecutable_file() -> Result<()> {
798 let mut context = TestContext::new()?;
799 context
800 .new_search_path_directory("path")?
801 .child(format!("python{}", env::consts::EXE_SUFFIX))
802 .touch()?;
803
804 let result = context.run(|| {
805 find_python_installation(
806 &PythonRequest::Default,
807 EnvironmentPreference::OnlySystem,
808 PythonPreference::default(),
809 None,
810 &context.cache,
811 )
812 });
813 assert_matches!(
814 result,
815 Ok(Err(PythonNotFound { .. })),
816 "With a non-executable Python, no Python installation should be detected; got {result:?}"
817 );
818
819 Ok(())
820 }
821
822 #[test]
823 fn find_python_valid_executable() -> Result<()> {
824 let mut context = TestContext::new()?;
825 context.add_python_versions(&["3.12.1"])?;
826
827 let interpreter = context.run(|| {
828 find_python_installation(
829 &PythonRequest::Default,
830 EnvironmentPreference::OnlySystem,
831 PythonPreference::default(),
832 None,
833 &context.cache,
834 )
835 })??;
836 assert_matches!(
837 interpreter,
838 PythonInstallation {
839 source: PythonSource::SearchPathFirst,
840 interpreter: _
841 },
842 "We should find the valid executable; got {interpreter:?}"
843 );
844
845 Ok(())
846 }
847
848 #[test]
849 fn find_or_download_skips_download_metadata_when_python_is_found() -> Result<()> {
850 let mut context = TestContext::new()?;
851 context.add_python_versions(&["3.12.1"])?;
852 let missing_downloads = context.tempdir.child("missing-downloads.json");
855
856 let interpreter = context.run(|| {
857 let client_builder = BaseClientBuilder::default();
858 tokio::runtime::Builder::new_current_thread()
859 .enable_all()
860 .build()
861 .expect("Failed to build runtime")
862 .block_on(PythonInstallation::find_or_download(
863 None,
864 EnvironmentPreference::OnlySystem,
865 PythonPreference::OnlySystem,
866 None,
867 PythonDownloads::Never,
868 &client_builder,
869 &context.cache,
870 None,
871 None,
872 None,
873 missing_downloads.path().to_str(),
874 ))
875 })?;
876
877 assert_matches!(
878 interpreter,
879 PythonInstallation {
880 source: PythonSource::SearchPathFirst,
881 interpreter: _
882 },
883 "We should find the local Python without reading download metadata; got {interpreter:?}"
884 );
885 assert_eq!(
886 &interpreter.interpreter().python_full_version().to_string(),
887 "3.12.1",
888 "We should find the local interpreter"
889 );
890
891 Ok(())
892 }
893
894 #[test]
895 fn find_python_valid_executable_after_invalid() -> Result<()> {
896 let mut context = TestContext::new()?;
897 let children = context.new_search_path_directories(&[
898 "query-parse-error",
899 "not-executable",
900 "empty",
901 "good",
902 ])?;
903
904 #[cfg(unix)]
906 fs_err::write(
907 children[0].join(format!("python{}", env::consts::EXE_SUFFIX)),
908 formatdoc! {r"
909 #!/bin/sh
910 echo 'foo'
911 "},
912 )?;
913 fs_err::set_permissions(
914 children[0].join(format!("python{}", env::consts::EXE_SUFFIX)),
915 std::os::unix::fs::PermissionsExt::from_mode(0o770),
916 )?;
917
918 ChildPath::new(children[1].join(format!("python{}", env::consts::EXE_SUFFIX))).touch()?;
920
921 let python_path = children[3].join(format!("python{}", env::consts::EXE_SUFFIX));
925 TestContext::create_mock_interpreter(
926 &python_path,
927 &PythonVersion::from_str("3.12.1").unwrap(),
928 ImplementationName::default(),
929 true,
930 false,
931 )?;
932
933 let python = context.run(|| {
934 find_python_installation(
935 &PythonRequest::Default,
936 EnvironmentPreference::OnlySystem,
937 PythonPreference::default(),
938 None,
939 &context.cache,
940 )
941 })??;
942 assert_matches!(
943 python,
944 PythonInstallation {
945 source: PythonSource::SearchPath,
946 interpreter: _
947 },
948 "We should skip the bad executables in favor of the good one; got {python:?}"
949 );
950 assert_eq!(python.interpreter().sys_executable(), python_path);
951
952 Ok(())
953 }
954
955 #[test]
956 fn find_python_installations_discovers_search_path_lazily() -> Result<()> {
957 let context = TestContext::new()?;
958 let first_directory = context.tempdir.child("first");
959 let second_directory = context.tempdir.child("second");
960
961 let python = first_directory.join(format!("python{}", env::consts::EXE_SUFFIX));
962 let second = second_directory.join(format!("python{}", env::consts::EXE_SUFFIX));
963
964 let installation = context.run(|| -> Result<_> {
965 let mut installations = find_python_installations(
966 &PythonRequest::Default,
967 EnvironmentPreference::OnlySystem,
968 PythonPreference::OnlySystem,
969 None,
970 &context.cache,
971 );
972
973 TestContext::create_mock_interpreter(
974 &python,
975 &PythonVersion::from_str("3.12.1").expect("Test uses a valid Python version"),
976 ImplementationName::CPython,
977 true,
978 false,
979 )?;
980
981 let search_path = env::join_paths([first_directory.path(), second_directory.path()])?;
982 with_vars(
983 [(EnvVars::PATH, Some(search_path.as_os_str()))],
984 || -> Result<_> {
985 let installation = installations
986 .next()
987 .expect("Deferred search path should contain an interpreter")??;
988
989 TestContext::create_mock_interpreter(
990 &second,
991 &PythonVersion::from_str("3.11.9")
992 .expect("Test uses a valid Python version"),
993 ImplementationName::CPython,
994 true,
995 false,
996 )?;
997 let second_installation = installations
998 .next()
999 .expect("Later search path directory should be discovered")??;
1000 assert_eq!(second_installation.interpreter().sys_executable(), second);
1001
1002 Ok(installation)
1003 },
1004 )
1005 })?;
1006
1007 assert_eq!(installation.interpreter().sys_executable(), python);
1008
1009 Ok(())
1010 }
1011
1012 #[test]
1013 fn find_python_installation_queries_lazily() -> Result<()> {
1014 let mut context = TestContext::new()?;
1015 let first_directory = context.new_search_path_directory("first")?;
1016 let second_directory = context.new_search_path_directory("second")?;
1017
1018 let first = first_directory.join(format!("python{}", env::consts::EXE_SUFFIX));
1019 TestContext::create_mock_interpreter(
1020 &first,
1021 &PythonVersion::from_str("3.12.1").expect("Test uses a valid Python version"),
1022 ImplementationName::CPython,
1023 true,
1024 false,
1025 )?;
1026
1027 let second = second_directory.join(format!("python{}", env::consts::EXE_SUFFIX));
1028 TestContext::create_mock_interpreter(
1029 &second,
1030 &PythonVersion::from_str("3.11.9").expect("Test uses a valid Python version"),
1031 ImplementationName::CPython,
1032 true,
1033 false,
1034 )?;
1035 let second_target =
1036 second_directory.join(format!("python-real{}", env::consts::EXE_SUFFIX));
1037 fs_err::rename(&second, &second_target)?;
1038
1039 let marker = context.tempdir.child("second-was-queried");
1040 fs_err::write(
1041 &second,
1042 formatdoc! {r#"
1043 #!/bin/sh
1044 : > "{marker}"
1045 exec "{target}" "$@"
1046 "#,
1047 marker = marker.path().display(),
1048 target = second_target.display()},
1049 )?;
1050 fs_err::set_permissions(&second, std::os::unix::fs::PermissionsExt::from_mode(0o770))?;
1051
1052 let installation = context.run(|| {
1053 find_python_installation(
1054 &PythonRequest::Default,
1055 EnvironmentPreference::OnlySystem,
1056 PythonPreference::OnlySystem,
1057 None,
1058 &context.cache,
1059 )
1060 })??;
1061
1062 assert_eq!(installation.interpreter().sys_executable(), first);
1063 assert!(
1064 !marker.path().exists(),
1065 "Sequential discovery should not query candidates after finding a match"
1066 );
1067
1068 Ok(())
1069 }
1070
1071 #[test]
1072 fn find_all_python_installations_matches_sequential_discovery() -> Result<()> {
1073 let mut context = TestContext::new()?;
1074 let sequential_cache = Cache::temp()?;
1075 let parallel_cache = Cache::temp()?;
1076
1077 let broken_directory = context.new_search_path_directory("broken")?;
1078 let broken = broken_directory.join(format!("python{}", env::consts::EXE_SUFFIX));
1079 fs_err::write(
1080 &broken,
1081 formatdoc! {r"
1082 #!/bin/sh
1083 echo 'not interpreter metadata'
1084 "},
1085 )?;
1086 fs_err::set_permissions(&broken, std::os::unix::fs::PermissionsExt::from_mode(0o770))?;
1087
1088 let cpython_311_directory = context.new_search_path_directory("cpython-3.11")?;
1089 let cpython_311 = cpython_311_directory.join(format!("python{}", env::consts::EXE_SUFFIX));
1090 TestContext::create_mock_interpreter(
1091 &cpython_311,
1092 &PythonVersion::from_str("3.11.9").expect("Test uses a valid Python version"),
1093 ImplementationName::CPython,
1094 true,
1095 false,
1096 )?;
1097 let cpython_311_target =
1098 cpython_311_directory.join(format!("python-real{}", env::consts::EXE_SUFFIX));
1099 fs_err::rename(&cpython_311, &cpython_311_target)?;
1100 fs_err::write(
1101 &cpython_311,
1102 formatdoc! {r#"
1103 #!/bin/sh
1104 sleep 1
1105 exec "{target}" "$@"
1106 "#,
1107 target = cpython_311_target.display()},
1108 )?;
1109 fs_err::set_permissions(
1110 &cpython_311,
1111 std::os::unix::fs::PermissionsExt::from_mode(0o770),
1112 )?;
1113
1114 let cpython_312_directory = context.new_search_path_directory("cpython-3.12")?;
1115 let cpython_312 = cpython_312_directory.join(format!("python{}", env::consts::EXE_SUFFIX));
1116 TestContext::create_mock_interpreter(
1117 &cpython_312,
1118 &PythonVersion::from_str("3.12.1").expect("Test uses a valid Python version"),
1119 ImplementationName::CPython,
1120 true,
1121 false,
1122 )?;
1123
1124 let pypy_directory = context.new_search_path_directory("pypy-3.10")?;
1125 let pypy = pypy_directory.join(format!("pypy{}", env::consts::EXE_SUFFIX));
1126 TestContext::create_mock_interpreter(
1127 &pypy,
1128 &PythonVersion::from_str("3.10.14").expect("Test uses a valid Python version"),
1129 ImplementationName::PyPy,
1130 true,
1131 false,
1132 )?;
1133
1134 let virtual_environment = context.tempdir.child("virtual-environment");
1135 TestContext::mock_venv(&virtual_environment, "3.12.1")?;
1136
1137 let key = context
1138 .run(|| {
1139 find_python_installation(
1140 &PythonRequest::File(cpython_312.clone()),
1141 EnvironmentPreference::OnlySystem,
1142 PythonPreference::OnlySystem,
1143 None,
1144 &context.cache,
1145 )
1146 })??
1147 .key()
1148 .to_string();
1149 let key_request = PythonRequest::parse(&key);
1150 assert_matches!(
1151 key_request,
1152 PythonRequest::Key(_),
1153 "Expected an installation key request, got {key_request:?}"
1154 );
1155
1156 let requests = [
1157 PythonRequest::Any,
1158 PythonRequest::Default,
1159 PythonRequest::parse("3.12"),
1160 PythonRequest::parse("cpython"),
1161 PythonRequest::parse("pypy@3.10"),
1162 PythonRequest::ExecutableName(format!("pypy{}", env::consts::EXE_SUFFIX)),
1163 PythonRequest::File(cpython_312),
1164 PythonRequest::Directory(virtual_environment.to_path_buf()),
1165 key_request,
1166 ];
1167
1168 for request in requests {
1169 let (sequential, parallel) = context.run(|| {
1170 let mut sequential = Vec::new();
1171 for result in find_python_installations(
1172 &request,
1173 EnvironmentPreference::OnlySystem,
1174 PythonPreference::OnlySystem,
1175 None,
1176 &sequential_cache,
1177 ) {
1178 match result {
1179 Ok(Ok(installation)) => sequential.push(installation),
1180 Ok(Err(_)) => {}
1181 Err(err) if err.is_critical() => return Err(err),
1182 Err(_) => {}
1183 }
1184 }
1185
1186 let parallel = find_all_python_installations(
1187 &request,
1188 EnvironmentPreference::OnlySystem,
1189 PythonPreference::OnlySystem,
1190 None,
1191 ¶llel_cache,
1192 )?;
1193 Ok::<_, discovery::Error>((sequential, parallel))
1194 })?;
1195
1196 let identifiers = |installations: Vec<PythonInstallation>| {
1197 installations
1198 .into_iter()
1199 .map(|installation| {
1200 (
1201 *installation.source(),
1202 installation.interpreter().sys_executable().to_path_buf(),
1203 installation.key().to_string(),
1204 )
1205 })
1206 .collect::<Vec<_>>()
1207 };
1208 assert_eq!(
1209 identifiers(sequential),
1210 identifiers(parallel),
1211 "Sequential and parallel discovery differ for {request}"
1212 );
1213 }
1214
1215 Ok(())
1216 }
1217
1218 #[test]
1219 fn find_python_only_python2_executable() -> Result<()> {
1220 let mut context = TestContext::new()?;
1221 let python = context
1222 .new_search_path_directory("python2")?
1223 .child(format!("python{}", env::consts::EXE_SUFFIX));
1224 TestContext::create_mock_python2_interpreter(&python)?;
1225
1226 let result = context.run(|| {
1227 find_python_installation(
1228 &PythonRequest::Default,
1229 EnvironmentPreference::OnlySystem,
1230 PythonPreference::default(),
1231 None,
1232 &context.cache,
1233 )
1234 });
1235 assert_matches!(
1236 result,
1237 Err(discovery::Error::Query(..)),
1238 "If only Python 2 is available, we should report the interpreter query error; got {result:?}"
1239 );
1240
1241 Ok(())
1242 }
1243
1244 #[test]
1245 fn find_python_skip_python2_executable() -> Result<()> {
1246 let mut context = TestContext::new()?;
1247
1248 let python2 = context
1249 .new_search_path_directory("python2")?
1250 .child(format!("python{}", env::consts::EXE_SUFFIX));
1251 TestContext::create_mock_python2_interpreter(&python2)?;
1252
1253 let python3 = context
1254 .new_search_path_directory("python3")?
1255 .child(format!("python{}", env::consts::EXE_SUFFIX));
1256 TestContext::create_mock_interpreter(
1257 &python3,
1258 &PythonVersion::from_str("3.12.1").unwrap(),
1259 ImplementationName::default(),
1260 true,
1261 false,
1262 )?;
1263
1264 let python = context.run(|| {
1265 find_python_installation(
1266 &PythonRequest::Default,
1267 EnvironmentPreference::OnlySystem,
1268 PythonPreference::default(),
1269 None,
1270 &context.cache,
1271 )
1272 })??;
1273 assert_matches!(
1274 python,
1275 PythonInstallation {
1276 source: PythonSource::SearchPath,
1277 interpreter: _
1278 },
1279 "We should skip the Python 2 installation and find the Python 3 interpreter; got {python:?}"
1280 );
1281 assert_eq!(python.interpreter().sys_executable(), python3.path());
1282
1283 Ok(())
1284 }
1285
1286 #[test]
1287 fn find_python_system_python_allowed() -> Result<()> {
1288 let mut context = TestContext::new()?;
1289 context.add_python_interpreters(&[
1290 (false, ImplementationName::CPython, "python", "3.10.0"),
1291 (true, ImplementationName::CPython, "python", "3.10.1"),
1292 ])?;
1293
1294 let python = context.run(|| {
1295 find_python_installation(
1296 &PythonRequest::Default,
1297 EnvironmentPreference::Any,
1298 PythonPreference::OnlySystem,
1299 None,
1300 &context.cache,
1301 )
1302 })??;
1303 assert_eq!(
1304 python.interpreter().python_full_version().to_string(),
1305 "3.10.0",
1306 "Should find the first interpreter regardless of system"
1307 );
1308
1309 context.reset_search_path();
1311 context.add_python_interpreters(&[
1312 (true, ImplementationName::CPython, "python", "3.10.1"),
1313 (false, ImplementationName::CPython, "python", "3.10.0"),
1314 ])?;
1315
1316 let python = context.run(|| {
1317 find_python_installation(
1318 &PythonRequest::Default,
1319 EnvironmentPreference::Any,
1320 PythonPreference::OnlySystem,
1321 None,
1322 &context.cache,
1323 )
1324 })??;
1325 assert_eq!(
1326 python.interpreter().python_full_version().to_string(),
1327 "3.10.1",
1328 "Should find the first interpreter regardless of system"
1329 );
1330
1331 Ok(())
1332 }
1333
1334 #[test]
1335 fn find_python_system_python_required() -> Result<()> {
1336 let mut context = TestContext::new()?;
1337 context.add_python_interpreters(&[
1338 (false, ImplementationName::CPython, "python", "3.10.0"),
1339 (true, ImplementationName::CPython, "python", "3.10.1"),
1340 ])?;
1341
1342 let python = context.run(|| {
1343 find_python_installation(
1344 &PythonRequest::Default,
1345 EnvironmentPreference::OnlySystem,
1346 PythonPreference::OnlySystem,
1347 None,
1348 &context.cache,
1349 )
1350 })??;
1351 assert_eq!(
1352 python.interpreter().python_full_version().to_string(),
1353 "3.10.1",
1354 "Should skip the virtual environment"
1355 );
1356
1357 Ok(())
1358 }
1359
1360 #[test]
1361 fn find_python_system_python_disallowed() -> Result<()> {
1362 let mut context = TestContext::new()?;
1363 context.add_python_interpreters(&[
1364 (true, ImplementationName::CPython, "python", "3.10.0"),
1365 (false, ImplementationName::CPython, "python", "3.10.1"),
1366 ])?;
1367
1368 let python = context.run(|| {
1369 find_python_installation(
1370 &PythonRequest::Default,
1371 EnvironmentPreference::Any,
1372 PythonPreference::OnlySystem,
1373 None,
1374 &context.cache,
1375 )
1376 })??;
1377 assert_eq!(
1378 python.interpreter().python_full_version().to_string(),
1379 "3.10.0",
1380 "Should skip the system Python"
1381 );
1382
1383 Ok(())
1384 }
1385
1386 #[test]
1387 fn find_python_version_minor() -> Result<()> {
1388 let mut context = TestContext::new()?;
1389 context.add_python_versions(&["3.10.1", "3.11.2", "3.12.3"])?;
1390
1391 let python = context.run(|| {
1392 find_python_installation(
1393 &PythonRequest::parse("3.11"),
1394 EnvironmentPreference::Any,
1395 PythonPreference::OnlySystem,
1396 None,
1397 &context.cache,
1398 )
1399 })??;
1400
1401 assert_matches!(
1402 python,
1403 PythonInstallation {
1404 source: PythonSource::SearchPath,
1405 interpreter: _
1406 },
1407 "We should find a python; got {python:?}"
1408 );
1409 assert_eq!(
1410 &python.interpreter().python_full_version().to_string(),
1411 "3.11.2",
1412 "We should find the correct interpreter for the request"
1413 );
1414
1415 Ok(())
1416 }
1417
1418 #[test]
1419 fn find_python_version_patch() -> Result<()> {
1420 let mut context = TestContext::new()?;
1421 context.add_python_versions(&["3.10.1", "3.11.3", "3.11.2", "3.12.3"])?;
1422
1423 let python = context.run(|| {
1424 find_python_installation(
1425 &PythonRequest::parse("3.11.2"),
1426 EnvironmentPreference::Any,
1427 PythonPreference::OnlySystem,
1428 None,
1429 &context.cache,
1430 )
1431 })??;
1432
1433 assert_matches!(
1434 python,
1435 PythonInstallation {
1436 source: PythonSource::SearchPath,
1437 interpreter: _
1438 },
1439 "We should find a python; got {python:?}"
1440 );
1441 assert_eq!(
1442 &python.interpreter().python_full_version().to_string(),
1443 "3.11.2",
1444 "We should find the correct interpreter for the request"
1445 );
1446
1447 Ok(())
1448 }
1449
1450 #[test]
1451 fn find_python_version_minor_no_match() -> Result<()> {
1452 let mut context = TestContext::new()?;
1453 context.add_python_versions(&["3.10.1", "3.11.2", "3.12.3"])?;
1454
1455 let result = context.run(|| {
1456 find_python_installation(
1457 &PythonRequest::parse("3.9"),
1458 EnvironmentPreference::Any,
1459 PythonPreference::OnlySystem,
1460 None,
1461 &context.cache,
1462 )
1463 })?;
1464 assert_matches!(
1465 result,
1466 Err(PythonNotFound { .. }),
1467 "We should not find a python; got {result:?}"
1468 );
1469
1470 Ok(())
1471 }
1472
1473 #[test]
1474 fn find_python_version_patch_no_match() -> Result<()> {
1475 let mut context = TestContext::new()?;
1476 context.add_python_versions(&["3.10.1", "3.11.2", "3.12.3"])?;
1477
1478 let result = context.run(|| {
1479 find_python_installation(
1480 &PythonRequest::parse("3.11.9"),
1481 EnvironmentPreference::Any,
1482 PythonPreference::OnlySystem,
1483 None,
1484 &context.cache,
1485 )
1486 })?;
1487 assert_matches!(
1488 result,
1489 Err(PythonNotFound { .. }),
1490 "We should not find a python; got {result:?}"
1491 );
1492
1493 Ok(())
1494 }
1495
1496 fn find_best_python_installation_no_download(
1497 request: &PythonRequest,
1498 environments: EnvironmentPreference,
1499 preference: PythonPreference,
1500 arch: Option<PythonArchitecture>,
1501 cache: &Cache,
1502 ) -> Result<PythonInstallation, crate::Error> {
1503 let client_builder = BaseClientBuilder::default();
1504 tokio::runtime::Builder::new_current_thread()
1505 .enable_all()
1506 .build()
1507 .expect("Failed to build runtime")
1508 .block_on(find_best_python_installation(
1509 request,
1510 environments,
1511 preference,
1512 arch,
1513 false,
1514 &client_builder,
1515 cache,
1516 None,
1517 None,
1518 None,
1519 None,
1520 ))
1521 }
1522
1523 #[test]
1524 fn find_best_python_version_patch_exact() -> Result<()> {
1525 let mut context = TestContext::new()?;
1526 context.add_python_versions(&["3.10.1", "3.11.2", "3.11.4", "3.11.3", "3.12.5"])?;
1527
1528 let python = context.run(|| {
1529 find_best_python_installation_no_download(
1530 &PythonRequest::parse("3.11.3"),
1531 EnvironmentPreference::Any,
1532 PythonPreference::OnlySystem,
1533 None,
1534 &context.cache,
1535 )
1536 })?;
1537
1538 assert_matches!(
1539 python,
1540 PythonInstallation {
1541 source: PythonSource::SearchPath,
1542 interpreter: _
1543 },
1544 "We should find a python; got {python:?}"
1545 );
1546 assert_eq!(
1547 &python.interpreter().python_full_version().to_string(),
1548 "3.11.3",
1549 "We should prefer the exact request"
1550 );
1551
1552 Ok(())
1553 }
1554
1555 #[test]
1556 fn find_best_python_version_patch_fallback() -> Result<()> {
1557 let mut context = TestContext::new()?;
1558 context.add_python_versions(&["3.10.1", "3.11.2", "3.11.4", "3.11.3", "3.12.5"])?;
1559
1560 let python = context.run(|| {
1561 find_best_python_installation_no_download(
1562 &PythonRequest::parse("3.11.11"),
1563 EnvironmentPreference::Any,
1564 PythonPreference::OnlySystem,
1565 None,
1566 &context.cache,
1567 )
1568 })?;
1569
1570 assert_matches!(
1571 python,
1572 PythonInstallation {
1573 source: PythonSource::SearchPath,
1574 interpreter: _
1575 },
1576 "We should find a python; got {python:?}"
1577 );
1578 assert_eq!(
1579 &python.interpreter().python_full_version().to_string(),
1580 "3.11.2",
1581 "We should fallback to the first matching minor"
1582 );
1583
1584 Ok(())
1585 }
1586
1587 #[test]
1588 fn find_best_python_skips_source_without_match() -> Result<()> {
1589 let mut context = TestContext::new()?;
1590 let venv = context.tempdir.child(".venv");
1591 TestContext::mock_venv(&venv, "3.12.0")?;
1592 context.add_python_versions(&["3.10.1"])?;
1593
1594 let python =
1595 context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
1596 find_best_python_installation_no_download(
1597 &PythonRequest::parse("3.10"),
1598 EnvironmentPreference::Any,
1599 PythonPreference::OnlySystem,
1600 None,
1601 &context.cache,
1602 )
1603 })?;
1604 assert_matches!(
1605 python,
1606 PythonInstallation {
1607 source: PythonSource::SearchPathFirst,
1608 interpreter: _
1609 },
1610 "We should skip the active environment in favor of the requested version; got {python:?}"
1611 );
1612
1613 Ok(())
1614 }
1615
1616 #[test]
1617 fn find_best_python_returns_to_earlier_source_on_fallback() -> Result<()> {
1618 let mut context = TestContext::new()?;
1619 let venv = context.tempdir.child(".venv");
1620 TestContext::mock_venv(&venv, "3.10.1")?;
1621 context.add_python_versions(&["3.10.3"])?;
1622
1623 let python =
1624 context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
1625 find_best_python_installation_no_download(
1626 &PythonRequest::parse("3.10.2"),
1627 EnvironmentPreference::Any,
1628 PythonPreference::OnlySystem,
1629 None,
1630 &context.cache,
1631 )
1632 })?;
1633 assert_matches!(
1634 python,
1635 PythonInstallation {
1636 source: PythonSource::ActiveEnvironment,
1637 interpreter: _
1638 },
1639 "We should prefer the active environment after relaxing; got {python:?}"
1640 );
1641 assert_eq!(
1642 python.interpreter().python_full_version().to_string(),
1643 "3.10.1",
1644 "We should prefer the active environment"
1645 );
1646
1647 Ok(())
1648 }
1649
1650 #[test]
1651 fn find_python_from_active_python() -> Result<()> {
1652 let context = TestContext::new()?;
1653 let venv = context.tempdir.child("some-venv");
1654 TestContext::mock_venv(&venv, "3.12.0")?;
1655
1656 let python =
1657 context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
1658 find_python_installation(
1659 &PythonRequest::Default,
1660 EnvironmentPreference::Any,
1661 PythonPreference::OnlySystem,
1662 None,
1663 &context.cache,
1664 )
1665 })??;
1666 assert_eq!(
1667 python.interpreter().python_full_version().to_string(),
1668 "3.12.0",
1669 "We should prefer the active environment"
1670 );
1671
1672 Ok(())
1673 }
1674
1675 #[test]
1676 fn find_python_from_active_python_prerelease() -> Result<()> {
1677 let mut context = TestContext::new()?;
1678 context.add_python_versions(&["3.12.0"])?;
1679 let venv = context.tempdir.child("some-venv");
1680 TestContext::mock_venv(&venv, "3.13.0rc1")?;
1681
1682 let python =
1683 context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
1684 find_python_installation(
1685 &PythonRequest::Default,
1686 EnvironmentPreference::Any,
1687 PythonPreference::OnlySystem,
1688 None,
1689 &context.cache,
1690 )
1691 })??;
1692 assert_eq!(
1693 python.interpreter().python_full_version().to_string(),
1694 "3.13.0rc1",
1695 "We should prefer the active environment"
1696 );
1697
1698 Ok(())
1699 }
1700
1701 #[test]
1702 fn find_python_from_conda_prefix() -> Result<()> {
1703 let context = TestContext::new()?;
1704 let condaenv = context.tempdir.child("condaenv");
1705 TestContext::mock_conda_prefix(&condaenv, "3.12.0")?;
1706
1707 let python = context
1708 .run_with_vars(
1709 &[(EnvVars::CONDA_PREFIX, Some(condaenv.as_os_str()))],
1710 || {
1711 find_python_installation(
1713 &PythonRequest::Default,
1714 EnvironmentPreference::OnlyVirtual,
1715 PythonPreference::OnlySystem,
1716 None,
1717 &context.cache,
1718 )
1719 },
1720 )?
1721 .unwrap();
1722 assert_eq!(
1723 python.interpreter().python_full_version().to_string(),
1724 "3.12.0",
1725 "We should allow the active conda python"
1726 );
1727
1728 let baseenv = context.tempdir.child("conda");
1729 TestContext::mock_conda_prefix(&baseenv, "3.12.1")?;
1730
1731 let result = context.run_with_vars_and_preview(
1733 &[
1734 (EnvVars::CONDA_PREFIX, Some(baseenv.as_os_str())),
1735 (EnvVars::CONDA_DEFAULT_ENV, Some(&OsString::from("base"))),
1736 (EnvVars::CONDA_ROOT, None),
1737 ],
1738 &[],
1739 || {
1740 find_python_installation(
1741 &PythonRequest::Default,
1742 EnvironmentPreference::OnlyVirtual,
1743 PythonPreference::OnlySystem,
1744 None,
1745 &context.cache,
1746 )
1747 },
1748 )?;
1749
1750 assert_matches!(
1751 result,
1752 Err(PythonNotFound { .. }),
1753 "We should not allow the non-virtual environment; got {result:?}"
1754 );
1755
1756 let python = context
1758 .run_with_vars_and_preview(
1759 &[
1760 (EnvVars::CONDA_PREFIX, Some(baseenv.as_os_str())),
1761 (EnvVars::CONDA_DEFAULT_ENV, Some(&OsString::from("base"))),
1762 (EnvVars::CONDA_ROOT, None),
1763 ],
1764 &[],
1765 || {
1766 find_python_installation(
1767 &PythonRequest::Default,
1768 EnvironmentPreference::OnlySystem,
1769 PythonPreference::OnlySystem,
1770 None,
1771 &context.cache,
1772 )
1773 },
1774 )?
1775 .unwrap();
1776
1777 assert_eq!(
1778 python.interpreter().python_full_version().to_string(),
1779 "3.12.1",
1780 "We should find the base conda environment"
1781 );
1782
1783 let python = context
1785 .run_with_vars_and_preview(
1786 &[
1787 (EnvVars::CONDA_PREFIX, Some(condaenv.as_os_str())),
1788 (
1789 EnvVars::CONDA_DEFAULT_ENV,
1790 Some(&OsString::from("condaenv")),
1791 ),
1792 ],
1793 &[],
1794 || {
1795 find_python_installation(
1796 &PythonRequest::Default,
1797 EnvironmentPreference::OnlyVirtual,
1798 PythonPreference::OnlySystem,
1799 None,
1800 &context.cache,
1801 )
1802 },
1803 )?
1804 .unwrap();
1805
1806 assert_eq!(
1807 python.interpreter().python_full_version().to_string(),
1808 "3.12.0",
1809 "We should find the conda environment when name matches"
1810 );
1811
1812 let result = context.run_with_vars_and_preview(
1815 &[
1816 (EnvVars::CONDA_PREFIX, Some(condaenv.as_os_str())),
1817 (EnvVars::CONDA_DEFAULT_ENV, Some(&OsString::from("base"))),
1818 ],
1819 &[],
1820 || {
1821 find_python_installation(
1822 &PythonRequest::Default,
1823 EnvironmentPreference::OnlyVirtual,
1824 PythonPreference::OnlySystem,
1825 None,
1826 &context.cache,
1827 )
1828 },
1829 )?;
1830
1831 assert_matches!(
1832 result,
1833 Err(PythonNotFound { .. }),
1834 "We should not allow the base environment when looking for virtual environments"
1835 );
1836
1837 for (name, version) in [("base", "3.12.6"), ("root", "3.12.7")] {
1840 let environment = context.tempdir.child(name);
1841 TestContext::mock_conda_prefix(&environment, version)?;
1842 let python = context
1843 .run_with_vars(
1844 &[
1845 (EnvVars::CONDA_PREFIX, Some(environment.as_os_str())),
1846 (EnvVars::CONDA_DEFAULT_ENV, Some(&OsString::from(name))),
1847 (EnvVars::CONDA_ROOT, None),
1848 ],
1849 || {
1850 find_python_installation(
1851 &PythonRequest::Default,
1852 EnvironmentPreference::OnlyVirtual,
1853 PythonPreference::OnlySystem,
1854 None,
1855 &context.cache,
1856 )
1857 },
1858 )?
1859 .unwrap();
1860
1861 assert_eq!(
1862 python.interpreter().python_full_version().to_string(),
1863 version,
1864 "We should find the child Conda environment named {name}"
1865 );
1866 }
1867
1868 let myenv_dir = context.tempdir.child("myenv");
1870 TestContext::mock_conda_prefix(&myenv_dir, "3.12.5")?;
1871 let python = context
1872 .run_with_vars_and_preview(
1873 &[
1874 (EnvVars::CONDA_PREFIX, Some(myenv_dir.as_os_str())),
1875 (EnvVars::CONDA_DEFAULT_ENV, Some(&OsString::from("myenv"))),
1876 ],
1877 &[],
1878 || {
1879 find_python_installation(
1880 &PythonRequest::Default,
1881 EnvironmentPreference::OnlyVirtual,
1882 PythonPreference::OnlySystem,
1883 None,
1884 &context.cache,
1885 )
1886 },
1887 )?
1888 .unwrap();
1889
1890 assert_eq!(
1891 python.interpreter().python_full_version().to_string(),
1892 "3.12.5",
1893 "We should find the child conda environment"
1894 );
1895
1896 let conda_root_env = context.tempdir.child("conda-root");
1898 TestContext::mock_conda_prefix(&conda_root_env, "3.12.2")?;
1899
1900 let result = context.run_with_vars(
1902 &[
1903 (EnvVars::CONDA_PREFIX, Some(conda_root_env.as_os_str())),
1904 (EnvVars::CONDA_ROOT, Some(conda_root_env.as_os_str())),
1905 (
1906 EnvVars::CONDA_DEFAULT_ENV,
1907 Some(&OsString::from("custom-name")),
1908 ),
1909 ],
1910 || {
1911 find_python_installation(
1912 &PythonRequest::Default,
1913 EnvironmentPreference::OnlyVirtual,
1914 PythonPreference::OnlySystem,
1915 None,
1916 &context.cache,
1917 )
1918 },
1919 )?;
1920
1921 assert_matches!(
1922 result,
1923 Err(PythonNotFound { .. }),
1924 "Base environment detected via _CONDA_ROOT should be excluded from virtual environments; got {result:?}"
1925 );
1926
1927 let other_conda_env = context.tempdir.child("other-conda");
1929 TestContext::mock_conda_prefix(&other_conda_env, "3.12.3")?;
1930
1931 let python = context
1932 .run_with_vars_and_preview(
1933 &[
1934 (EnvVars::CONDA_PREFIX, Some(other_conda_env.as_os_str())),
1935 (EnvVars::CONDA_ROOT, Some(conda_root_env.as_os_str())),
1936 (
1937 EnvVars::CONDA_DEFAULT_ENV,
1938 Some(&OsString::from("other-conda")),
1939 ),
1940 ],
1941 &[],
1942 || {
1943 find_python_installation(
1944 &PythonRequest::Default,
1945 EnvironmentPreference::OnlyVirtual,
1946 PythonPreference::OnlySystem,
1947 None,
1948 &context.cache,
1949 )
1950 },
1951 )?
1952 .unwrap();
1953
1954 assert_eq!(
1955 python.interpreter().python_full_version().to_string(),
1956 "3.12.3",
1957 "Non-base conda environment should be available for virtual environment preference"
1958 );
1959
1960 let unnamed_env = context.tempdir.child("my-conda-env");
1962 TestContext::mock_conda_prefix(&unnamed_env, "3.12.4")?;
1963 let unnamed_env_path = unnamed_env.to_string_lossy().to_string();
1964
1965 let python = context.run_with_vars(
1966 &[
1967 (EnvVars::CONDA_PREFIX, Some(unnamed_env.as_os_str())),
1968 (
1969 EnvVars::CONDA_DEFAULT_ENV,
1970 Some(&OsString::from(&unnamed_env_path)),
1971 ),
1972 ],
1973 || {
1974 find_python_installation(
1975 &PythonRequest::Default,
1976 EnvironmentPreference::OnlyVirtual,
1977 PythonPreference::OnlySystem,
1978 None,
1979 &context.cache,
1980 )
1981 },
1982 )??;
1983
1984 assert_eq!(
1985 python.interpreter().python_full_version().to_string(),
1986 "3.12.4",
1987 "We should find the unnamed conda environment"
1988 );
1989
1990 Ok(())
1991 }
1992
1993 #[test]
1994 fn find_python_from_conda_prefix_and_virtualenv() -> Result<()> {
1995 let context = TestContext::new()?;
1996 let venv = context.tempdir.child(".venv");
1997 TestContext::mock_venv(&venv, "3.12.0")?;
1998 let condaenv = context.tempdir.child("condaenv");
1999 TestContext::mock_conda_prefix(&condaenv, "3.12.1")?;
2000
2001 let python = context.run_with_vars(
2002 &[
2003 (EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())),
2004 (EnvVars::CONDA_PREFIX, Some(condaenv.as_os_str())),
2005 ],
2006 || {
2007 find_python_installation(
2008 &PythonRequest::Default,
2009 EnvironmentPreference::Any,
2010 PythonPreference::OnlySystem,
2011 None,
2012 &context.cache,
2013 )
2014 },
2015 )??;
2016 assert_eq!(
2017 python.interpreter().python_full_version().to_string(),
2018 "3.12.0",
2019 "We should prefer the non-conda python"
2020 );
2021
2022 let venv = context.workdir.child(".venv");
2024 TestContext::mock_venv(venv, "3.12.2")?;
2025 let python = context.run_with_vars(
2026 &[(EnvVars::CONDA_PREFIX, Some(condaenv.as_os_str()))],
2027 || {
2028 find_python_installation(
2029 &PythonRequest::Default,
2030 EnvironmentPreference::Any,
2031 PythonPreference::OnlySystem,
2032 None,
2033 &context.cache,
2034 )
2035 },
2036 )??;
2037 assert_eq!(
2038 python.interpreter().python_full_version().to_string(),
2039 "3.12.1",
2040 "We should prefer the conda python over inactive virtual environments"
2041 );
2042
2043 Ok(())
2044 }
2045
2046 #[test]
2047 fn find_python_from_discovered_python() -> Result<()> {
2048 let mut context = TestContext::new()?;
2049
2050 let venv = context.tempdir.child(".venv");
2052 TestContext::mock_venv(venv, "3.12.0")?;
2053
2054 let python = context.run(|| {
2055 find_python_installation(
2056 &PythonRequest::Default,
2057 EnvironmentPreference::Any,
2058 PythonPreference::OnlySystem,
2059 None,
2060 &context.cache,
2061 )
2062 })??;
2063
2064 assert_eq!(
2065 python.interpreter().python_full_version().to_string(),
2066 "3.12.0",
2067 "We should find the python"
2068 );
2069
2070 context.add_python_versions(&["3.12.1", "3.12.2"])?;
2072 let python = context.run(|| {
2073 find_python_installation(
2074 &PythonRequest::Default,
2075 EnvironmentPreference::Any,
2076 PythonPreference::OnlySystem,
2077 None,
2078 &context.cache,
2079 )
2080 })??;
2081
2082 assert_eq!(
2083 python.interpreter().python_full_version().to_string(),
2084 "3.12.0",
2085 "We should prefer the discovered virtual environment over available system versions"
2086 );
2087
2088 Ok(())
2089 }
2090
2091 #[test]
2092 fn find_python_skips_broken_active_python() -> Result<()> {
2093 let context = TestContext::new()?;
2094 let venv = context.tempdir.child(".venv");
2095 TestContext::mock_venv(&venv, "3.12.0")?;
2096
2097 fs_err::remove_file(venv.join("pyvenv.cfg"))?;
2099
2100 let python =
2101 context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
2102 find_python_installation(
2103 &PythonRequest::Default,
2104 EnvironmentPreference::Any,
2105 PythonPreference::OnlySystem,
2106 None,
2107 &context.cache,
2108 )
2109 })??;
2110 assert_eq!(
2111 python.interpreter().python_full_version().to_string(),
2112 "3.12.0",
2113 "We should prefer the active environment"
2115 );
2116
2117 Ok(())
2118 }
2119
2120 #[test]
2121 fn find_python_from_parent_interpreter() -> Result<()> {
2122 let mut context = TestContext::new()?;
2123
2124 let parent = context.tempdir.child("python").to_path_buf();
2125 TestContext::create_mock_interpreter(
2126 &parent,
2127 &PythonVersion::from_str("3.12.0").unwrap(),
2128 ImplementationName::CPython,
2129 true,
2131 false,
2132 )?;
2133
2134 let python = context.run_with_vars(
2135 &[(
2136 EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
2137 Some(parent.as_os_str()),
2138 )],
2139 || {
2140 find_python_installation(
2141 &PythonRequest::Default,
2142 EnvironmentPreference::Any,
2143 PythonPreference::OnlySystem,
2144 None,
2145 &context.cache,
2146 )
2147 },
2148 )??;
2149 assert_eq!(
2150 python.interpreter().python_full_version().to_string(),
2151 "3.12.0",
2152 "We should find the parent interpreter"
2153 );
2154
2155 let venv = context.tempdir.child(".venv");
2157 TestContext::mock_venv(&venv, "3.12.2")?;
2158 context.add_python_versions(&["3.12.3"])?;
2159 let python = context.run_with_vars(
2160 &[
2161 (
2162 EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
2163 Some(parent.as_os_str()),
2164 ),
2165 (EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())),
2166 ],
2167 || {
2168 find_python_installation(
2169 &PythonRequest::Default,
2170 EnvironmentPreference::Any,
2171 PythonPreference::OnlySystem,
2172 None,
2173 &context.cache,
2174 )
2175 },
2176 )??;
2177 assert_eq!(
2178 python.interpreter().python_full_version().to_string(),
2179 "3.12.0",
2180 "We should prefer the parent interpreter"
2181 );
2182
2183 let python = context.run_with_vars(
2185 &[
2186 (
2187 EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
2188 Some(parent.as_os_str()),
2189 ),
2190 (EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())),
2191 ],
2192 || {
2193 find_python_installation(
2194 &PythonRequest::Default,
2195 EnvironmentPreference::ExplicitSystem,
2196 PythonPreference::OnlySystem,
2197 None,
2198 &context.cache,
2199 )
2200 },
2201 )??;
2202 assert_eq!(
2203 python.interpreter().python_full_version().to_string(),
2204 "3.12.0",
2205 "We should prefer the parent interpreter"
2206 );
2207
2208 let python = context.run_with_vars(
2210 &[
2211 (
2212 EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
2213 Some(parent.as_os_str()),
2214 ),
2215 (EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())),
2216 ],
2217 || {
2218 find_python_installation(
2219 &PythonRequest::Default,
2220 EnvironmentPreference::OnlySystem,
2221 PythonPreference::OnlySystem,
2222 None,
2223 &context.cache,
2224 )
2225 },
2226 )??;
2227 assert_eq!(
2228 python.interpreter().python_full_version().to_string(),
2229 "3.12.0",
2230 "We should prefer the parent interpreter since it's not virtual"
2231 );
2232
2233 let python = context.run_with_vars(
2235 &[
2236 (
2237 EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
2238 Some(parent.as_os_str()),
2239 ),
2240 (EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())),
2241 ],
2242 || {
2243 find_python_installation(
2244 &PythonRequest::Default,
2245 EnvironmentPreference::OnlyVirtual,
2246 PythonPreference::OnlySystem,
2247 None,
2248 &context.cache,
2249 )
2250 },
2251 )??;
2252 assert_eq!(
2253 python.interpreter().python_full_version().to_string(),
2254 "3.12.2",
2255 "We find the virtual environment Python because a system is explicitly not allowed"
2256 );
2257
2258 Ok(())
2259 }
2260
2261 #[test]
2262 fn find_python_from_parent_interpreter_prerelease() -> Result<()> {
2263 let mut context = TestContext::new()?;
2264 context.add_python_versions(&["3.12.0"])?;
2265 let parent = context.tempdir.child("python").to_path_buf();
2266 TestContext::create_mock_interpreter(
2267 &parent,
2268 &PythonVersion::from_str("3.13.0rc2").unwrap(),
2269 ImplementationName::CPython,
2270 true,
2272 false,
2273 )?;
2274
2275 let python = context.run_with_vars(
2276 &[(
2277 EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
2278 Some(parent.as_os_str()),
2279 )],
2280 || {
2281 find_python_installation(
2282 &PythonRequest::Default,
2283 EnvironmentPreference::Any,
2284 PythonPreference::OnlySystem,
2285 None,
2286 &context.cache,
2287 )
2288 },
2289 )??;
2290 assert_eq!(
2291 python.interpreter().python_full_version().to_string(),
2292 "3.13.0rc2",
2293 "We should find the parent interpreter"
2294 );
2295
2296 Ok(())
2297 }
2298
2299 #[test]
2300 fn find_python_active_python_skipped_if_system_required() -> Result<()> {
2301 let mut context = TestContext::new()?;
2302 let venv = context.tempdir.child(".venv");
2303 TestContext::mock_venv(&venv, "3.9.0")?;
2304 context.add_python_versions(&["3.10.0", "3.11.1", "3.12.2"])?;
2305
2306 let python =
2308 context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
2309 find_python_installation(
2310 &PythonRequest::Default,
2311 EnvironmentPreference::OnlySystem,
2312 PythonPreference::OnlySystem,
2313 None,
2314 &context.cache,
2315 )
2316 })??;
2317 assert_eq!(
2318 python.interpreter().python_full_version().to_string(),
2319 "3.10.0",
2320 "We should skip the active environment"
2321 );
2322
2323 let python =
2325 context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
2326 find_python_installation(
2327 &PythonRequest::parse("3.12"),
2328 EnvironmentPreference::OnlySystem,
2329 PythonPreference::OnlySystem,
2330 None,
2331 &context.cache,
2332 )
2333 })??;
2334 assert_eq!(
2335 python.interpreter().python_full_version().to_string(),
2336 "3.12.2",
2337 "We should skip the active environment"
2338 );
2339
2340 let result =
2342 context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
2343 find_python_installation(
2344 &PythonRequest::parse("3.12.3"),
2345 EnvironmentPreference::OnlySystem,
2346 PythonPreference::OnlySystem,
2347 None,
2348 &context.cache,
2349 )
2350 })?;
2351 assert!(
2352 result.is_err(),
2353 "We should not find an python; got {result:?}"
2354 );
2355
2356 Ok(())
2357 }
2358
2359 #[test]
2360 fn find_python_fails_if_no_virtualenv_and_system_not_allowed() -> Result<()> {
2361 let mut context = TestContext::new()?;
2362 context.add_python_versions(&["3.10.1", "3.11.2"])?;
2363
2364 let result = context.run(|| {
2365 find_python_installation(
2366 &PythonRequest::Default,
2367 EnvironmentPreference::OnlyVirtual,
2368 PythonPreference::OnlySystem,
2369 None,
2370 &context.cache,
2371 )
2372 })?;
2373 assert_matches!(
2374 result,
2375 Err(PythonNotFound { .. }),
2376 "We should not find an python; got {result:?}"
2377 );
2378
2379 let result = context.run_with_vars(
2381 &[(EnvVars::VIRTUAL_ENV, Some(context.tempdir.as_os_str()))],
2382 || {
2383 find_python_installation(
2384 &PythonRequest::parse("3.12.3"),
2385 EnvironmentPreference::OnlySystem,
2386 PythonPreference::OnlySystem,
2387 None,
2388 &context.cache,
2389 )
2390 },
2391 )?;
2392 assert_matches!(
2393 result,
2394 Err(PythonNotFound { .. }),
2395 "We should not find an python; got {result:?}"
2396 );
2397 Ok(())
2398 }
2399
2400 #[cfg(unix)]
2401 #[test]
2402 fn find_python_does_not_traverse_broken_child_virtualenv() -> Result<()> {
2403 let context = TestContext::new()?;
2404
2405 let parent_venv = context.tempdir.child(".venv");
2406 TestContext::mock_venv(&parent_venv, "3.12.0")?;
2407 let child_venv = context.workdir.child(".venv");
2408 fs_err::os::unix::fs::symlink(context.workdir.child("missing"), &child_venv)?;
2409
2410 let result = context.run(|| {
2411 find_python_installation(
2412 &PythonRequest::Default,
2413 EnvironmentPreference::OnlyVirtual,
2414 PythonPreference::OnlySystem,
2415 None,
2416 &context.cache,
2417 )
2418 });
2419 assert_matches!(
2420 &result,
2421 Err(discovery::Error::VirtualEnv(
2422 crate::virtualenv::Error::MissingPyVenvCfg(path)
2423 )) if path == child_venv.path()
2424 ,
2425 "A broken symlink at `.venv` should be eagerly rejected; got {result:?}"
2426 );
2427
2428 Ok(())
2429 }
2430
2431 #[test]
2432 fn find_python_propagates_virtualenv_metadata_errors() -> Result<()> {
2433 let context = TestContext::new()?;
2434
2435 let permissions = fs_err::metadata(&context.workdir)?.permissions();
2436 fs_err::set_permissions(&context.workdir, Permissions::from_mode(0o000))?;
2437 let result = context.run(|| {
2438 find_python_installation(
2439 &PythonRequest::Default,
2440 EnvironmentPreference::OnlyVirtual,
2441 PythonPreference::OnlySystem,
2442 None,
2443 &context.cache,
2444 )
2445 });
2446 fs_err::set_permissions(&context.workdir, permissions)?;
2447
2448 assert_matches!(
2449 &result,
2450 Err(discovery::Error::VirtualEnv(crate::virtualenv::Error::Io(error)))
2451 if error.kind() == io::ErrorKind::PermissionDenied
2452 ,
2453 "A virtual environment metadata error should not be ignored; got {result:?}"
2454 );
2455
2456 Ok(())
2457 }
2458
2459 #[test]
2460 fn find_python_allows_name_in_working_directory() -> Result<()> {
2461 let context = TestContext::new()?;
2462 context.add_python_to_workdir("foobar", "3.10.0")?;
2463
2464 let python = context.run(|| {
2465 find_python_installation(
2466 &PythonRequest::parse("foobar"),
2467 EnvironmentPreference::Any,
2468 PythonPreference::OnlySystem,
2469 None,
2470 &context.cache,
2471 )
2472 })??;
2473 assert_eq!(
2474 python.interpreter().python_full_version().to_string(),
2475 "3.10.0",
2476 "We should find the named executable"
2477 );
2478
2479 let result = context.run(|| {
2480 find_python_installation(
2481 &PythonRequest::Default,
2482 EnvironmentPreference::Any,
2483 PythonPreference::OnlySystem,
2484 None,
2485 &context.cache,
2486 )
2487 })?;
2488 assert_matches!(
2489 result,
2490 Err(PythonNotFound { .. }),
2491 "We should not find it without a specific request"
2492 );
2493
2494 let result = context.run(|| {
2495 find_python_installation(
2496 &PythonRequest::parse("3.10.0"),
2497 EnvironmentPreference::Any,
2498 PythonPreference::OnlySystem,
2499 None,
2500 &context.cache,
2501 )
2502 })?;
2503 assert_matches!(
2504 result,
2505 Err(PythonNotFound { .. }),
2506 "We should not find it via a matching version request"
2507 );
2508
2509 Ok(())
2510 }
2511
2512 #[test]
2513 fn find_python_allows_relative_file_path() -> Result<()> {
2514 let mut context = TestContext::new()?;
2515 let python = context.workdir.child("foo").join("bar");
2516 TestContext::create_mock_interpreter(
2517 &python,
2518 &PythonVersion::from_str("3.10.0").unwrap(),
2519 ImplementationName::default(),
2520 true,
2521 false,
2522 )?;
2523
2524 let python = context.run(|| {
2525 find_python_installation(
2526 &PythonRequest::parse("./foo/bar"),
2527 EnvironmentPreference::Any,
2528 PythonPreference::OnlySystem,
2529 None,
2530 &context.cache,
2531 )
2532 })??;
2533 assert_eq!(
2534 python.interpreter().python_full_version().to_string(),
2535 "3.10.0",
2536 "We should find the `bar` executable"
2537 );
2538
2539 context.add_python_versions(&["3.11.1"])?;
2540 let python = context.run(|| {
2541 find_python_installation(
2542 &PythonRequest::parse("./foo/bar"),
2543 EnvironmentPreference::Any,
2544 PythonPreference::OnlySystem,
2545 None,
2546 &context.cache,
2547 )
2548 })??;
2549 assert_eq!(
2550 python.interpreter().python_full_version().to_string(),
2551 "3.10.0",
2552 "We should prefer the `bar` executable over the system and virtualenvs"
2553 );
2554
2555 Ok(())
2556 }
2557
2558 #[test]
2559 fn find_python_allows_absolute_file_path() -> Result<()> {
2560 let mut context = TestContext::new()?;
2561 let python_path = context.tempdir.child("foo").join("bar");
2562 TestContext::create_mock_interpreter(
2563 &python_path,
2564 &PythonVersion::from_str("3.10.0").unwrap(),
2565 ImplementationName::default(),
2566 true,
2567 false,
2568 )?;
2569
2570 let python = context.run(|| {
2571 find_python_installation(
2572 &PythonRequest::parse(python_path.to_str().unwrap()),
2573 EnvironmentPreference::Any,
2574 PythonPreference::OnlySystem,
2575 None,
2576 &context.cache,
2577 )
2578 })??;
2579 assert_eq!(
2580 python.interpreter().python_full_version().to_string(),
2581 "3.10.0",
2582 "We should find the `bar` executable"
2583 );
2584
2585 let python = context.run(|| {
2587 find_python_installation(
2588 &PythonRequest::parse(python_path.to_str().unwrap()),
2589 EnvironmentPreference::ExplicitSystem,
2590 PythonPreference::OnlySystem,
2591 None,
2592 &context.cache,
2593 )
2594 })??;
2595 assert_eq!(
2596 python.interpreter().python_full_version().to_string(),
2597 "3.10.0",
2598 "We should allow the `bar` executable with explicit system"
2599 );
2600
2601 let python = context.run(|| {
2603 find_python_installation(
2604 &PythonRequest::parse(python_path.to_str().unwrap()),
2605 EnvironmentPreference::OnlyVirtual,
2606 PythonPreference::OnlySystem,
2607 None,
2608 &context.cache,
2609 )
2610 })??;
2611 assert_eq!(
2612 python.interpreter().python_full_version().to_string(),
2613 "3.10.0",
2614 "We should allow the `bar` executable and verify it is virtual"
2615 );
2616
2617 context.add_python_versions(&["3.11.1"])?;
2618 let python = context.run(|| {
2619 find_python_installation(
2620 &PythonRequest::parse(python_path.to_str().unwrap()),
2621 EnvironmentPreference::Any,
2622 PythonPreference::OnlySystem,
2623 None,
2624 &context.cache,
2625 )
2626 })??;
2627 assert_eq!(
2628 python.interpreter().python_full_version().to_string(),
2629 "3.10.0",
2630 "We should prefer the `bar` executable over the system and virtualenvs"
2631 );
2632
2633 Ok(())
2634 }
2635
2636 #[test]
2637 fn find_python_allows_venv_directory_path() -> Result<()> {
2638 let mut context = TestContext::new()?;
2639
2640 let venv = context.tempdir.child("foo").child(".venv");
2641 TestContext::mock_venv(&venv, "3.10.0")?;
2642 let python = context.run(|| {
2643 find_python_installation(
2644 &PythonRequest::parse("../foo/.venv"),
2645 EnvironmentPreference::Any,
2646 PythonPreference::OnlySystem,
2647 None,
2648 &context.cache,
2649 )
2650 })??;
2651 assert_eq!(
2652 python.interpreter().python_full_version().to_string(),
2653 "3.10.0",
2654 "We should find the relative venv path"
2655 );
2656
2657 let python = context.run(|| {
2658 find_python_installation(
2659 &PythonRequest::parse(venv.to_str().unwrap()),
2660 EnvironmentPreference::Any,
2661 PythonPreference::OnlySystem,
2662 None,
2663 &context.cache,
2664 )
2665 })??;
2666 assert_eq!(
2667 python.interpreter().python_full_version().to_string(),
2668 "3.10.0",
2669 "We should find the absolute venv path"
2670 );
2671
2672 let python_path = context.tempdir.child("bar").join("bin").join("python");
2674 TestContext::create_mock_interpreter(
2675 &python_path,
2676 &PythonVersion::from_str("3.10.0").unwrap(),
2677 ImplementationName::default(),
2678 true,
2679 false,
2680 )?;
2681 let python = context.run(|| {
2682 find_python_installation(
2683 &PythonRequest::parse(context.tempdir.child("bar").to_str().unwrap()),
2684 EnvironmentPreference::Any,
2685 PythonPreference::OnlySystem,
2686 None,
2687 &context.cache,
2688 )
2689 })??;
2690 assert_eq!(
2691 python.interpreter().python_full_version().to_string(),
2692 "3.10.0",
2693 "We should find the executable in the directory"
2694 );
2695
2696 let other_venv = context.tempdir.child("foobar").child(".venv");
2697 TestContext::mock_venv(&other_venv, "3.11.1")?;
2698 context.add_python_versions(&["3.12.2"])?;
2699 let python = context.run_with_vars(
2700 &[(EnvVars::VIRTUAL_ENV, Some(other_venv.as_os_str()))],
2701 || {
2702 find_python_installation(
2703 &PythonRequest::parse(venv.to_str().unwrap()),
2704 EnvironmentPreference::Any,
2705 PythonPreference::OnlySystem,
2706 None,
2707 &context.cache,
2708 )
2709 },
2710 )??;
2711 assert_eq!(
2712 python.interpreter().python_full_version().to_string(),
2713 "3.10.0",
2714 "We should prefer the requested directory over the system and active virtual environments"
2715 );
2716
2717 Ok(())
2718 }
2719
2720 #[test]
2721 fn find_python_venv_symlink() -> Result<()> {
2722 let context = TestContext::new()?;
2723
2724 let venv = context.tempdir.child("target").child("env");
2725 TestContext::mock_venv(&venv, "3.10.6")?;
2726 let symlink = context.tempdir.child("proj").child(".venv");
2727 context.tempdir.child("proj").create_dir_all()?;
2728 symlink.symlink_to_dir(venv)?;
2729
2730 let python = context.run(|| {
2731 find_python_installation(
2732 &PythonRequest::parse("../proj/.venv"),
2733 EnvironmentPreference::Any,
2734 PythonPreference::OnlySystem,
2735 None,
2736 &context.cache,
2737 )
2738 })??;
2739 assert_eq!(
2740 python.interpreter().python_full_version().to_string(),
2741 "3.10.6",
2742 "We should find the symlinked venv"
2743 );
2744 Ok(())
2745 }
2746
2747 #[test]
2748 fn find_python_treats_missing_file_path_as_file() -> Result<()> {
2749 let context = TestContext::new()?;
2750 context.workdir.child("foo").create_dir_all()?;
2751
2752 let result = context.run(|| {
2753 find_python_installation(
2754 &PythonRequest::parse("./foo/bar"),
2755 EnvironmentPreference::Any,
2756 PythonPreference::OnlySystem,
2757 None,
2758 &context.cache,
2759 )
2760 })?;
2761 assert_matches!(
2762 result,
2763 Err(PythonNotFound { .. }),
2764 "We should not find the file; got {result:?}"
2765 );
2766
2767 Ok(())
2768 }
2769
2770 #[test]
2771 fn find_python_executable_name_in_search_path() -> Result<()> {
2772 let mut context = TestContext::new()?;
2773 let python = context.tempdir.child("foo").join("bar");
2774 TestContext::create_mock_interpreter(
2775 &python,
2776 &PythonVersion::from_str("3.10.0").unwrap(),
2777 ImplementationName::default(),
2778 true,
2779 false,
2780 )?;
2781 context.add_to_search_path(context.tempdir.child("foo").to_path_buf());
2782
2783 let python = context.run(|| {
2784 find_python_installation(
2785 &PythonRequest::parse("bar"),
2786 EnvironmentPreference::Any,
2787 PythonPreference::OnlySystem,
2788 None,
2789 &context.cache,
2790 )
2791 })??;
2792 assert_eq!(
2793 python.interpreter().python_full_version().to_string(),
2794 "3.10.0",
2795 "We should find the `bar` executable"
2796 );
2797
2798 let result = context.run(|| {
2800 find_python_installation(
2801 &PythonRequest::parse("bar"),
2802 EnvironmentPreference::ExplicitSystem,
2803 PythonPreference::OnlySystem,
2804 None,
2805 &context.cache,
2806 )
2807 })?;
2808 assert_matches!(
2809 result,
2810 Err(PythonNotFound { .. }),
2811 "We should not allow a system interpreter; got {result:?}"
2812 );
2813
2814 let mut context = TestContext::new()?;
2816 let python = context.tempdir.child("foo").join("bar");
2817 TestContext::create_mock_interpreter(
2818 &python,
2819 &PythonVersion::from_str("3.10.0").unwrap(),
2820 ImplementationName::default(),
2821 false, false,
2823 )?;
2824 context.add_to_search_path(context.tempdir.child("foo").to_path_buf());
2825
2826 let python = context
2827 .run(|| {
2828 find_python_installation(
2829 &PythonRequest::parse("bar"),
2830 EnvironmentPreference::ExplicitSystem,
2831 PythonPreference::OnlySystem,
2832 None,
2833 &context.cache,
2834 )
2835 })
2836 .unwrap()
2837 .unwrap();
2838 assert_eq!(
2839 python.interpreter().python_full_version().to_string(),
2840 "3.10.0",
2841 "We should find the `bar` executable"
2842 );
2843
2844 Ok(())
2845 }
2846
2847 #[test]
2848 fn find_python_pypy() -> Result<()> {
2849 let mut context = TestContext::new()?;
2850
2851 context.add_python_interpreters(&[(true, ImplementationName::PyPy, "pypy", "3.10.0")])?;
2852 let result = context.run(|| {
2853 find_python_installation(
2854 &PythonRequest::Default,
2855 EnvironmentPreference::Any,
2856 PythonPreference::OnlySystem,
2857 None,
2858 &context.cache,
2859 )
2860 })?;
2861 assert_matches!(
2862 result,
2863 Err(PythonNotFound { .. }),
2864 "We should not find the pypy interpreter if not named `python` or requested; got {result:?}"
2865 );
2866
2867 context.reset_search_path();
2869 context.add_python_interpreters(&[(true, ImplementationName::PyPy, "python", "3.10.1")])?;
2870 let python = context.run(|| {
2871 find_python_installation(
2872 &PythonRequest::Default,
2873 EnvironmentPreference::Any,
2874 PythonPreference::OnlySystem,
2875 None,
2876 &context.cache,
2877 )
2878 })??;
2879 assert_eq!(
2880 python.interpreter().python_full_version().to_string(),
2881 "3.10.1",
2882 "We should find the pypy interpreter if it's the only one"
2883 );
2884
2885 let python = context.run(|| {
2886 find_python_installation(
2887 &PythonRequest::parse("pypy"),
2888 EnvironmentPreference::Any,
2889 PythonPreference::OnlySystem,
2890 None,
2891 &context.cache,
2892 )
2893 })??;
2894 assert_eq!(
2895 python.interpreter().python_full_version().to_string(),
2896 "3.10.1",
2897 "We should find the pypy interpreter if it's requested"
2898 );
2899
2900 Ok(())
2901 }
2902
2903 #[test]
2904 fn find_python_pypy_request_ignores_cpython() -> Result<()> {
2905 let mut context = TestContext::new()?;
2906 context.add_python_interpreters(&[
2907 (true, ImplementationName::CPython, "python", "3.10.0"),
2908 (true, ImplementationName::PyPy, "pypy", "3.10.1"),
2909 ])?;
2910
2911 let python = context.run(|| {
2912 find_python_installation(
2913 &PythonRequest::parse("pypy"),
2914 EnvironmentPreference::Any,
2915 PythonPreference::OnlySystem,
2916 None,
2917 &context.cache,
2918 )
2919 })??;
2920 assert_eq!(
2921 python.interpreter().python_full_version().to_string(),
2922 "3.10.1",
2923 "We should skip the CPython interpreter"
2924 );
2925
2926 let python = context.run(|| {
2927 find_python_installation(
2928 &PythonRequest::Default,
2929 EnvironmentPreference::Any,
2930 PythonPreference::OnlySystem,
2931 None,
2932 &context.cache,
2933 )
2934 })??;
2935 assert_eq!(
2936 python.interpreter().python_full_version().to_string(),
2937 "3.10.0",
2938 "We should take the first interpreter without a specific request"
2939 );
2940
2941 Ok(())
2942 }
2943
2944 #[test]
2945 fn find_python_pypy_request_skips_wrong_versions() -> Result<()> {
2946 let mut context = TestContext::new()?;
2947 context.add_python_interpreters(&[
2948 (true, ImplementationName::PyPy, "pypy", "3.9"),
2949 (true, ImplementationName::PyPy, "pypy", "3.10.1"),
2950 ])?;
2951
2952 let python = context.run(|| {
2953 find_python_installation(
2954 &PythonRequest::parse("pypy3.10"),
2955 EnvironmentPreference::Any,
2956 PythonPreference::OnlySystem,
2957 None,
2958 &context.cache,
2959 )
2960 })??;
2961 assert_eq!(
2962 python.interpreter().python_full_version().to_string(),
2963 "3.10.1",
2964 "We should skip the first interpreter"
2965 );
2966
2967 Ok(())
2968 }
2969
2970 #[test]
2971 fn find_python_pypy_finds_executable_with_version_name() -> Result<()> {
2972 let mut context = TestContext::new()?;
2973 context.add_python_interpreters(&[
2974 (true, ImplementationName::PyPy, "pypy3.9", "3.10.0"), (true, ImplementationName::PyPy, "pypy3.10", "3.10.1"),
2976 (true, ImplementationName::PyPy, "pypy", "3.10.2"),
2977 ])?;
2978
2979 let python = context.run(|| {
2980 find_python_installation(
2981 &PythonRequest::parse("pypy@3.10"),
2982 EnvironmentPreference::Any,
2983 PythonPreference::OnlySystem,
2984 None,
2985 &context.cache,
2986 )
2987 })??;
2988 assert_eq!(
2989 python.interpreter().python_full_version().to_string(),
2990 "3.10.1",
2991 "We should find the requested interpreter version"
2992 );
2993
2994 Ok(())
2995 }
2996
2997 #[test]
2998 fn find_python_all_minors() -> Result<()> {
2999 let mut context = TestContext::new()?;
3000 context.add_python_interpreters(&[
3001 (true, ImplementationName::CPython, "python", "3.10.0"),
3002 (true, ImplementationName::CPython, "python3", "3.10.0"),
3003 (true, ImplementationName::CPython, "python3.12", "3.12.0"),
3004 ])?;
3005
3006 let python = context.run(|| {
3007 find_python_installation(
3008 &PythonRequest::parse(">= 3.11"),
3009 EnvironmentPreference::Any,
3010 PythonPreference::OnlySystem,
3011 None,
3012 &context.cache,
3013 )
3014 })??;
3015 assert_eq!(
3016 python.interpreter().python_full_version().to_string(),
3017 "3.12.0",
3018 "We should find matching minor version even if they aren't called `python` or `python3`"
3019 );
3020
3021 Ok(())
3022 }
3023
3024 #[test]
3025 fn find_python_all_minors_prerelease() -> Result<()> {
3026 let mut context = TestContext::new()?;
3027 context.add_python_interpreters(&[
3028 (true, ImplementationName::CPython, "python", "3.10.0"),
3029 (true, ImplementationName::CPython, "python3", "3.10.0"),
3030 (true, ImplementationName::CPython, "python3.11", "3.11.0b0"),
3031 ])?;
3032
3033 let python = context.run(|| {
3034 find_python_installation(
3035 &PythonRequest::parse(">= 3.11"),
3036 EnvironmentPreference::Any,
3037 PythonPreference::OnlySystem,
3038 None,
3039 &context.cache,
3040 )
3041 })??;
3042 assert_eq!(
3043 python.interpreter().python_full_version().to_string(),
3044 "3.11.0b0",
3045 "We should find the 3.11 prerelease even though >=3.11 would normally exclude prereleases"
3046 );
3047
3048 Ok(())
3049 }
3050
3051 #[test]
3052 fn find_python_all_minors_prerelease_next() -> Result<()> {
3053 let mut context = TestContext::new()?;
3054 context.add_python_interpreters(&[
3055 (true, ImplementationName::CPython, "python", "3.10.0"),
3056 (true, ImplementationName::CPython, "python3", "3.10.0"),
3057 (true, ImplementationName::CPython, "python3.12", "3.12.0b0"),
3058 ])?;
3059
3060 let python = context.run(|| {
3061 find_python_installation(
3062 &PythonRequest::parse(">= 3.11"),
3063 EnvironmentPreference::Any,
3064 PythonPreference::OnlySystem,
3065 None,
3066 &context.cache,
3067 )
3068 })??;
3069 assert_eq!(
3070 python.interpreter().python_full_version().to_string(),
3071 "3.12.0b0",
3072 "We should find the 3.12 prerelease"
3073 );
3074
3075 Ok(())
3076 }
3077
3078 #[test]
3079 fn find_python_graalpy() -> Result<()> {
3080 let mut context = TestContext::new()?;
3081
3082 context.add_python_interpreters(&[(
3083 true,
3084 ImplementationName::GraalPy,
3085 "graalpy",
3086 "3.10.0",
3087 )])?;
3088 let result = context.run(|| {
3089 find_python_installation(
3090 &PythonRequest::Default,
3091 EnvironmentPreference::Any,
3092 PythonPreference::OnlySystem,
3093 None,
3094 &context.cache,
3095 )
3096 })?;
3097 assert_matches!(
3098 result,
3099 Err(PythonNotFound { .. }),
3100 "We should not the graalpy interpreter if not named `python` or requested; got {result:?}"
3101 );
3102
3103 context.reset_search_path();
3105 context.add_python_interpreters(&[(
3106 true,
3107 ImplementationName::GraalPy,
3108 "python",
3109 "3.10.1",
3110 )])?;
3111 let python = context.run(|| {
3112 find_python_installation(
3113 &PythonRequest::Default,
3114 EnvironmentPreference::Any,
3115 PythonPreference::OnlySystem,
3116 None,
3117 &context.cache,
3118 )
3119 })??;
3120 assert_eq!(
3121 python.interpreter().python_full_version().to_string(),
3122 "3.10.1",
3123 "We should find the graalpy interpreter if it's the only one"
3124 );
3125
3126 let python = context.run(|| {
3127 find_python_installation(
3128 &PythonRequest::parse("graalpy"),
3129 EnvironmentPreference::Any,
3130 PythonPreference::OnlySystem,
3131 None,
3132 &context.cache,
3133 )
3134 })??;
3135 assert_eq!(
3136 python.interpreter().python_full_version().to_string(),
3137 "3.10.1",
3138 "We should find the graalpy interpreter if it's requested"
3139 );
3140
3141 Ok(())
3142 }
3143
3144 #[test]
3145 fn find_python_graalpy_request_ignores_cpython() -> Result<()> {
3146 let mut context = TestContext::new()?;
3147 context.add_python_interpreters(&[
3148 (true, ImplementationName::CPython, "python", "3.10.0"),
3149 (true, ImplementationName::GraalPy, "graalpy", "3.10.1"),
3150 ])?;
3151
3152 let python = context.run(|| {
3153 find_python_installation(
3154 &PythonRequest::parse("graalpy"),
3155 EnvironmentPreference::Any,
3156 PythonPreference::OnlySystem,
3157 None,
3158 &context.cache,
3159 )
3160 })??;
3161 assert_eq!(
3162 python.interpreter().python_full_version().to_string(),
3163 "3.10.1",
3164 "We should skip the CPython interpreter"
3165 );
3166
3167 let python = context.run(|| {
3168 find_python_installation(
3169 &PythonRequest::Default,
3170 EnvironmentPreference::Any,
3171 PythonPreference::OnlySystem,
3172 None,
3173 &context.cache,
3174 )
3175 })??;
3176 assert_eq!(
3177 python.interpreter().python_full_version().to_string(),
3178 "3.10.0",
3179 "We should take the first interpreter without a specific request"
3180 );
3181
3182 Ok(())
3183 }
3184
3185 #[test]
3186 fn find_python_executable_name_preference() -> Result<()> {
3187 let mut context = TestContext::new()?;
3188 TestContext::create_mock_interpreter(
3189 &context.tempdir.join("pypy3.10"),
3190 &PythonVersion::from_str("3.10.0").unwrap(),
3191 ImplementationName::PyPy,
3192 true,
3193 false,
3194 )?;
3195 TestContext::create_mock_interpreter(
3196 &context.tempdir.join("pypy"),
3197 &PythonVersion::from_str("3.10.1").unwrap(),
3198 ImplementationName::PyPy,
3199 true,
3200 false,
3201 )?;
3202 context.add_to_search_path(context.tempdir.to_path_buf());
3203
3204 let python = context
3205 .run(|| {
3206 find_python_installation(
3207 &PythonRequest::parse("pypy@3.10"),
3208 EnvironmentPreference::Any,
3209 PythonPreference::OnlySystem,
3210 None,
3211 &context.cache,
3212 )
3213 })
3214 .unwrap()
3215 .unwrap();
3216 assert_eq!(
3217 python.interpreter().python_full_version().to_string(),
3218 "3.10.0",
3219 "We should prefer the versioned one when a version is requested"
3220 );
3221
3222 let python = context
3223 .run(|| {
3224 find_python_installation(
3225 &PythonRequest::parse("pypy"),
3226 EnvironmentPreference::Any,
3227 PythonPreference::OnlySystem,
3228 None,
3229 &context.cache,
3230 )
3231 })
3232 .unwrap()
3233 .unwrap();
3234 assert_eq!(
3235 python.interpreter().python_full_version().to_string(),
3236 "3.10.1",
3237 "We should prefer the generic one when no version is requested"
3238 );
3239
3240 let mut context = TestContext::new()?;
3241 TestContext::create_mock_interpreter(
3242 &context.tempdir.join("python3.10"),
3243 &PythonVersion::from_str("3.10.0").unwrap(),
3244 ImplementationName::PyPy,
3245 true,
3246 false,
3247 )?;
3248 TestContext::create_mock_interpreter(
3249 &context.tempdir.join("pypy"),
3250 &PythonVersion::from_str("3.10.1").unwrap(),
3251 ImplementationName::PyPy,
3252 true,
3253 false,
3254 )?;
3255 TestContext::create_mock_interpreter(
3256 &context.tempdir.join("python"),
3257 &PythonVersion::from_str("3.10.2").unwrap(),
3258 ImplementationName::PyPy,
3259 true,
3260 false,
3261 )?;
3262 context.add_to_search_path(context.tempdir.to_path_buf());
3263
3264 let python = context
3265 .run(|| {
3266 find_python_installation(
3267 &PythonRequest::parse("pypy@3.10"),
3268 EnvironmentPreference::Any,
3269 PythonPreference::OnlySystem,
3270 None,
3271 &context.cache,
3272 )
3273 })
3274 .unwrap()
3275 .unwrap();
3276 assert_eq!(
3277 python.interpreter().python_full_version().to_string(),
3278 "3.10.1",
3279 "We should prefer the implementation name over the generic name"
3280 );
3281
3282 let python = context
3283 .run(|| {
3284 find_python_installation(
3285 &PythonRequest::parse("default"),
3286 EnvironmentPreference::Any,
3287 PythonPreference::OnlySystem,
3288 None,
3289 &context.cache,
3290 )
3291 })
3292 .unwrap()
3293 .unwrap();
3294 assert_eq!(
3295 python.interpreter().python_full_version().to_string(),
3296 "3.10.2",
3297 "We should prefer the generic name over the implementation name, but not the versioned name"
3298 );
3299
3300 let mut context = TestContext::new()?;
3303 TestContext::create_mock_interpreter(
3304 &context.tempdir.join("python"),
3305 &PythonVersion::from_str("3.10.0").unwrap(),
3306 ImplementationName::GraalPy,
3307 true,
3308 false,
3309 )?;
3310 TestContext::create_mock_interpreter(
3311 &context.tempdir.join("graalpy"),
3312 &PythonVersion::from_str("3.10.1").unwrap(),
3313 ImplementationName::GraalPy,
3314 true,
3315 false,
3316 )?;
3317 context.add_to_search_path(context.tempdir.to_path_buf());
3318
3319 let python = context
3320 .run(|| {
3321 find_python_installation(
3322 &PythonRequest::parse("graalpy@3.10"),
3323 EnvironmentPreference::Any,
3324 PythonPreference::OnlySystem,
3325 None,
3326 &context.cache,
3327 )
3328 })
3329 .unwrap()
3330 .unwrap();
3331 assert_eq!(
3332 python.interpreter().python_full_version().to_string(),
3333 "3.10.1",
3334 );
3335
3336 context.reset_search_path();
3338 context.add_python_interpreters(&[
3339 (true, ImplementationName::GraalPy, "python", "3.10.2"),
3340 (true, ImplementationName::GraalPy, "graalpy", "3.10.3"),
3341 ])?;
3342 let python = context
3343 .run(|| {
3344 find_python_installation(
3345 &PythonRequest::parse("graalpy@3.10"),
3346 EnvironmentPreference::Any,
3347 PythonPreference::OnlySystem,
3348 None,
3349 &context.cache,
3350 )
3351 })
3352 .unwrap()
3353 .unwrap();
3354 assert_eq!(
3355 python.interpreter().python_full_version().to_string(),
3356 "3.10.2",
3357 );
3358
3359 context.reset_search_path();
3361 context.add_python_interpreters(&[
3362 (true, ImplementationName::GraalPy, "graalpy", "3.10.3"),
3363 (true, ImplementationName::GraalPy, "python", "3.10.2"),
3364 ])?;
3365 let python = context
3366 .run(|| {
3367 find_python_installation(
3368 &PythonRequest::parse("graalpy@3.10"),
3369 EnvironmentPreference::Any,
3370 PythonPreference::OnlySystem,
3371 None,
3372 &context.cache,
3373 )
3374 })
3375 .unwrap()
3376 .unwrap();
3377 assert_eq!(
3378 python.interpreter().python_full_version().to_string(),
3379 "3.10.3",
3380 );
3381
3382 Ok(())
3383 }
3384
3385 #[test]
3386 fn find_python_version_free_threaded() -> Result<()> {
3387 let mut context = TestContext::new()?;
3388
3389 TestContext::create_mock_interpreter(
3390 &context.tempdir.join("python"),
3391 &PythonVersion::from_str("3.13.1").unwrap(),
3392 ImplementationName::CPython,
3393 true,
3394 false,
3395 )?;
3396 TestContext::create_mock_interpreter(
3397 &context.tempdir.join("python3.13t"),
3398 &PythonVersion::from_str("3.13.0").unwrap(),
3399 ImplementationName::CPython,
3400 true,
3401 true,
3402 )?;
3403 context.add_to_search_path(context.tempdir.to_path_buf());
3404
3405 let python = context.run(|| {
3406 find_python_installation(
3407 &PythonRequest::parse("3.13t"),
3408 EnvironmentPreference::Any,
3409 PythonPreference::OnlySystem,
3410 None,
3411 &context.cache,
3412 )
3413 })??;
3414
3415 assert_matches!(
3416 python,
3417 PythonInstallation {
3418 source: PythonSource::SearchPathFirst,
3419 interpreter: _
3420 },
3421 "We should find a python; got {python:?}"
3422 );
3423 assert_eq!(
3424 &python.interpreter().python_full_version().to_string(),
3425 "3.13.0",
3426 "We should find the correct interpreter for the request"
3427 );
3428 assert!(
3429 &python.interpreter().gil_disabled(),
3430 "We should find a python without the GIL"
3431 );
3432
3433 Ok(())
3434 }
3435
3436 #[test]
3437 fn find_python_version_prefer_non_free_threaded() -> Result<()> {
3438 let mut context = TestContext::new()?;
3439
3440 TestContext::create_mock_interpreter(
3441 &context.tempdir.join("python"),
3442 &PythonVersion::from_str("3.13.0").unwrap(),
3443 ImplementationName::CPython,
3444 true,
3445 false,
3446 )?;
3447 TestContext::create_mock_interpreter(
3448 &context.tempdir.join("python3.13t"),
3449 &PythonVersion::from_str("3.13.0").unwrap(),
3450 ImplementationName::CPython,
3451 true,
3452 true,
3453 )?;
3454 context.add_to_search_path(context.tempdir.to_path_buf());
3455
3456 let python = context.run(|| {
3457 find_python_installation(
3458 &PythonRequest::parse("3.13"),
3459 EnvironmentPreference::Any,
3460 PythonPreference::OnlySystem,
3461 None,
3462 &context.cache,
3463 )
3464 })??;
3465
3466 assert_matches!(
3467 python,
3468 PythonInstallation {
3469 source: PythonSource::SearchPathFirst,
3470 interpreter: _
3471 },
3472 "We should find a python; got {python:?}"
3473 );
3474 assert_eq!(
3475 &python.interpreter().python_full_version().to_string(),
3476 "3.13.0",
3477 "We should find the correct interpreter for the request"
3478 );
3479 assert!(
3480 !&python.interpreter().gil_disabled(),
3481 "We should prefer a python with the GIL"
3482 );
3483
3484 Ok(())
3485 }
3486
3487 #[test]
3488 fn find_python_pyodide() -> Result<()> {
3489 let mut context = TestContext::new()?;
3490
3491 context.add_pyodide_version("3.13.2")?;
3492
3493 let result = context.run(|| {
3495 find_python_installation(
3496 &PythonRequest::Default,
3497 EnvironmentPreference::Any,
3498 PythonPreference::OnlySystem,
3499 None,
3500 &context.cache,
3501 )
3502 })?;
3503 assert!(
3504 result.is_err(),
3505 "We should not find an python; got {result:?}"
3506 );
3507
3508 let python = context.run(|| {
3510 find_python_installation(
3511 &PythonRequest::Any,
3512 EnvironmentPreference::Any,
3513 PythonPreference::OnlySystem,
3514 None,
3515 &context.cache,
3516 )
3517 })??;
3518 assert_eq!(
3519 python.interpreter().python_full_version().to_string(),
3520 "3.13.2"
3521 );
3522
3523 context.add_python_versions(&["3.15.7"])?;
3525
3526 let python = context.run(|| {
3527 find_python_installation(
3528 &PythonRequest::Default,
3529 EnvironmentPreference::Any,
3530 PythonPreference::OnlySystem,
3531 None,
3532 &context.cache,
3533 )
3534 })??;
3535 assert_eq!(
3536 python.interpreter().python_full_version().to_string(),
3537 "3.15.7"
3538 );
3539
3540 Ok(())
3541 }
3542}