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