diff --git a/crates/uv-resolver/src/lock.rs b/crates/uv-resolver/src/lock.rs index 078db6050..f2c3e5dfe 100644 --- a/crates/uv-resolver/src/lock.rs +++ b/crates/uv-resolver/src/lock.rs @@ -20,7 +20,7 @@ use distribution_types::{ GitSourceDist, IndexUrl, PathBuiltDist, PathSourceDist, RegistryBuiltDist, RegistryBuiltWheel, RegistrySourceDist, RemoteSource, Resolution, ResolvedDist, ToUrlError, }; -use pep440_rs::Version; +use pep440_rs::{Version, VersionSpecifiers}; use pep508_rs::{MarkerEnvironment, MarkerTree, VerbatimUrl}; use platform_tags::{TagCompatibility, TagPriority, Tags}; use pypi_types::{HashDigest, ParsedArchiveUrl, ParsedGitUrl}; @@ -36,6 +36,8 @@ use crate::{lock, ResolutionGraph}; pub struct Lock { version: u32, distributions: Vec, + /// The range of supported Python versions. + requires_python: Option, /// A map from distribution ID to index in `distributions`. /// /// This can be used to quickly lookup the full distribution for any ID @@ -87,15 +89,21 @@ impl Lock { } } - let lock = Self::new(locked_dists.into_values().collect())?; + let distributions = locked_dists.into_values().collect(); + let requires_python = graph.requires_python.clone(); + let lock = Self::new(distributions, requires_python)?; Ok(lock) } /// Initialize a [`Lock`] from a list of [`Distribution`] entries. - fn new(distributions: Vec) -> Result { + fn new( + distributions: Vec, + requires_python: Option, + ) -> Result { let wire = LockWire { version: 1, distributions, + requires_python, }; Self::try_from(wire) } @@ -196,6 +204,8 @@ struct LockWire { version: u32, #[serde(rename = "distribution")] distributions: Vec, + #[serde(rename = "requires-python")] + requires_python: Option, } impl From for LockWire { @@ -203,6 +213,7 @@ impl From for LockWire { LockWire { version: lock.version, distributions: lock.distributions, + requires_python: lock.requires_python, } } } @@ -215,6 +226,10 @@ impl Lock { let mut doc = toml_edit::DocumentMut::new(); doc.insert("version", value(i64::from(self.version))); + if let Some(ref requires_python) = self.requires_python { + doc.insert("requires-python", value(requires_python.to_string())); + } + let mut distributions = ArrayOfTables::new(); for dist in &self.distributions { let mut table = Table::new(); @@ -344,6 +359,7 @@ impl TryFrom for Lock { Ok(Lock { version: wire.version, distributions: wire.distributions, + requires_python: wire.requires_python, by_id, }) } diff --git a/crates/uv-resolver/src/python_requirement.rs b/crates/uv-resolver/src/python_requirement.rs index 0b8c2598e..f1c4278ab 100644 --- a/crates/uv-resolver/src/python_requirement.rs +++ b/crates/uv-resolver/src/python_requirement.rs @@ -60,13 +60,14 @@ impl PythonRequirement { #[derive(Debug, Clone, Eq, PartialEq)] pub enum RequiresPython { - /// The `RequiresPython` specifier is a single version specifier, as provided via + /// The [`RequiresPython`] specifier is a single version specifier, as provided via /// `--python-version` on the command line. /// /// The use of a separate enum variant allows us to use a verbatim representation when reporting /// back to the user. Specifier(StringVersion), - /// The `RequiresPython` specifier is a set of version specifiers. + /// The [`RequiresPython`] specifier is a set of version specifiers, as extracted from the + /// `Requires-Python` field in a `pyproject.toml` or `METADATA` file. Specifiers(VersionSpecifiers), } @@ -93,6 +94,14 @@ impl RequiresPython { } } } + + /// Returns the [`VersionSpecifiers`] for the [`RequiresPython`] specifier. + pub fn as_specifiers(&self) -> Option<&VersionSpecifiers> { + match self { + RequiresPython::Specifier(_) => None, + RequiresPython::Specifiers(specifiers) => Some(specifiers), + } + } } impl std::fmt::Display for RequiresPython { diff --git a/crates/uv-resolver/src/resolution/graph.rs b/crates/uv-resolver/src/resolution/graph.rs index b63e4626b..6a6a91389 100644 --- a/crates/uv-resolver/src/resolution/graph.rs +++ b/crates/uv-resolver/src/resolution/graph.rs @@ -9,7 +9,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; use distribution_types::{ Dist, DistributionMetadata, Name, ResolutionDiagnostic, VersionId, VersionOrUrlRef, }; -use pep440_rs::{Version, VersionSpecifier}; +use pep440_rs::{Version, VersionSpecifier, VersionSpecifiers}; use pep508_rs::{MarkerEnvironment, MarkerTree}; use pypi_types::{ParsedUrlError, Yanked}; use uv_git::GitResolver; @@ -17,10 +17,13 @@ use uv_normalize::{ExtraName, PackageName}; use crate::preferences::Preferences; use crate::pubgrub::{PubGrubDistribution, PubGrubPackageInner}; +use crate::python_requirement::RequiresPython; use crate::redirect::url_to_precise; use crate::resolution::AnnotatedDist; use crate::resolver::Resolution; -use crate::{InMemoryIndex, Manifest, MetadataResponse, ResolveError, VersionsResponse}; +use crate::{ + InMemoryIndex, Manifest, MetadataResponse, PythonRequirement, ResolveError, VersionsResponse, +}; /// A complete resolution graph in which every node represents a pinned package and every edge /// represents a dependency between two pinned packages. @@ -28,6 +31,8 @@ use crate::{InMemoryIndex, Manifest, MetadataResponse, ResolveError, VersionsRes pub struct ResolutionGraph { /// The underlying graph. pub(crate) petgraph: Graph, + /// The range of supported Python versions. + pub(crate) requires_python: Option, /// Any diagnostics that were encountered while building the graph. pub(crate) diagnostics: Vec, } @@ -39,9 +44,10 @@ impl ResolutionGraph { index: &InMemoryIndex, preferences: &Preferences, git: &GitResolver, + python: &PythonRequirement, resolution: Resolution, ) -> anyhow::Result { - // Collect all marker expressions from relevant pubgrub packages. + // Collect all marker expressions from relevant PubGrub packages. let mut markers: FxHashMap<(&PackageName, &Version, &Option), MarkerTree> = FxHashMap::default(); for (package, versions) in &resolution.packages { @@ -267,8 +273,17 @@ impl ResolutionGraph { } } + // Extract the `Requires-Python` range, if provided. + // TODO(charlie): Infer the supported Python range from the `Requires-Python` of the + // included packages. + let requires_python = python + .target() + .and_then(RequiresPython::as_specifiers) + .cloned(); + Ok(Self { petgraph, + requires_python, diagnostics, }) } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 145cd5802..b94995502 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -557,7 +557,13 @@ impl ResolverState Result<()> { assert_snapshot!( lock, @r###" version = 1 + requires-python = ">=3.7" [[distribution]] name = "dataclasses" @@ -1163,6 +1164,7 @@ fn lock_requires_python() -> Result<()> { assert_snapshot!( lock, @r###" version = 1 + requires-python = ">=3.7.9, <4" [[distribution]] name = "attrs"