diff --git a/crates/pep508-rs/src/marker.rs b/crates/pep508-rs/src/marker.rs index 97289b088cb6..9764099a24fc 100644 --- a/crates/pep508-rs/src/marker.rs +++ b/crates/pep508-rs/src/marker.rs @@ -319,7 +319,7 @@ impl FromStr for StringVersion { impl Display for StringVersion { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.version.fmt(f) + self.string.fmt(f) } } diff --git a/crates/uv-interpreter/src/interpreter.rs b/crates/uv-interpreter/src/interpreter.rs index 6ff3c326d7d5..ad7b8c7e59ba 100644 --- a/crates/uv-interpreter/src/interpreter.rs +++ b/crates/uv-interpreter/src/interpreter.rs @@ -11,7 +11,7 @@ use tracing::{debug, warn}; use cache_key::digest; use install_wheel_rs::Layout; use pep440_rs::Version; -use pep508_rs::MarkerEnvironment; +use pep508_rs::{MarkerEnvironment, StringVersion}; use platform_host::Platform; use platform_tags::{Tags, TagsError}; use pypi_types::Scheme; @@ -182,6 +182,12 @@ impl Interpreter { &self.markers.python_full_version.version } + /// Returns the `python_full_version` marker corresponding to this Python version. + #[inline] + pub const fn python_full_version(&self) -> &StringVersion { + &self.markers.python_full_version + } + /// Return the major version of this Python version. pub fn python_major(&self) -> u8 { let major = self.markers.python_full_version.version.release()[0]; diff --git a/crates/uv-interpreter/src/python_version.rs b/crates/uv-interpreter/src/python_version.rs index 6b75d1e4402b..dffb46fbff4c 100644 --- a/crates/uv-interpreter/src/python_version.rs +++ b/crates/uv-interpreter/src/python_version.rs @@ -1,9 +1,10 @@ -use pep440_rs::Version; -use pep508_rs::{MarkerEnvironment, StringVersion}; use std::fmt::{Display, Formatter}; use std::ops::Deref; use std::str::FromStr; +use pep440_rs::Version; +use pep508_rs::{MarkerEnvironment, StringVersion}; + use crate::Interpreter; #[derive(Debug, Clone)] @@ -59,18 +60,58 @@ impl PythonVersion { // Ex) `implementation_version == "3.12.0"` if markers.implementation_name == "cpython" { - markers.implementation_version = self.0.clone(); + let python_full_version = self.python_full_version(); + markers.implementation_version = StringVersion { + // Retain the verbatim representation, provided by the user. + string: self.0.to_string(), + version: python_full_version, + }; } // Ex) `python_full_version == "3.12.0"` - markers.python_full_version = self.0.clone(); + let python_full_version = self.python_full_version(); + markers.python_full_version = StringVersion { + // Retain the verbatim representation, provided by the user. + string: self.0.to_string(), + version: python_full_version, + }; // Ex) `python_version == "3.12"` - markers.python_version = self.0; + let python_version = self.python_version(); + markers.python_version = StringVersion { + string: python_version.to_string(), + version: python_version, + }; markers } + /// Return the `python_version` marker corresponding to this Python version. + /// + /// This should include exactly a major and minor version, but no patch version. + /// + /// Ex) `python_version == "3.12"` + pub fn python_version(&self) -> Version { + let major = self.release().first().copied().unwrap_or(0); + let minor = self.release().get(1).copied().unwrap_or(0); + Version::new([major, minor]) + } + + /// Return the `python_full_version` marker corresponding to this Python version. + /// + /// This should include exactly a major, minor, and patch version (even if it's zero), along + /// with any pre-release or post-release information. + /// + /// Ex) `python_full_version == "3.12.0b1"` + pub fn python_full_version(&self) -> Version { + let major = self.release().first().copied().unwrap_or(0); + let minor = self.release().get(1).copied().unwrap_or(0); + let patch = self.release().get(2).copied().unwrap_or(0); + Version::new([major, minor, patch]) + .with_pre(self.0.pre()) + .with_post(self.0.post()) + } + /// Return the full parsed Python version. pub fn version(&self) -> &Version { &self.0.version @@ -78,24 +119,12 @@ impl PythonVersion { /// Return the major version of this Python version. pub fn major(&self) -> u8 { - u8::try_from(self.0.release()[0]).expect("invalid major version") + u8::try_from(self.0.release().first().copied().unwrap_or(0)).expect("invalid major version") } /// Return the minor version of this Python version. pub fn minor(&self) -> u8 { - u8::try_from(self.0.release()[1]).expect("invalid minor version") - } - - /// Check if this Python version is satisfied by the given interpreter. - /// - /// If a patch version is present, we will require an exact match. - /// Otherwise, just the major and minor version numbers need to match. - pub fn is_satisfied_by(&self, interpreter: &Interpreter) -> bool { - if self.patch().is_some() { - self.version() == interpreter.python_version() - } else { - (self.major(), self.minor()) == interpreter.python_tuple() - } + u8::try_from(self.0.release().get(1).copied().unwrap_or(0)).expect("invalid minor version") } /// Return the patch version of this Python version, if set. @@ -113,4 +142,52 @@ impl PythonVersion { Self::from_str(format!("{}.{}", self.major(), self.minor()).as_str()) .expect("dropping a patch should always be valid") } + + /// Check if this Python version is satisfied by the given interpreter. + /// + /// If a patch version is present, we will require an exact match. + /// Otherwise, just the major and minor version numbers need to match. + pub fn is_satisfied_by(&self, interpreter: &Interpreter) -> bool { + if self.patch().is_some() { + self.version() == interpreter.python_version() + } else { + (self.major(), self.minor()) == interpreter.python_tuple() + } + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use pep440_rs::{PreRelease, PreReleaseKind, Version}; + + use crate::PythonVersion; + + #[test] + fn python_markers() { + let version = PythonVersion::from_str("3.11.0").expect("valid python version"); + assert_eq!(version.python_version(), Version::new([3, 11])); + assert_eq!(version.python_version().to_string(), "3.11"); + assert_eq!(version.python_full_version(), Version::new([3, 11, 0])); + assert_eq!(version.python_full_version().to_string(), "3.11.0"); + + let version = PythonVersion::from_str("3.11").expect("valid python version"); + assert_eq!(version.python_version(), Version::new([3, 11])); + assert_eq!(version.python_version().to_string(), "3.11"); + assert_eq!(version.python_full_version(), Version::new([3, 11, 0])); + assert_eq!(version.python_full_version().to_string(), "3.11.0"); + + let version = PythonVersion::from_str("3.11.8a1").expect("valid python version"); + assert_eq!(version.python_version(), Version::new([3, 11])); + assert_eq!(version.python_version().to_string(), "3.11"); + assert_eq!( + version.python_full_version(), + Version::new([3, 11, 8]).with_pre(Some(PreRelease { + kind: PreReleaseKind::Alpha, + number: 1 + })) + ); + assert_eq!(version.python_full_version().to_string(), "3.11.8a1"); + } } diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index be9d3e59e362..d83ab963cc48 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -1,6 +1,7 @@ use std::collections::BTreeSet; use std::convert::Infallible; use std::fmt::Formatter; +use std::ops::Deref; use dashmap::{DashMap, DashSet}; use indexmap::IndexMap; @@ -190,13 +191,13 @@ impl NoSolutionError { PubGrubPackage::Python(PubGrubPython::Installed) => { available_versions.insert( package.clone(), - BTreeSet::from([python_requirement.installed().clone()]), + BTreeSet::from([python_requirement.installed().deref().clone()]), ); } PubGrubPackage::Python(PubGrubPython::Target) => { available_versions.insert( package.clone(), - BTreeSet::from([python_requirement.target().clone()]), + BTreeSet::from([python_requirement.target().deref().clone()]), ); } PubGrubPackage::Package(name, ..) => { diff --git a/crates/uv-resolver/src/python_requirement.rs b/crates/uv-resolver/src/python_requirement.rs index 80f0b84633da..8c8aa0cfe03a 100644 --- a/crates/uv-resolver/src/python_requirement.rs +++ b/crates/uv-resolver/src/python_requirement.rs @@ -1,32 +1,31 @@ -use pep440_rs::Version; -use pep508_rs::MarkerEnvironment; +use pep508_rs::{MarkerEnvironment, StringVersion}; use uv_interpreter::Interpreter; -#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct PythonRequirement { /// The installed version of Python. - installed: Version, + installed: StringVersion, /// The target version of Python; that is, the version of Python for which we are resolving /// dependencies. This is typically the same as the installed version, but may be different /// when specifying an alternate Python version for the resolution. - target: Version, + target: StringVersion, } impl PythonRequirement { pub fn new(interpreter: &Interpreter, markers: &MarkerEnvironment) -> Self { Self { - installed: interpreter.python_version().clone(), - target: markers.python_full_version.version.clone(), + installed: interpreter.python_full_version().clone(), + target: markers.python_full_version.clone(), } } /// Return the installed version of Python. - pub fn installed(&self) -> &Version { + pub fn installed(&self) -> &StringVersion { &self.installed } /// Return the target version of Python. - pub fn target(&self) -> &Version { + pub fn target(&self) -> &StringVersion { &self.target } } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index a0b1c58e4602..a5ad108c206a 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -2,6 +2,7 @@ use std::borrow::Cow; use std::fmt::{Display, Formatter}; +use std::ops::Deref; use std::sync::Arc; use anyhow::Result; @@ -554,7 +555,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { PubGrubPackage::Python(PubGrubPython::Installed) => { let version = self.python_requirement.installed(); if range.contains(version) { - Ok(Some(ResolverVersion::Available(version.clone()))) + Ok(Some(ResolverVersion::Available(version.deref().clone()))) } else { Ok(None) } @@ -563,7 +564,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { PubGrubPackage::Python(PubGrubPython::Target) => { let version = self.python_requirement.target(); if range.contains(version) { - Ok(Some(ResolverVersion::Available(version.clone()))) + Ok(Some(ResolverVersion::Available(version.deref().clone()))) } else { Ok(None) }