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