diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 83f6d87e44ea..607df133bbb5 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -303,7 +303,7 @@ pub enum Commands { /// interpreters are found by searching for Python executables in the `PATH` environment /// variable. /// - /// On Windows, the `py` launcher is also invoked to find Python executables. + /// On Windows, the registry is also searched for Python executables. /// /// By default, uv will download Python if a version cannot be found. This behavior can be /// disabled with the `--no-python-downloads` flag or the `python-downloads` setting. diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index de4016aaf2bf..be3b5bb77faf 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -133,6 +133,12 @@ pub enum EnvironmentPreference { Any, } +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub(crate) struct DiscoveryPreferences { + python_preference: PythonPreference, + environment_preference: EnvironmentPreference, +} + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum PythonVariant { #[default] @@ -276,7 +282,7 @@ fn python_executables_from_environments<'a>( /// /// - Managed Python installations (e.g. `uv python install`) /// - The search path (i.e. `PATH`) -/// - The `py` launcher (Windows only) +/// - The registry (Windows only) /// /// The ordering and presence of each source is determined by the [`PythonPreference`]. /// @@ -753,6 +759,12 @@ pub fn find_python_installations<'a>( preference: PythonPreference, cache: &'a Cache, ) -> Box> + 'a> { + let sources = DiscoveryPreferences { + python_preference: preference, + environment_preference: environments, + } + .sources(request); + match request { PythonRequest::File(path) => Box::new(std::iter::once({ if preference.allows(PythonSource::ProvidedPath) { @@ -831,7 +843,7 @@ pub fn find_python_installations<'a>( } } PythonRequest::Any => Box::new({ - debug!("Searching for any Python interpreter in {preference}"); + debug!("Searching for any Python interpreter in {sources}"); python_interpreters(&VersionRequest::Any, None, environments, preference, cache).map( |result| { result @@ -841,7 +853,7 @@ pub fn find_python_installations<'a>( ) }), PythonRequest::Default => Box::new({ - debug!("Searching for default Python interpreter in {preference}"); + debug!("Searching for default Python interpreter in {sources}"); python_interpreters( &VersionRequest::Default, None, @@ -860,7 +872,7 @@ pub fn find_python_installations<'a>( return Box::new(std::iter::once(Err(Error::InvalidVersionRequest(err)))); }; Box::new({ - debug!("Searching for {request} in {preference}"); + debug!("Searching for {request} in {sources}"); python_interpreters(version, None, environments, preference, cache).map(|result| { result .map(PythonInstallation::from_tuple) @@ -869,7 +881,7 @@ pub fn find_python_installations<'a>( }) } PythonRequest::Implementation(implementation) => Box::new({ - debug!("Searching for a {request} interpreter in {preference}"); + debug!("Searching for a {request} interpreter in {sources}"); python_interpreters( &VersionRequest::Default, Some(implementation), @@ -894,7 +906,7 @@ pub fn find_python_installations<'a>( return Box::new(std::iter::once(Err(Error::InvalidVersionRequest(err)))); }; Box::new({ - debug!("Searching for {request} in {preference}"); + debug!("Searching for {request} in {sources}"); python_interpreters( version, Some(implementation), @@ -922,7 +934,7 @@ pub fn find_python_installations<'a>( }; }; Box::new({ - debug!("Searching for {request} in {preference}"); + debug!("Searching for {request} in {sources}"); python_interpreters( request.version().unwrap_or(&VersionRequest::Default), request.implementation(), @@ -2256,22 +2268,27 @@ impl fmt::Display for PythonSource { } impl PythonPreference { - /// Return the sources that are considered when searching for a Python interpreter. - fn sources(self) -> &'static [&'static str] { + /// Return the sources that are considered when searching for a Python interpreter with this + /// preference. + fn sources(self) -> &'static [PythonSource] { match self { - Self::OnlyManaged => &["managed installations"], + Self::OnlyManaged => &[PythonSource::Managed], Self::Managed | Self::System => { if cfg!(windows) { - &["managed installations", "system path", "`py` launcher"] + &[ + PythonSource::Managed, + PythonSource::SearchPath, + PythonSource::Registry, + ] } else { - &["managed installations", "system path"] + &[PythonSource::Managed, PythonSource::SearchPath] } } Self::OnlySystem => { if cfg!(windows) { - &["system path", "`py` launcher"] + &[PythonSource::Registry, PythonSource::SearchPath] } else { - &["system path"] + &[PythonSource::SearchPath] } } } @@ -2280,29 +2297,62 @@ impl PythonPreference { impl fmt::Display for PythonPreference { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", conjunction(self.sources())) + f.write_str(match self { + Self::OnlyManaged => "only managed", + Self::Managed => "prefer managed", + Self::System => "prefer system", + Self::OnlySystem => "only system", + }) } } -impl fmt::Display for PythonNotFound { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let sources = match self.environment_preference { +impl DiscoveryPreferences { + /// Return a string describing the sources that are considered when searching for Python with + /// the given preferences. + fn sources(&self, request: &PythonRequest) -> String { + let python_sources = self + .python_preference + .sources() + .iter() + .map(ToString::to_string) + .collect::>(); + match self.environment_preference { EnvironmentPreference::Any => conjunction( &["virtual environments"] .into_iter() - .chain(self.python_preference.sources().iter().copied()) + .chain(python_sources.iter().map(String::as_str)) .collect::>(), ), EnvironmentPreference::ExplicitSystem => { - if self.request.is_explicit_system() { - conjunction(&["virtual", "system environment"]) + if request.is_explicit_system() { + conjunction( + &["virtual environments"] + .into_iter() + .chain(python_sources.iter().map(String::as_str)) + .collect::>(), + ) } else { - conjunction(&["virtual environment"]) + conjunction(&["virtual environments"]) } } - EnvironmentPreference::OnlySystem => conjunction(self.python_preference.sources()), + EnvironmentPreference::OnlySystem => conjunction( + &python_sources + .iter() + .map(String::as_str) + .collect::>(), + ), EnvironmentPreference::OnlyVirtual => conjunction(&["virtual environments"]), - }; + } + } +} + +impl fmt::Display for PythonNotFound { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let sources = DiscoveryPreferences { + python_preference: self.python_preference, + environment_preference: self.environment_preference, + } + .sources(&self.request); match self.request { PythonRequest::Default | PythonRequest::Any => { diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 3aa7df9e94ff..2fd4d4b98b09 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -147,11 +147,11 @@ impl TestContext { #[must_use] pub fn with_filtered_python_sources(mut self) -> Self { self.filters.push(( - "managed installations or system path".to_string(), + "managed installations or search path".to_string(), "[PYTHON SOURCES]".to_string(), )); self.filters.push(( - "managed installations, system path, or `py` launcher".to_string(), + "managed installations, search path, or registry".to_string(), "[PYTHON SOURCES]".to_string(), )); self diff --git a/crates/uv/tests/it/help.rs b/crates/uv/tests/it/help.rs index f7464569ba83..dfa88fbad3df 100644 --- a/crates/uv/tests/it/help.rs +++ b/crates/uv/tests/it/help.rs @@ -223,7 +223,7 @@ fn help_subcommand() { interpreters are found by searching for Python executables in the `PATH` environment variable. - On Windows, the `py` launcher is also invoked to find Python executables. + On Windows, the registry is also searched for Python executables. By default, uv will download Python if a version cannot be found. This behavior can be disabled with the `--no-python-downloads` flag or the `python-downloads` setting. diff --git a/crates/uv/tests/it/python_find.rs b/crates/uv/tests/it/python_find.rs index 03a7323f7e82..aa92f83c6f97 100644 --- a/crates/uv/tests/it/python_find.rs +++ b/crates/uv/tests/it/python_find.rs @@ -20,7 +20,7 @@ fn python_find() { ----- stdout ----- ----- stderr ----- - error: No interpreter found in virtual environments, managed installations, system path, or `py` launcher + error: No interpreter found in virtual environments, managed installations, search path, or registry "###); } else { uv_snapshot!(context.filters(), context.python_find().env(EnvVars::UV_TEST_PYTHON_PATH, ""), @r###" @@ -29,7 +29,7 @@ fn python_find() { ----- stdout ----- ----- stderr ----- - error: No interpreter found in virtual environments, managed installations, or system path + error: No interpreter found in virtual environments, managed installations, or search path "###); } @@ -116,7 +116,7 @@ fn python_find() { ----- stdout ----- ----- stderr ----- - error: No interpreter found for PyPy in virtual environments, managed installations, system path, or `py` launcher + error: No interpreter found for PyPy in virtual environments, managed installations, search path, or registry "###); } else { uv_snapshot!(context.filters(), context.python_find().arg("pypy"), @r###" @@ -125,7 +125,7 @@ fn python_find() { ----- stdout ----- ----- stderr ----- - error: No interpreter found for PyPy in virtual environments, managed installations, or system path + error: No interpreter found for PyPy in virtual environments, managed installations, or search path "###); } @@ -441,7 +441,7 @@ fn python_find_unsupported_version() { ----- stdout ----- ----- stderr ----- - error: No interpreter found for Python 4.2 in virtual environments, managed installations, or system path + error: No interpreter found for Python 4.2 in virtual environments, managed installations, or search path "###); // Request a low version with a range @@ -451,7 +451,7 @@ fn python_find_unsupported_version() { ----- stdout ----- ----- stderr ----- - error: No interpreter found for Python <3.0 in virtual environments, managed installations, or system path + error: No interpreter found for Python <3.0 in virtual environments, managed installations, or search path "###); // Request free-threaded Python on unsupported version diff --git a/crates/uv/tests/it/python_pin.rs b/crates/uv/tests/it/python_pin.rs index aaa5b0494a0b..2faf19c3e45c 100644 --- a/crates/uv/tests/it/python_pin.rs +++ b/crates/uv/tests/it/python_pin.rs @@ -168,7 +168,7 @@ fn python_pin() { Updated `.python-version` from `[PYTHON-3.11]` -> `pypy` ----- stderr ----- - warning: No interpreter found for PyPy in managed installations or system path + warning: No interpreter found for PyPy in managed installations or search path "###); let python_version = context.read(PYTHON_VERSION_FILENAME); @@ -188,7 +188,7 @@ fn python_pin() { Updated `.python-version` from `pypy` -> `3.7` ----- stderr ----- - warning: No interpreter found for Python 3.7 in managed installations or system path + warning: No interpreter found for Python 3.7 in managed installations or search path "###); let python_version = context.read(PYTHON_VERSION_FILENAME); @@ -212,7 +212,7 @@ fn python_pin_no_python() { Pinned `.python-version` to `3.12` ----- stderr ----- - warning: No interpreter found for Python 3.12 in managed installations or system path + warning: No interpreter found for Python 3.12 in managed installations or search path "###); } @@ -411,7 +411,7 @@ fn warning_pinned_python_version_not_installed() -> anyhow::Result<()> { 3.12 ----- stderr ----- - warning: Failed to resolve pinned Python version `3.12`: No interpreter found for Python 3.12 in managed installations, system path, or `py` launcher + warning: Failed to resolve pinned Python version `3.12`: No interpreter found for Python 3.12 in managed installations, search path, or registry "###); } else { uv_snapshot!(context.filters(), context.python_pin(), @r###" @@ -421,7 +421,7 @@ fn warning_pinned_python_version_not_installed() -> anyhow::Result<()> { 3.12 ----- stderr ----- - warning: Failed to resolve pinned Python version `3.12`: No interpreter found for Python 3.12 in managed installations or system path + warning: Failed to resolve pinned Python version `3.12`: No interpreter found for Python 3.12 in managed installations or search path "###); } @@ -440,7 +440,7 @@ fn python_pin_resolve_no_python() { ----- stdout ----- ----- stderr ----- - error: No interpreter found for Python 3.12 in managed installations, system path, or `py` launcher + error: No interpreter found for Python 3.12 in managed installations, search path, or registry "###); } else { uv_snapshot!(context.filters(), context.python_pin().arg("--resolved").arg("3.12"), @r###" @@ -449,7 +449,7 @@ fn python_pin_resolve_no_python() { ----- stdout ----- ----- stderr ----- - error: No interpreter found for Python 3.12 in managed installations or system path + error: No interpreter found for Python 3.12 in managed installations or search path "###); } } @@ -605,7 +605,7 @@ fn python_pin_resolve() { ----- stdout ----- ----- stderr ----- - error: No interpreter found for PyPy in managed installations or system path + error: No interpreter found for PyPy in managed installations or search path "###); let python_version = context.read(PYTHON_VERSION_FILENAME); @@ -626,7 +626,7 @@ fn python_pin_resolve() { ----- stdout ----- ----- stderr ----- - error: No interpreter found for Python 3.7 in managed installations or system path + error: No interpreter found for Python 3.7 in managed installations or search path "###); let python_version = context.read(PYTHON_VERSION_FILENAME); diff --git a/crates/uv/tests/it/venv.rs b/crates/uv/tests/it/venv.rs index 1bc4ece4e86a..072bcff7dc9e 100644 --- a/crates/uv/tests/it/venv.rs +++ b/crates/uv/tests/it/venv.rs @@ -605,7 +605,7 @@ fn create_venv_unknown_python_minor() { ----- stdout ----- ----- stderr ----- - × No interpreter found for Python 3.100 in managed installations, system path, or `py` launcher + × No interpreter found for Python 3.100 in managed installations, search path, or registry "### ); } else { @@ -615,7 +615,7 @@ fn create_venv_unknown_python_minor() { ----- stdout ----- ----- stderr ----- - × No interpreter found for Python 3.100 in managed installations or system path + × No interpreter found for Python 3.100 in managed installations or search path "### ); } @@ -643,7 +643,7 @@ fn create_venv_unknown_python_patch() { ----- stdout ----- ----- stderr ----- - × No interpreter found for Python 3.12.100 in managed installations, system path, or `py` launcher + × No interpreter found for Python 3.12.100 in managed installations, search path, or registry "### ); } else { @@ -653,7 +653,7 @@ fn create_venv_unknown_python_patch() { ----- stdout ----- ----- stderr ----- - × No interpreter found for Python 3.12.100 in managed installations or system path + × No interpreter found for Python 3.12.100 in managed installations or search path "### ); } diff --git a/docs/reference/cli.md b/docs/reference/cli.md index b9729fc14b59..59f9bbb7440e 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -4142,7 +4142,7 @@ environment is not required, uv will then search for a Python interpreter. Pytho interpreters are found by searching for Python executables in the `PATH` environment variable. -On Windows, the `py` launcher is also invoked to find Python executables. +On Windows, the registry is also searched for Python executables. By default, uv will download Python if a version cannot be found. This behavior can be disabled with the `--no-python-downloads` flag or the `python-downloads` setting.