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