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