Ignore global Python pins when incompatible with requires-python (#15473)
<!-- Thank you for contributing to uv! To help us out with reviewing, please consider the following: - Does this pull request include a summary of the change? (See below.) - Does this pull request include a descriptive title? - Does this pull request include references to any relevant issues? --> ## Summary @zanieb in #14916 found an interesting bug. Global pin is the user preference across projects, but if the project locally has a local pin, it should be an authoritative constraint and end up with error. This avoids blocking new projects that intentionally require a newer Python version than the user’s global default. We still error on a local `.python-version` inside the project to preserve explicit, repo-scoped intent. * Explicit `--python` → Always wins regardless of constraints * Local `.python-version` → Project-scoped, errors on conflict * Global `.python-version` → User preference, ignored if conflicts with project. Global pins are suggestions that can be overridden by project requirements * Project `requires-python` → Fallback when no pins exist Implementation : * If a global `~/.config/uv/.python-version` conflicts with a project `requires-python`, we ignore the pin and use the project requirement * If a local project `.python-version` conflicts, we error, with guidance to update the pin * Explicit `--python` continues to override both Now, global pins are suggestions that can be overridden by project requirements, rather than hard constraints that block project setup. ## Test Plan A new has been added in `sync_python_version()` along with manual testing : ``` harshps22ugp@lab:~/projects/uv$ target/debug/uv python pin --global 3.10 Pinned `/home/harshps22ugp/.config/uv/.python-version` to `3.10` harshps22ugp@lab:~/projects/uv$ mkdir -p /tmp/uv-global-pin && cd /tmp/uv-global-pin harshps22ugp@lab:/tmp/uv-global-pin$ cat > pyproject.toml <<'EOF' > [project] > name = "project" > version = "0.1.0" > requires-python = ">=3.11" > dependencies = ["anyio==3.7.0"] > EOF harshps22ugp@lab:/tmp/uv-global-pin$ /home/harshps22ugp/projects/uv/target/debug/uv sync Using CPython 3.13.5 Creating virtual environment at: .venv Resolved 4 packages in 276ms Prepared 3 packages in 149ms ░░░░░░░░░░░░░░░░░░░░ [0/3] Installing wheels... warning: Failed to hardlink files; falling back to full copy. This may lead to degraded performance. If the cache and target directories are on different filesystems, hardlinking may not be supported. If this is intentional, set `export UV_LINK_MODE=copy` or use `--link-mode=copy` to suppress this warning. Installed 3 packages in 35ms + anyio==3.7.0 + idna==3.10 + sniffio==1.3.1 harshps22ugp@lab:/tmp/uv-global-pin$ . .venv/bin/activate (project) harshps22ugp@lab:/tmp/uv-global-pin$ python -V Python 3.13.5 (project) harshps22ugp@lab:/tmp/uv-global-pin$ ``` No error was thrown! --------- Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
committed by
GitHub
parent
876023ac3d
commit
cd49736234
@@ -2222,6 +2222,19 @@ impl PythonRequest {
|
||||
Self::Key(request) => request.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an interpreter request into a concrete PEP 440 `Version` when possible.
|
||||
///
|
||||
/// Returns `None` if the request doesn't carry an exact version
|
||||
pub fn as_pep440_version(&self) -> Option<Version> {
|
||||
match self {
|
||||
Self::Version(v) | Self::ImplementationVersion(_, v) => v.as_pep440_version(),
|
||||
Self::Key(download_request) => download_request
|
||||
.version()
|
||||
.and_then(VersionRequest::as_pep440_version),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PythonSource {
|
||||
@@ -3057,6 +3070,28 @@ impl VersionRequest {
|
||||
| Self::Range(_, variant) => Some(*variant),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert this request into a concrete PEP 440 `Version` when possible.
|
||||
///
|
||||
/// Returns `None` for non-concrete requests
|
||||
pub fn as_pep440_version(&self) -> Option<Version> {
|
||||
match self {
|
||||
Self::Default | Self::Any | Self::Range(_, _) => None,
|
||||
Self::Major(major, _) => Some(Version::new([u64::from(*major)])),
|
||||
Self::MajorMinor(major, minor, _) => {
|
||||
Some(Version::new([u64::from(*major), u64::from(*minor)]))
|
||||
}
|
||||
Self::MajorMinorPatch(major, minor, patch, _) => Some(Version::new([
|
||||
u64::from(*major),
|
||||
u64::from(*minor),
|
||||
u64::from(*patch),
|
||||
])),
|
||||
// Pre-releases of Python versions are always for the zero patch version
|
||||
Self::MajorMinorPrerelease(major, minor, prerelease, _) => Some(
|
||||
Version::new([u64::from(*major), u64::from(*minor), 0]).with_pre(Some(*prerelease)),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for VersionRequest {
|
||||
@@ -3454,7 +3489,7 @@ mod tests {
|
||||
use assert_fs::{TempDir, prelude::*};
|
||||
use target_lexicon::{Aarch64Architecture, Architecture};
|
||||
use test_log::test;
|
||||
use uv_pep440::{Prerelease, PrereleaseKind, VersionSpecifiers};
|
||||
use uv_pep440::{Prerelease, PrereleaseKind, Version, VersionSpecifiers};
|
||||
|
||||
use crate::{
|
||||
discovery::{PythonRequest, VersionRequest},
|
||||
@@ -4120,4 +4155,134 @@ mod tests {
|
||||
// @ is not allowed if the prefix is empty.
|
||||
assert!(PythonRequest::try_split_prefix_and_version("", "@3").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_request_as_pep440_version() {
|
||||
// Non-concrete requests return `None`
|
||||
assert_eq!(VersionRequest::Default.as_pep440_version(), None);
|
||||
assert_eq!(VersionRequest::Any.as_pep440_version(), None);
|
||||
assert_eq!(
|
||||
VersionRequest::from_str(">=3.10")
|
||||
.unwrap()
|
||||
.as_pep440_version(),
|
||||
None
|
||||
);
|
||||
|
||||
// `VersionRequest::Major`
|
||||
assert_eq!(
|
||||
VersionRequest::Major(3, PythonVariant::Default).as_pep440_version(),
|
||||
Some(Version::from_str("3").unwrap())
|
||||
);
|
||||
|
||||
// `VersionRequest::MajorMinor`
|
||||
assert_eq!(
|
||||
VersionRequest::MajorMinor(3, 12, PythonVariant::Default).as_pep440_version(),
|
||||
Some(Version::from_str("3.12").unwrap())
|
||||
);
|
||||
|
||||
// `VersionRequest::MajorMinorPatch`
|
||||
assert_eq!(
|
||||
VersionRequest::MajorMinorPatch(3, 12, 5, PythonVariant::Default).as_pep440_version(),
|
||||
Some(Version::from_str("3.12.5").unwrap())
|
||||
);
|
||||
|
||||
// `VersionRequest::MajorMinorPrerelease`
|
||||
assert_eq!(
|
||||
VersionRequest::MajorMinorPrerelease(
|
||||
3,
|
||||
14,
|
||||
Prerelease {
|
||||
kind: PrereleaseKind::Alpha,
|
||||
number: 1
|
||||
},
|
||||
PythonVariant::Default
|
||||
)
|
||||
.as_pep440_version(),
|
||||
Some(Version::from_str("3.14.0a1").unwrap())
|
||||
);
|
||||
assert_eq!(
|
||||
VersionRequest::MajorMinorPrerelease(
|
||||
3,
|
||||
14,
|
||||
Prerelease {
|
||||
kind: PrereleaseKind::Beta,
|
||||
number: 2
|
||||
},
|
||||
PythonVariant::Default
|
||||
)
|
||||
.as_pep440_version(),
|
||||
Some(Version::from_str("3.14.0b2").unwrap())
|
||||
);
|
||||
assert_eq!(
|
||||
VersionRequest::MajorMinorPrerelease(
|
||||
3,
|
||||
13,
|
||||
Prerelease {
|
||||
kind: PrereleaseKind::Rc,
|
||||
number: 3
|
||||
},
|
||||
PythonVariant::Default
|
||||
)
|
||||
.as_pep440_version(),
|
||||
Some(Version::from_str("3.13.0rc3").unwrap())
|
||||
);
|
||||
|
||||
// Variant is ignored
|
||||
assert_eq!(
|
||||
VersionRequest::Major(3, PythonVariant::Freethreaded).as_pep440_version(),
|
||||
Some(Version::from_str("3").unwrap())
|
||||
);
|
||||
assert_eq!(
|
||||
VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded).as_pep440_version(),
|
||||
Some(Version::from_str("3.13").unwrap())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn python_request_as_pep440_version() {
|
||||
// `PythonRequest::Any` and `PythonRequest::Default` return `None`
|
||||
assert_eq!(PythonRequest::Any.as_pep440_version(), None);
|
||||
assert_eq!(PythonRequest::Default.as_pep440_version(), None);
|
||||
|
||||
// `PythonRequest::Version` delegates to `VersionRequest`
|
||||
assert_eq!(
|
||||
PythonRequest::Version(VersionRequest::MajorMinor(3, 11, PythonVariant::Default))
|
||||
.as_pep440_version(),
|
||||
Some(Version::from_str("3.11").unwrap())
|
||||
);
|
||||
|
||||
// `PythonRequest::ImplementationVersion` extracts version
|
||||
assert_eq!(
|
||||
PythonRequest::ImplementationVersion(
|
||||
ImplementationName::CPython,
|
||||
VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default),
|
||||
)
|
||||
.as_pep440_version(),
|
||||
Some(Version::from_str("3.12.1").unwrap())
|
||||
);
|
||||
|
||||
// `PythonRequest::Implementation` returns `None` (no version)
|
||||
assert_eq!(
|
||||
PythonRequest::Implementation(ImplementationName::CPython).as_pep440_version(),
|
||||
None
|
||||
);
|
||||
|
||||
// `PythonRequest::Key` with version
|
||||
assert_eq!(
|
||||
PythonRequest::parse("cpython-3.13.2").as_pep440_version(),
|
||||
Some(Version::from_str("3.13.2").unwrap())
|
||||
);
|
||||
|
||||
// `PythonRequest::Key` without version returns `None`
|
||||
assert_eq!(
|
||||
PythonRequest::parse("cpython-macos-aarch64-none").as_pep440_version(),
|
||||
None
|
||||
);
|
||||
|
||||
// Range versions return `None`
|
||||
assert_eq!(
|
||||
PythonRequest::Version(VersionRequest::from_str(">=3.10").unwrap()).as_pep440_version(),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user