Add hashes to pip-compile output (#894)

## Summary

Adds hashes to `pip-compile` output, though we don't actually check
those hashes in `pip-sync` yet.

Closes https://github.com/astral-sh/puffin/issues/131.
This commit is contained in:
Charlie Marsh 2024-01-12 12:44:19 -05:00 committed by GitHub
parent 1629141d67
commit 06039e1293
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 281 additions and 37 deletions

View file

@ -22,7 +22,9 @@ use puffin_dispatch::BuildDispatch;
use puffin_installer::Downloader; use puffin_installer::Downloader;
use puffin_interpreter::{Interpreter, PythonVersion}; use puffin_interpreter::{Interpreter, PythonVersion};
use puffin_normalize::ExtraName; use puffin_normalize::ExtraName;
use puffin_resolver::{Manifest, PreReleaseMode, ResolutionMode, ResolutionOptions, Resolver}; use puffin_resolver::{
DisplayResolutionGraph, Manifest, PreReleaseMode, ResolutionMode, ResolutionOptions, Resolver,
};
use puffin_traits::SetupPyStrategy; use puffin_traits::SetupPyStrategy;
use requirements_txt::EditableRequirement; use requirements_txt::EditableRequirement;
@ -44,6 +46,7 @@ pub(crate) async fn pip_compile(
resolution_mode: ResolutionMode, resolution_mode: ResolutionMode,
prerelease_mode: PreReleaseMode, prerelease_mode: PreReleaseMode,
upgrade_mode: UpgradeMode, upgrade_mode: UpgradeMode,
generate_hashes: bool,
index_urls: IndexUrls, index_urls: IndexUrls,
setup_py: SetupPyStrategy, setup_py: SetupPyStrategy,
no_build: bool, no_build: bool,
@ -275,7 +278,11 @@ pub(crate) async fn pip_compile(
"{}", "{}",
format!("# puffin {}", env::args().skip(1).join(" ")).green() format!("# puffin {}", env::args().skip(1).join(" ")).green()
)?; )?;
write!(writer, "{resolution}")?; write!(
writer,
"{}",
DisplayResolutionGraph::new(&resolution, generate_hashes)
)?;
Ok(ExitStatus::Success) Ok(ExitStatus::Success)
} }

View file

@ -167,6 +167,10 @@ struct PipCompileArgs {
#[clap(long)] #[clap(long)]
upgrade: bool, upgrade: bool,
/// Include distribution hashes in the output file.
#[clap(long)]
generate_hashes: bool,
/// Use legacy `setuptools` behavior when building source distributions without a /// Use legacy `setuptools` behavior when building source distributions without a
/// `pyproject.toml`. /// `pyproject.toml`.
#[clap(long)] #[clap(long)]
@ -511,6 +515,7 @@ async fn inner() -> Result<ExitStatus> {
args.resolution, args.resolution,
args.prerelease, args.prerelease,
args.upgrade.into(), args.upgrade.into(),
args.generate_hashes,
index_urls, index_urls,
if args.legacy_setup_py { if args.legacy_setup_py {
SetupPyStrategy::Setuptools SetupPyStrategy::Setuptools

View file

@ -2911,3 +2911,127 @@ fn compile_legacy_sdist_setuptools() -> Result<()> {
Ok(()) Ok(())
} }
/// Include hashes in the generated output.
#[test]
fn generate_hashes() -> Result<()> {
let temp_dir = TempDir::new()?;
let cache_dir = TempDir::new()?;
let venv = create_venv_py312(&temp_dir, &cache_dir);
let requirements_in = temp_dir.child("requirements.in");
requirements_in.write_str("flask==3.0.0")?;
insta::with_settings!({
filters => INSTA_FILTERS.to_vec()
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("pip-compile")
.arg("requirements.in")
.arg("--generate-hashes")
.arg("--cache-dir")
.arg(cache_dir.path())
.arg("--exclude-newer")
.arg(EXCLUDE_NEWER)
.env("VIRTUAL_ENV", venv.as_os_str())
.current_dir(&temp_dir), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by Puffin v0.0.1 via the following command:
# puffin pip-compile requirements.in --generate-hashes --cache-dir [CACHE_DIR]
blinker==1.7.0 \
--hash=sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9 \
--hash=sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182
# via flask
click==8.1.7 \
--hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \
--hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de
# via flask
flask==3.0.0 \
--hash=sha256:21128f47e4e3b9d597a3e8521a329bf56909b690fcc3fa3e477725aa81367638 \
--hash=sha256:cfadcdb638b609361d29ec22360d6070a77d7463dcb3ab08d2c2f2f168845f58
itsdangerous==2.1.2 \
--hash=sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44 \
--hash=sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a
# via flask
jinja2==3.1.2 \
--hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \
--hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
# via flask
markupsafe==2.1.3 \
--hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \
--hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \
--hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \
--hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \
--hash=sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c \
--hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \
--hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \
--hash=sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb \
--hash=sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939 \
--hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \
--hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \
--hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \
--hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \
--hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \
--hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \
--hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \
--hash=sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd \
--hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \
--hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \
--hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \
--hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \
--hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \
--hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \
--hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \
--hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \
--hash=sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007 \
--hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \
--hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \
--hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \
--hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \
--hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \
--hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \
--hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \
--hash=sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1 \
--hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \
--hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \
--hash=sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c \
--hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \
--hash=sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823 \
--hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \
--hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \
--hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \
--hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \
--hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \
--hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \
--hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \
--hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \
--hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \
--hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \
--hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \
--hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \
--hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \
--hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \
--hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \
--hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \
--hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \
--hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \
--hash=sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc \
--hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2 \
--hash=sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11
# via
# jinja2
# werkzeug
werkzeug==3.0.1 \
--hash=sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc \
--hash=sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10
# via flask
----- stderr -----
Resolved 7 packages in [TIME]
"###);
});
Ok(())
}

View file

@ -2,7 +2,7 @@ pub use error::ResolveError;
pub use finder::{DistFinder, Reporter as FinderReporter}; pub use finder::{DistFinder, Reporter as FinderReporter};
pub use manifest::Manifest; pub use manifest::Manifest;
pub use prerelease_mode::PreReleaseMode; pub use prerelease_mode::PreReleaseMode;
pub use resolution::{Diagnostic, ResolutionGraph}; pub use resolution::{Diagnostic, DisplayResolutionGraph, ResolutionGraph};
pub use resolution_mode::ResolutionMode; pub use resolution_mode::ResolutionMode;
pub use resolution_options::ResolutionOptions; pub use resolution_options::ResolutionOptions;
pub use resolver::{BuildId, Reporter as ResolverReporter, Resolver, ResolverProvider}; pub use resolver::{BuildId, Reporter as ResolverReporter, Resolver, ResolverProvider};

View file

@ -15,10 +15,11 @@ use pep440_rs::Version;
use pep508_rs::{Requirement, VerbatimUrl}; use pep508_rs::{Requirement, VerbatimUrl};
use puffin_normalize::{ExtraName, PackageName}; use puffin_normalize::{ExtraName, PackageName};
use puffin_traits::OnceMap; use puffin_traits::OnceMap;
use pypi_types::Metadata21; use pypi_types::{Hashes, Metadata21};
use crate::pins::FilePins; use crate::pins::FilePins;
use crate::pubgrub::{PubGrubDistribution, PubGrubPackage, PubGrubPriority, PubGrubVersion}; use crate::pubgrub::{PubGrubDistribution, PubGrubPackage, PubGrubPriority, PubGrubVersion};
use crate::version_map::VersionMap;
use crate::ResolveError; use crate::ResolveError;
/// A complete resolution graph in which every node represents a pinned package and every edge /// A complete resolution graph in which every node represents a pinned package and every edge
@ -27,6 +28,8 @@ use crate::ResolveError;
pub struct ResolutionGraph { pub struct ResolutionGraph {
/// The underlying graph. /// The underlying graph.
petgraph: petgraph::graph::Graph<Dist, Range<PubGrubVersion>, petgraph::Directed>, petgraph: petgraph::graph::Graph<Dist, Range<PubGrubVersion>, petgraph::Directed>,
/// The metadata for every distribution in this resolution.
hashes: FxHashMap<PackageName, Vec<Hashes>>,
/// The set of editable requirements in this resolution. /// The set of editable requirements in this resolution.
editables: FxHashMap<PackageName, (LocalEditable, Metadata21)>, editables: FxHashMap<PackageName, (LocalEditable, Metadata21)>,
/// Any diagnostics that were encountered while building the graph. /// Any diagnostics that were encountered while building the graph.
@ -38,6 +41,7 @@ impl ResolutionGraph {
pub(crate) fn from_state( pub(crate) fn from_state(
selection: &SelectedDependencies<PubGrubPackage, PubGrubVersion>, selection: &SelectedDependencies<PubGrubPackage, PubGrubVersion>,
pins: &FilePins, pins: &FilePins,
packages: &OnceMap<PackageName, VersionMap>,
distributions: &OnceMap<PackageId, Metadata21>, distributions: &OnceMap<PackageId, Metadata21>,
redirects: &OnceMap<Url, Url>, redirects: &OnceMap<Url, Url>,
state: &State<PubGrubPackage, Range<PubGrubVersion>, PubGrubPriority>, state: &State<PubGrubPackage, Range<PubGrubVersion>, PubGrubPriority>,
@ -46,6 +50,8 @@ impl ResolutionGraph {
// TODO(charlie): petgraph is a really heavy and unnecessary dependency here. We should // TODO(charlie): petgraph is a really heavy and unnecessary dependency here. We should
// write our own graph, given that our requirements are so simple. // write our own graph, given that our requirements are so simple.
let mut petgraph = petgraph::graph::Graph::with_capacity(selection.len(), selection.len()); let mut petgraph = petgraph::graph::Graph::with_capacity(selection.len(), selection.len());
let mut hashes =
FxHashMap::with_capacity_and_hasher(selection.len(), BuildHasherDefault::default());
let mut diagnostics = Vec::new(); let mut diagnostics = Vec::new();
// Add every package to the graph. // Add every package to the graph.
@ -54,16 +60,28 @@ impl ResolutionGraph {
for (package, version) in selection { for (package, version) in selection {
match package { match package {
PubGrubPackage::Package(package_name, None, None) => { PubGrubPackage::Package(package_name, None, None) => {
let version = Version::from(version.clone()); // Create the distribution.
let pinned_package = pins let pinned_package = pins
.get(package_name, &version) .get(package_name, &Version::from(version.clone()))
.expect("Every package should be pinned") .expect("Every package should be pinned")
.clone(); .clone();
// Add its hashes to the index.
if let Some(entry) = packages.get(package_name) {
let version_map = entry.value();
hashes.insert(package_name.clone(), {
let mut hashes = version_map.hashes(version);
hashes.sort_unstable();
hashes
});
}
// Add the distribution to the graph.
let index = petgraph.add_node(pinned_package); let index = petgraph.add_node(pinned_package);
inverse.insert(package_name, index); inverse.insert(package_name, index);
} }
PubGrubPackage::Package(package_name, None, Some(url)) => { PubGrubPackage::Package(package_name, None, Some(url)) => {
// Create the distribution.
let pinned_package = if let Some((editable, _)) = editables.get(package_name) { let pinned_package = if let Some((editable, _)) = editables.get(package_name) {
Dist::from_editable(package_name.clone(), editable.clone())? Dist::from_editable(package_name.clone(), editable.clone())?
} else { } else {
@ -74,6 +92,17 @@ impl ResolutionGraph {
Dist::from_url(package_name.clone(), url)? Dist::from_url(package_name.clone(), url)?
}; };
// Add its hashes to the index.
if let Some(entry) = packages.get(package_name) {
let version_map = entry.value();
hashes.insert(package_name.clone(), {
let mut hashes = version_map.hashes(version);
hashes.sort_unstable();
hashes
});
}
// Add the distribution to the graph.
let index = petgraph.add_node(pinned_package); let index = petgraph.add_node(pinned_package);
inverse.insert(package_name, index); inverse.insert(package_name, index);
} }
@ -156,6 +185,7 @@ impl ResolutionGraph {
Ok(Self { Ok(Self {
petgraph, petgraph,
hashes,
editables, editables,
diagnostics, diagnostics,
}) })
@ -194,29 +224,74 @@ impl ResolutionGraph {
} }
} }
/// A [`std::fmt::Display`] implementation for the resolution graph.
#[derive(Debug)]
pub struct DisplayResolutionGraph<'a> {
/// The underlying graph.
resolution: &'a ResolutionGraph,
/// Whether to include hashes in the output.
show_hashes: bool,
}
impl<'a> DisplayResolutionGraph<'a> {
/// Create a new [`DisplayResolutionGraph`] for the given graph.
pub fn new(underlying: &'a ResolutionGraph, show_hashes: bool) -> DisplayResolutionGraph<'a> {
Self {
resolution: underlying,
show_hashes,
}
}
}
impl<'a> From<&'a ResolutionGraph> for DisplayResolutionGraph<'a> {
fn from(resolution: &'a ResolutionGraph) -> Self {
Self::new(resolution, false)
}
}
/// Write the graph in the `{name}=={version}` format of requirements.txt that pip uses. /// Write the graph in the `{name}=={version}` format of requirements.txt that pip uses.
impl std::fmt::Display for ResolutionGraph { impl std::fmt::Display for DisplayResolutionGraph<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Collect and sort all packages. // Collect and sort all packages.
let mut nodes = self let mut nodes = self
.resolution
.petgraph .petgraph
.node_indices() .node_indices()
.map(|node| (node, &self.petgraph[node])) .map(|node| (node, &self.resolution.petgraph[node]))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
nodes.sort_unstable_by_key(|(_, package)| package.name()); nodes.sort_unstable_by_key(|(_, package)| package.name());
// Print out the dependency graph. // Print out the dependency graph.
for (index, package) in nodes { for (index, package) in nodes {
if let Some((editable, _)) = self.editables.get(package.name()) { // Display the node itself.
writeln!(f, "-e {}", editable.verbatim())?; if let Some((editable, _)) = self.resolution.editables.get(package.name()) {
write!(f, "-e {}", editable.verbatim())?;
} else { } else {
writeln!(f, "{}", package.verbatim())?; write!(f, "{}", package.verbatim())?;
} }
// Display the distribution hashes, if any.
if self.show_hashes {
if let Some(hashes) = self
.resolution
.hashes
.get(package.name())
.filter(|hashes| !hashes.is_empty())
{
for hash in hashes {
writeln!(f, " \\")?;
write!(f, " --hash={hash}")?;
}
}
}
writeln!(f)?;
// Display all dependencies.
let mut edges = self let mut edges = self
.resolution
.petgraph .petgraph
.edges_directed(index, Direction::Incoming) .edges_directed(index, Direction::Incoming)
.map(|edge| &self.petgraph[edge.source()]) .map(|edge| &self.resolution.petgraph[edge.source()])
.collect::<Vec<_>>(); .collect::<Vec<_>>();
edges.sort_unstable_by_key(|package| package.name()); edges.sort_unstable_by_key(|package| package.name());

View file

@ -260,6 +260,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
return ResolutionGraph::from_state( return ResolutionGraph::from_state(
&selection, &selection,
&pins, &pins,
&self.index.packages,
&self.index.distributions, &self.index.distributions,
&self.index.redirects, &self.index.redirects,
&state, &state,

View file

@ -11,7 +11,7 @@ use platform_tags::{TagPriority, Tags};
use puffin_client::SimpleMetadata; use puffin_client::SimpleMetadata;
use puffin_normalize::PackageName; use puffin_normalize::PackageName;
use puffin_warnings::warn_user_once; use puffin_warnings::warn_user_once;
use pypi_types::{BaseUrl, Yanked}; use pypi_types::{BaseUrl, Hashes, Yanked};
use crate::pubgrub::PubGrubVersion; use crate::pubgrub::PubGrubVersion;
use crate::python_requirement::PythonRequirement; use crate::python_requirement::PythonRequirement;
@ -42,7 +42,7 @@ impl VersionMap {
for (version, files) in metadata { for (version, files) in metadata {
for (filename, file) in files.all() { for (filename, file) in files.all() {
// Support resolving as if it were an earlier timestamp, at least as long files have // Support resolving as if it were an earlier timestamp, at least as long files have
// upload time information // upload time information.
if let Some(exclude_newer) = exclude_newer { if let Some(exclude_newer) = exclude_newer {
match file.upload_time.as_ref() { match file.upload_time.as_ref() {
Some(upload_time) if upload_time >= exclude_newer => { Some(upload_time) if upload_time >= exclude_newer => {
@ -50,9 +50,8 @@ impl VersionMap {
} }
None => { None => {
warn_user_once!( warn_user_once!(
"{} is missing an upload date, but user provided: {}", "{} is missing an upload date, but user provided: {exclude_newer}",
file.filename, file.filename,
exclude_newer,
); );
continue; continue;
} }
@ -69,7 +68,9 @@ impl VersionMap {
} }
} }
// Prioritize amongst all available files.
let requires_python = file.requires_python.clone(); let requires_python = file.requires_python.clone();
let hash = file.hashes.clone();
match filename { match filename {
DistFilename::WheelFilename(filename) => { DistFilename::WheelFilename(filename) => {
// To be compatible, the wheel must both have compatible tags _and_ have a // To be compatible, the wheel must both have compatible tags _and_ have a
@ -92,12 +93,13 @@ impl VersionMap {
Entry::Occupied(mut entry) => { Entry::Occupied(mut entry) => {
entry entry
.get_mut() .get_mut()
.insert_built(dist, requires_python, priority); .insert_built(dist, requires_python, hash, priority);
} }
Entry::Vacant(entry) => { Entry::Vacant(entry) => {
entry.insert(PrioritizedDistribution::from_built( entry.insert(PrioritizedDistribution::from_built(
dist, dist,
requires_python, requires_python,
hash,
priority, priority,
)); ));
} }
@ -113,12 +115,13 @@ impl VersionMap {
); );
match version_map.entry(version.clone().into()) { match version_map.entry(version.clone().into()) {
Entry::Occupied(mut entry) => { Entry::Occupied(mut entry) => {
entry.get_mut().insert_source(dist, requires_python); entry.get_mut().insert_source(dist, requires_python, hash);
} }
Entry::Vacant(entry) => { Entry::Vacant(entry) => {
entry.insert(PrioritizedDistribution::from_source( entry.insert(PrioritizedDistribution::from_source(
dist, dist,
requires_python, requires_python,
hash,
)); ));
} }
} }
@ -143,6 +146,14 @@ impl VersionMap {
.iter() .iter()
.filter_map(|(version, file)| Some((version, file.get()?))) .filter_map(|(version, file)| Some((version, file.get()?)))
} }
/// Return the [`Hashes`] for the given version, if any.
pub(crate) fn hashes(&self, version: &PubGrubVersion) -> Vec<Hashes> {
self.0
.get(version)
.map(|file| file.hashes.clone())
.unwrap_or_default()
}
} }
/// Attach its requires-python to a [`Dist`], since downstream needs this information to filter /// Attach its requires-python to a [`Dist`], since downstream needs this information to filter
@ -161,6 +172,8 @@ struct PrioritizedDistribution {
compatible_wheel: Option<(DistRequiresPython, TagPriority)>, compatible_wheel: Option<(DistRequiresPython, TagPriority)>,
/// An arbitrary, platform-incompatible wheel for the package version. /// An arbitrary, platform-incompatible wheel for the package version.
incompatible_wheel: Option<DistRequiresPython>, incompatible_wheel: Option<DistRequiresPython>,
/// The hashes for each distribution.
hashes: Vec<Hashes>,
} }
impl PrioritizedDistribution { impl PrioritizedDistribution {
@ -168,6 +181,7 @@ impl PrioritizedDistribution {
fn from_built( fn from_built(
dist: Dist, dist: Dist,
requires_python: Option<VersionSpecifiers>, requires_python: Option<VersionSpecifiers>,
hash: Hashes,
priority: Option<TagPriority>, priority: Option<TagPriority>,
) -> Self { ) -> Self {
if let Some(priority) = priority { if let Some(priority) = priority {
@ -182,6 +196,7 @@ impl PrioritizedDistribution {
priority, priority,
)), )),
incompatible_wheel: None, incompatible_wheel: None,
hashes: vec![hash],
} }
} else { } else {
Self { Self {
@ -191,12 +206,13 @@ impl PrioritizedDistribution {
dist, dist,
requires_python, requires_python,
}), }),
hashes: vec![hash],
} }
} }
} }
/// Create a new [`PrioritizedDistribution`] from the given source distribution. /// Create a new [`PrioritizedDistribution`] from the given source distribution.
fn from_source(dist: Dist, requires_python: Option<VersionSpecifiers>) -> Self { fn from_source(dist: Dist, requires_python: Option<VersionSpecifiers>, hash: Hashes) -> Self {
Self { Self {
source: Some(DistRequiresPython { source: Some(DistRequiresPython {
dist, dist,
@ -204,6 +220,7 @@ impl PrioritizedDistribution {
}), }),
compatible_wheel: None, compatible_wheel: None,
incompatible_wheel: None, incompatible_wheel: None,
hashes: vec![hash],
} }
} }
@ -212,6 +229,7 @@ impl PrioritizedDistribution {
&mut self, &mut self,
dist: Dist, dist: Dist,
requires_python: Option<VersionSpecifiers>, requires_python: Option<VersionSpecifiers>,
hash: Hashes,
priority: Option<TagPriority>, priority: Option<TagPriority>,
) { ) {
// Prefer the highest-priority, platform-compatible wheel. // Prefer the highest-priority, platform-compatible wheel.
@ -241,16 +259,23 @@ impl PrioritizedDistribution {
requires_python, requires_python,
}); });
} }
self.hashes.push(hash);
} }
/// Insert the given source distribution into the [`PrioritizedDistribution`]. /// Insert the given source distribution into the [`PrioritizedDistribution`].
fn insert_source(&mut self, dist: Dist, requires_python: Option<VersionSpecifiers>) { fn insert_source(
&mut self,
dist: Dist,
requires_python: Option<VersionSpecifiers>,
hash: Hashes,
) {
if self.source.is_none() { if self.source.is_none() {
self.source = Some(DistRequiresPython { self.source = Some(DistRequiresPython {
dist, dist,
requires_python, requires_python,
}); });
} }
self.hashes.push(hash);
} }
/// Return the highest-priority distribution for the package version, if any. /// Return the highest-priority distribution for the package version, if any.

View file

@ -18,7 +18,8 @@ use puffin_cache::Cache;
use puffin_client::RegistryClientBuilder; use puffin_client::RegistryClientBuilder;
use puffin_interpreter::{Interpreter, Virtualenv}; use puffin_interpreter::{Interpreter, Virtualenv};
use puffin_resolver::{ use puffin_resolver::{
Manifest, PreReleaseMode, ResolutionGraph, ResolutionMode, ResolutionOptions, Resolver, DisplayResolutionGraph, Manifest, PreReleaseMode, ResolutionGraph, ResolutionMode,
ResolutionOptions, Resolver,
}; };
use puffin_traits::{BuildContext, BuildKind, SetupPyStrategy, SourceBuildTrait}; use puffin_traits::{BuildContext, BuildKind, SetupPyStrategy, SourceBuildTrait};
@ -140,7 +141,7 @@ async fn black() -> Result<()> {
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?; let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(resolution, @r###" assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
black==23.9.1 black==23.9.1
click==8.1.7 click==8.1.7
# via black # via black
@ -170,7 +171,7 @@ async fn black_colorama() -> Result<()> {
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?; let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(resolution, @r###" assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
black==23.9.1 black==23.9.1
click==8.1.7 click==8.1.7
# via black # via black
@ -202,7 +203,7 @@ async fn black_tensorboard() -> Result<()> {
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?; let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(resolution, @r###" assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
black==23.9.1 black==23.9.1
click==8.1.7 click==8.1.7
# via black # via black
@ -230,7 +231,7 @@ async fn black_python_310() -> Result<()> {
let resolution = resolve(manifest, options, &MARKERS_310, &TAGS_310).await?; let resolution = resolve(manifest, options, &MARKERS_310, &TAGS_310).await?;
assert_snapshot!(resolution, @r###" assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
black==23.9.1 black==23.9.1
click==8.1.7 click==8.1.7
# via black # via black
@ -271,7 +272,7 @@ async fn black_mypy_extensions() -> Result<()> {
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?; let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(resolution, @r###" assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
black==23.9.1 black==23.9.1
click==8.1.7 click==8.1.7
# via black # via black
@ -308,7 +309,7 @@ async fn black_mypy_extensions_extra() -> Result<()> {
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?; let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(resolution, @r###" assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
black==23.9.1 black==23.9.1
click==8.1.7 click==8.1.7
# via black # via black
@ -345,7 +346,7 @@ async fn black_flake8() -> Result<()> {
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?; let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(resolution, @r###" assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
black==23.9.1 black==23.9.1
click==8.1.7 click==8.1.7
# via black # via black
@ -373,7 +374,7 @@ async fn black_lowest() -> Result<()> {
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?; let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(resolution, @r###" assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
black==22.1.0 black==22.1.0
click==8.0.0 click==8.0.0
# via black # via black
@ -401,7 +402,7 @@ async fn black_lowest_direct() -> Result<()> {
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?; let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(resolution, @r###" assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
black==22.1.0 black==22.1.0
click==8.1.7 click==8.1.7
# via black # via black
@ -436,7 +437,7 @@ async fn black_respect_preference() -> Result<()> {
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?; let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(resolution, @r###" assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
black==23.9.0 black==23.9.0
click==8.1.7 click==8.1.7
# via black # via black
@ -471,7 +472,7 @@ async fn black_ignore_preference() -> Result<()> {
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?; let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(resolution, @r###" assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
black==23.9.1 black==23.9.1
click==8.1.7 click==8.1.7
# via black # via black
@ -543,7 +544,7 @@ async fn pylint_disallow_prerelease() -> Result<()> {
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?; let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(resolution, @r###" assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
astroid==3.0.1 astroid==3.0.1
# via pylint # via pylint
isort==5.12.0 isort==5.12.0
@ -567,7 +568,7 @@ async fn pylint_allow_prerelease() -> Result<()> {
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?; let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(resolution, @r###" assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
astroid==3.0.1 astroid==3.0.1
# via pylint # via pylint
isort==6.0.0b2 isort==6.0.0b2
@ -594,7 +595,7 @@ async fn pylint_allow_explicit_prerelease_without_marker() -> Result<()> {
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?; let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(resolution, @r###" assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
astroid==3.0.1 astroid==3.0.1
# via pylint # via pylint
isort==5.12.0 isort==5.12.0
@ -621,7 +622,7 @@ async fn pylint_allow_explicit_prerelease_with_marker() -> Result<()> {
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?; let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(resolution, @r###" assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
astroid==3.0.1 astroid==3.0.1
# via pylint # via pylint
isort==6.0.0b2 isort==6.0.0b2

View file

@ -85,8 +85,14 @@ impl Yanked {
/// ///
/// PEP 691 says multiple hashes can be included and the interpretation is left to the client, we /// PEP 691 says multiple hashes can be included and the interpretation is left to the client, we
/// only support SHA 256 atm. /// only support SHA 256 atm.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct Hashes { pub struct Hashes {
// TODO(charlie): Hashes should be optional. // TODO(charlie): Hashes should be optional.
pub sha256: String, pub sha256: String,
} }
impl std::fmt::Display for Hashes {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "sha256:{}", self.sha256)
}
}