Respect global Python version pins in uv tool run and uv tool install (#14112)

Closes #12921

For `uv tool run`, we'll just use the global Python version for all
invocations without an explicit alternative request (i.e., via the
`--python` flag).

For `uv tool install`, it's a bit more complicated:

- If the tool is not installed, we'll use the global Python version
- If the tool is already installed, we won't change the Python version
unless `--reinstall` or `--python` is used
- If the tool was installed with `--python`, we won't use the global
Python version, unless the tool is uninstalled first

The behavior can be demonstrated as follows

```
$ uv python pin --global 3.12
$ uv tool install flask  # uses 3.12
$ uv tool install flask  # no-op
$ uv python pin --global 3.13
$ uv tool install flask  # no-op
$ uv tool install flask --reinstall  # uses 3.13
$ uv tool install flask -p 3.12 # uses 3.12
$ uv tool install flask  # no-op
$ uv tool install flask --reinstall # uses 3.12
```

This is a little more complicated than always reinstalling when the
global Python version pin changes, but I think it's probably more
intuitive when actually using the tool. We briefly touched on this when
adding global version pins at
https://github.com/astral-sh/uv/pull/12115#discussion_r1992222278

Minor note: I need to do a self-review of this implementation, as it's a
little awkward to encode this behavior in the existing logic.
This commit is contained in:
Zanie Blue
2026-02-03 11:24:00 -06:00
parent 3632202197
commit 4ae86f9b0a
6 changed files with 393 additions and 90 deletions
+13 -1
View File
@@ -46,7 +46,7 @@ use crate::{BrokenSymlink, Interpreter, PythonVersion};
/// A request to find a Python installation.
///
/// See [`PythonRequest::from_str`].
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
#[derive(Debug, Clone, Eq, Default)]
pub enum PythonRequest {
/// An appropriate default Python installation
///
@@ -73,6 +73,18 @@ pub enum PythonRequest {
Key(PythonDownloadRequest),
}
impl PartialEq for PythonRequest {
fn eq(&self, other: &Self) -> bool {
self.to_canonical_string() == other.to_canonical_string()
}
}
impl std::hash::Hash for PythonRequest {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.to_canonical_string().hash(state);
}
}
impl<'a> serde::Deserialize<'a> for PythonRequest {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where