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