Skip to content

Commit

Permalink
Preserve hashes for pinned packages
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Mar 19, 2024
1 parent 1d10014 commit 109ebc7
Show file tree
Hide file tree
Showing 15 changed files with 523 additions and 78 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

90 changes: 90 additions & 0 deletions crates/pypi-types/src/simple_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,93 @@ impl Hashes {
self.sha256.as_deref().or(self.md5.as_deref())
}
}

impl FromStr for Hashes {
type Err = HashError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split(':');

// Extract the key and value.
let name = parts
.next()
.ok_or_else(|| HashError::InvalidStructure(s.to_string()))?;
let value = parts
.next()
.ok_or_else(|| HashError::InvalidStructure(s.to_string()))?;

// Ensure there are no more parts.
if parts.next().is_some() {
return Err(HashError::InvalidStructure(s.to_string()));
}

match name {
"md5" => {
let md5 = value.to_string();
Ok(Hashes {
md5: Some(md5),
sha256: None,
})
}
"sha256" => {
let sha256 = value.to_string();
Ok(Hashes {
md5: None,
sha256: Some(sha256),
})
}
_ => Err(HashError::UnsupportedHashAlgorithm(s.to_string())),
}
}
}

#[derive(thiserror::Error, Debug)]
pub enum HashError {
#[error("Unexpected hash (expected `sha256:<hash>` or `md5:<hash>`) on: {0}")]
InvalidStructure(String),

#[error("Unsupported hash algorithm (expected `sha256` or `md5`) on: {0}")]
UnsupportedHashAlgorithm(String),
}

#[cfg(test)]
mod tests {
use crate::{HashError, Hashes};

#[test]
fn parse_hashes() -> Result<(), HashError> {
let hashes: Hashes =
"sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".parse()?;
assert_eq!(
hashes,
Hashes {
md5: None,
sha256: Some(
"40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".to_string()
)
}
);

let hashes: Hashes =
"md5:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2".parse()?;
assert_eq!(
hashes,
Hashes {
md5: Some(
"090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2".to_string()
),
sha256: None
}
);

let result = "sha256=40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"
.parse::<Hashes>();
assert!(result.is_err());

let result = "sha512:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"
.parse::<Hashes>();
assert!(result.is_err());

Ok(())
}
}
2 changes: 1 addition & 1 deletion crates/uv-client/src/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ pub enum Error {
#[error("Unexpected fragment (expected `#sha256=...`) on URL: {0}")]
FragmentParse(String),

#[error("Unsupported hash algorithm (expected `sha256`) on: {0}")]
#[error("Unsupported hash algorithm (expected `sha256` or `md5`) on: {0}")]
UnsupportedHashAlgorithm(String),

#[error("Invalid `requires-python` specifier: {0}")]
Expand Down
1 change: 1 addition & 0 deletions crates/uv-resolver/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pep440_rs = { path = "../pep440-rs", features = ["pubgrub"] }
pep508_rs = { path = "../pep508-rs" }
platform-tags = { path = "../platform-tags" }
pypi-types = { path = "../pypi-types" }
requirements-txt = { path = "../requirements-txt" }
uv-cache = { path = "../uv-cache" }
uv-client = { path = "../uv-client" }
uv-distribution = { path = "../uv-distribution" }
Expand Down
46 changes: 4 additions & 42 deletions crates/uv-resolver/src/candidate_selector.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use pubgrub::range::Range;
use rustc_hash::FxHashMap;

use distribution_types::{CompatibleDist, IncompatibleDist, IncompatibleSource};
use distribution_types::{DistributionMetadata, IncompatibleWheel, Name, PrioritizedDist};
use pep440_rs::Version;
use pep508_rs::{MarkerEnvironment, Requirement, VersionOrUrl};
use pep508_rs::MarkerEnvironment;
use uv_normalize::PackageName;

use crate::preferences::Preferences;
use crate::prerelease_mode::PreReleaseStrategy;
use crate::resolution_mode::ResolutionStrategy;
use crate::version_map::{VersionMap, VersionMapDistHandle};
Expand All @@ -16,7 +16,6 @@ use crate::{Manifest, Options};
pub(crate) struct CandidateSelector {
resolution_strategy: ResolutionStrategy,
prerelease_strategy: PreReleaseStrategy,
preferences: Preferences,
}

impl CandidateSelector {
Expand All @@ -37,7 +36,6 @@ impl CandidateSelector {
manifest,
markers,
),
preferences: Preferences::from_requirements(manifest.preferences.as_slice(), markers),
}
}

Expand All @@ -54,43 +52,6 @@ impl CandidateSelector {
}
}

/// A set of pinned packages that should be preserved during resolution, if possible.
#[derive(Debug, Clone)]
struct Preferences(FxHashMap<PackageName, Version>);

impl Preferences {
/// Create a set of [`Preferences`] from a set of requirements.
fn from_requirements(requirements: &[Requirement], markers: &MarkerEnvironment) -> Self {
Self(
requirements
.iter()
.filter_map(|requirement| {
if !requirement.evaluate_markers(markers, &[]) {
return None;
}
let Some(VersionOrUrl::VersionSpecifier(version_specifiers)) =
requirement.version_or_url.as_ref()
else {
return None;
};
let [version_specifier] = version_specifiers.as_ref() else {
return None;
};
Some((
requirement.name.clone(),
version_specifier.version().clone(),
))
})
.collect(),
)
}

/// Return the pinned version for a package, if any.
fn get(&self, package_name: &PackageName) -> Option<&Version> {
self.0.get(package_name)
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum AllowPreRelease {
Yes,
Expand All @@ -105,10 +66,11 @@ impl CandidateSelector {
package_name: &'a PackageName,
range: &'a Range<Version>,
version_map: &'a VersionMap,
preferences: &'a Preferences,
) -> Option<Candidate<'a>> {
// If the package has a preference (e.g., an existing version from an existing lockfile),
// and the preference satisfies the current range, use that.
if let Some(version) = self.preferences.get(package_name) {
if let Some(version) = preferences.version(package_name) {
if range.contains(version) {
if let Some(file) = version_map.get(version) {
return Some(Candidate::new(package_name, version, file));
Expand Down
2 changes: 2 additions & 0 deletions crates/uv-resolver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub use error::ResolveError;
pub use finder::{DistFinder, Reporter as FinderReporter};
pub use manifest::Manifest;
pub use options::{Options, OptionsBuilder};
pub use preferences::Preference;
pub use prerelease_mode::PreReleaseMode;
pub use python_requirement::PythonRequirement;
pub use resolution::{AnnotationStyle, Diagnostic, DisplayResolutionGraph, ResolutionGraph};
Expand All @@ -24,6 +25,7 @@ mod manifest;
mod options;
mod overrides;
mod pins;
mod preferences;
mod prerelease_mode;
mod pubgrub;
mod python_requirement;
Expand Down
5 changes: 3 additions & 2 deletions crates/uv-resolver/src/manifest.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::preferences::Preference;
use distribution_types::LocalEditable;
use pep508_rs::Requirement;
use pypi_types::Metadata23;
Expand All @@ -9,7 +10,7 @@ pub struct Manifest {
pub(crate) requirements: Vec<Requirement>,
pub(crate) constraints: Vec<Requirement>,
pub(crate) overrides: Vec<Requirement>,
pub(crate) preferences: Vec<Requirement>,
pub(crate) preferences: Vec<Preference>,
pub(crate) project: Option<PackageName>,
pub(crate) editables: Vec<(LocalEditable, Metadata23)>,
}
Expand All @@ -19,7 +20,7 @@ impl Manifest {
requirements: Vec<Requirement>,
constraints: Vec<Requirement>,
overrides: Vec<Requirement>,
preferences: Vec<Requirement>,
preferences: Vec<Preference>,
project: Option<PackageName>,
editables: Vec<(LocalEditable, Metadata23)>,
) -> Self {
Expand Down
133 changes: 133 additions & 0 deletions crates/uv-resolver/src/preferences.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
use std::str::FromStr;

use rustc_hash::FxHashMap;

use pep440_rs::{Operator, Version};
use pep508_rs::{MarkerEnvironment, Requirement, VersionOrUrl};
use pypi_types::{HashError, Hashes};
use requirements_txt::RequirementEntry;
use uv_normalize::PackageName;

/// A pinned requirement, as extracted from a `requirements.txt` file.
#[derive(Debug)]
pub struct Preference {
requirement: Requirement,
hashes: Vec<Hashes>,
}

impl Preference {
/// Create a [`Preference`] from a [`RequirementEntry`].
pub fn from_entry(entry: RequirementEntry) -> Result<Self, HashError> {
Ok(Self {
requirement: entry.requirement,
hashes: entry
.hashes
.iter()
.map(String::as_str)
.map(Hashes::from_str)
.collect::<Result<_, _>>()?,
})
}

/// Create a [`Preference`] from a [`Requirement`].
pub fn from_requirement(requirement: Requirement) -> Self {
Self {
requirement,
hashes: Vec::new(),
}
}

/// Return the name of the package for this preference.
pub fn name(&self) -> &PackageName {
&self.requirement.name
}

/// Return the [`Requirement`] for this preference.
pub fn requirement(&self) -> &Requirement {
&self.requirement
}
}

/// A set of pinned packages that should be preserved during resolution, if possible.
#[derive(Debug, Clone)]
pub(crate) struct Preferences(FxHashMap<PackageName, Pin>);

impl Preferences {
/// Create a map of pinned packages from a list of [`Preference`] entries.
pub(crate) fn from_requirements(
requirements: Vec<Preference>,
markers: &MarkerEnvironment,
) -> Self {
Self(
requirements
.into_iter()
.filter_map(
|Preference {
requirement,
hashes,
}| {
// Search for, e.g., `flask==1.2.3` entries that match the current environment.
if !requirement.evaluate_markers(markers, &[]) {
return None;
}
let Some(VersionOrUrl::VersionSpecifier(version_specifiers)) =
requirement.version_or_url.as_ref()
else {
return None;
};
let [version_specifier] = version_specifiers.as_ref() else {
return None;
};
if *version_specifier.operator() != Operator::Equal {
return None;
}
Some((
requirement.name,
Pin {
version: version_specifier.version().clone(),
hashes,
},
))
},
)
.collect(),
)
}

/// Return the pinned version for a package, if any.
pub(crate) fn version(&self, package_name: &PackageName) -> Option<&Version> {
self.0.get(package_name).map(Pin::version)
}

/// Return the hashes for a package, if the version matches that of the pin.
pub(crate) fn match_hashes(
&self,
package_name: &PackageName,
version: &Version,
) -> Option<&[Hashes]> {
let pin = self.0.get(package_name)?;
if pin.version() != version {
return None;
}
Some(pin.hashes())
}
}

/// The pinned data associated with a package in a locked `requirements.txt` file (e.g., `flask==1.2.3`).
#[derive(Debug, Clone)]
struct Pin {
version: Version,
hashes: Vec<Hashes>,
}

impl Pin {
/// Return the version of the pinned package.
fn version(&self) -> &Version {
&self.version
}

/// Return the hashes of the pinned package.
fn hashes(&self) -> &[Hashes] {
&self.hashes
}
}
Loading

0 comments on commit 109ebc7

Please sign in to comment.