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