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