diff --git a/crates/puffin-cli/src/commands/pip_compile.rs b/crates/puffin-cli/src/commands/pip_compile.rs index 9cefcfa6c..cee3b08c3 100644 --- a/crates/puffin-cli/src/commands/pip_compile.rs +++ b/crates/puffin-cli/src/commands/pip_compile.rs @@ -22,7 +22,9 @@ use puffin_dispatch::BuildDispatch; use puffin_installer::Downloader; use puffin_interpreter::{Interpreter, PythonVersion}; 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 requirements_txt::EditableRequirement; @@ -44,6 +46,7 @@ pub(crate) async fn pip_compile( resolution_mode: ResolutionMode, prerelease_mode: PreReleaseMode, upgrade_mode: UpgradeMode, + generate_hashes: bool, index_urls: IndexUrls, setup_py: SetupPyStrategy, no_build: bool, @@ -275,7 +278,11 @@ pub(crate) async fn pip_compile( "{}", format!("# puffin {}", env::args().skip(1).join(" ")).green() )?; - write!(writer, "{resolution}")?; + write!( + writer, + "{}", + DisplayResolutionGraph::new(&resolution, generate_hashes) + )?; Ok(ExitStatus::Success) } diff --git a/crates/puffin-cli/src/main.rs b/crates/puffin-cli/src/main.rs index 3abc587ee..b5d0253a1 100644 --- a/crates/puffin-cli/src/main.rs +++ b/crates/puffin-cli/src/main.rs @@ -167,6 +167,10 @@ struct PipCompileArgs { #[clap(long)] upgrade: bool, + /// Include distribution hashes in the output file. + #[clap(long)] + generate_hashes: bool, + /// Use legacy `setuptools` behavior when building source distributions without a /// `pyproject.toml`. #[clap(long)] @@ -511,6 +515,7 @@ async fn inner() -> Result { args.resolution, args.prerelease, args.upgrade.into(), + args.generate_hashes, index_urls, if args.legacy_setup_py { SetupPyStrategy::Setuptools diff --git a/crates/puffin-cli/tests/pip_compile.rs b/crates/puffin-cli/tests/pip_compile.rs index e5ee14a46..499046691 100644 --- a/crates/puffin-cli/tests/pip_compile.rs +++ b/crates/puffin-cli/tests/pip_compile.rs @@ -2911,3 +2911,127 @@ fn compile_legacy_sdist_setuptools() -> Result<()> { 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(()) +} diff --git a/crates/puffin-resolver/src/lib.rs b/crates/puffin-resolver/src/lib.rs index bcedbd454..c6b7956b5 100644 --- a/crates/puffin-resolver/src/lib.rs +++ b/crates/puffin-resolver/src/lib.rs @@ -2,7 +2,7 @@ pub use error::ResolveError; pub use finder::{DistFinder, Reporter as FinderReporter}; pub use manifest::Manifest; pub use prerelease_mode::PreReleaseMode; -pub use resolution::{Diagnostic, ResolutionGraph}; +pub use resolution::{Diagnostic, DisplayResolutionGraph, ResolutionGraph}; pub use resolution_mode::ResolutionMode; pub use resolution_options::ResolutionOptions; pub use resolver::{BuildId, Reporter as ResolverReporter, Resolver, ResolverProvider}; diff --git a/crates/puffin-resolver/src/resolution.rs b/crates/puffin-resolver/src/resolution.rs index a17d6095d..da4d132f1 100644 --- a/crates/puffin-resolver/src/resolution.rs +++ b/crates/puffin-resolver/src/resolution.rs @@ -15,10 +15,11 @@ use pep440_rs::Version; use pep508_rs::{Requirement, VerbatimUrl}; use puffin_normalize::{ExtraName, PackageName}; use puffin_traits::OnceMap; -use pypi_types::Metadata21; +use pypi_types::{Hashes, Metadata21}; use crate::pins::FilePins; use crate::pubgrub::{PubGrubDistribution, PubGrubPackage, PubGrubPriority, PubGrubVersion}; +use crate::version_map::VersionMap; use crate::ResolveError; /// 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 { /// The underlying graph. petgraph: petgraph::graph::Graph, petgraph::Directed>, + /// The metadata for every distribution in this resolution. + hashes: FxHashMap>, /// The set of editable requirements in this resolution. editables: FxHashMap, /// Any diagnostics that were encountered while building the graph. @@ -38,6 +41,7 @@ impl ResolutionGraph { pub(crate) fn from_state( selection: &SelectedDependencies, pins: &FilePins, + packages: &OnceMap, distributions: &OnceMap, redirects: &OnceMap, state: &State, PubGrubPriority>, @@ -46,6 +50,8 @@ impl ResolutionGraph { // TODO(charlie): petgraph is a really heavy and unnecessary dependency here. We should // 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 hashes = + FxHashMap::with_capacity_and_hasher(selection.len(), BuildHasherDefault::default()); let mut diagnostics = Vec::new(); // Add every package to the graph. @@ -54,16 +60,28 @@ impl ResolutionGraph { for (package, version) in selection { match package { PubGrubPackage::Package(package_name, None, None) => { - let version = Version::from(version.clone()); + // Create the distribution. let pinned_package = pins - .get(package_name, &version) + .get(package_name, &Version::from(version.clone())) .expect("Every package should be pinned") .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); inverse.insert(package_name, index); } PubGrubPackage::Package(package_name, None, Some(url)) => { + // Create the distribution. let pinned_package = if let Some((editable, _)) = editables.get(package_name) { Dist::from_editable(package_name.clone(), editable.clone())? } else { @@ -74,6 +92,17 @@ impl ResolutionGraph { 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); inverse.insert(package_name, index); } @@ -156,6 +185,7 @@ impl ResolutionGraph { Ok(Self { petgraph, + hashes, editables, 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. -impl std::fmt::Display for ResolutionGraph { +impl std::fmt::Display for DisplayResolutionGraph<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { // Collect and sort all packages. let mut nodes = self + .resolution .petgraph .node_indices() - .map(|node| (node, &self.petgraph[node])) + .map(|node| (node, &self.resolution.petgraph[node])) .collect::>(); nodes.sort_unstable_by_key(|(_, package)| package.name()); // Print out the dependency graph. for (index, package) in nodes { - if let Some((editable, _)) = self.editables.get(package.name()) { - writeln!(f, "-e {}", editable.verbatim())?; + // Display the node itself. + if let Some((editable, _)) = self.resolution.editables.get(package.name()) { + write!(f, "-e {}", editable.verbatim())?; } 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 + .resolution .petgraph .edges_directed(index, Direction::Incoming) - .map(|edge| &self.petgraph[edge.source()]) + .map(|edge| &self.resolution.petgraph[edge.source()]) .collect::>(); edges.sort_unstable_by_key(|package| package.name()); diff --git a/crates/puffin-resolver/src/resolver/mod.rs b/crates/puffin-resolver/src/resolver/mod.rs index 458299678..2df666073 100644 --- a/crates/puffin-resolver/src/resolver/mod.rs +++ b/crates/puffin-resolver/src/resolver/mod.rs @@ -260,6 +260,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { return ResolutionGraph::from_state( &selection, &pins, + &self.index.packages, &self.index.distributions, &self.index.redirects, &state, diff --git a/crates/puffin-resolver/src/version_map.rs b/crates/puffin-resolver/src/version_map.rs index 2773d0d9f..0ecb06b91 100644 --- a/crates/puffin-resolver/src/version_map.rs +++ b/crates/puffin-resolver/src/version_map.rs @@ -11,7 +11,7 @@ use platform_tags::{TagPriority, Tags}; use puffin_client::SimpleMetadata; use puffin_normalize::PackageName; use puffin_warnings::warn_user_once; -use pypi_types::{BaseUrl, Yanked}; +use pypi_types::{BaseUrl, Hashes, Yanked}; use crate::pubgrub::PubGrubVersion; use crate::python_requirement::PythonRequirement; @@ -42,7 +42,7 @@ impl VersionMap { for (version, files) in metadata { for (filename, file) in files.all() { // 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 { match file.upload_time.as_ref() { Some(upload_time) if upload_time >= exclude_newer => { @@ -50,9 +50,8 @@ impl VersionMap { } None => { warn_user_once!( - "{} is missing an upload date, but user provided: {}", + "{} is missing an upload date, but user provided: {exclude_newer}", file.filename, - exclude_newer, ); continue; } @@ -69,7 +68,9 @@ impl VersionMap { } } + // Prioritize amongst all available files. let requires_python = file.requires_python.clone(); + let hash = file.hashes.clone(); match filename { DistFilename::WheelFilename(filename) => { // To be compatible, the wheel must both have compatible tags _and_ have a @@ -92,12 +93,13 @@ impl VersionMap { Entry::Occupied(mut entry) => { entry .get_mut() - .insert_built(dist, requires_python, priority); + .insert_built(dist, requires_python, hash, priority); } Entry::Vacant(entry) => { entry.insert(PrioritizedDistribution::from_built( dist, requires_python, + hash, priority, )); } @@ -113,12 +115,13 @@ impl VersionMap { ); match version_map.entry(version.clone().into()) { 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.insert(PrioritizedDistribution::from_source( dist, requires_python, + hash, )); } } @@ -143,6 +146,14 @@ impl VersionMap { .iter() .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 { + 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 @@ -161,6 +172,8 @@ struct PrioritizedDistribution { compatible_wheel: Option<(DistRequiresPython, TagPriority)>, /// An arbitrary, platform-incompatible wheel for the package version. incompatible_wheel: Option, + /// The hashes for each distribution. + hashes: Vec, } impl PrioritizedDistribution { @@ -168,6 +181,7 @@ impl PrioritizedDistribution { fn from_built( dist: Dist, requires_python: Option, + hash: Hashes, priority: Option, ) -> Self { if let Some(priority) = priority { @@ -182,6 +196,7 @@ impl PrioritizedDistribution { priority, )), incompatible_wheel: None, + hashes: vec![hash], } } else { Self { @@ -191,12 +206,13 @@ impl PrioritizedDistribution { dist, requires_python, }), + hashes: vec![hash], } } } /// Create a new [`PrioritizedDistribution`] from the given source distribution. - fn from_source(dist: Dist, requires_python: Option) -> Self { + fn from_source(dist: Dist, requires_python: Option, hash: Hashes) -> Self { Self { source: Some(DistRequiresPython { dist, @@ -204,6 +220,7 @@ impl PrioritizedDistribution { }), compatible_wheel: None, incompatible_wheel: None, + hashes: vec![hash], } } @@ -212,6 +229,7 @@ impl PrioritizedDistribution { &mut self, dist: Dist, requires_python: Option, + hash: Hashes, priority: Option, ) { // Prefer the highest-priority, platform-compatible wheel. @@ -241,16 +259,23 @@ impl PrioritizedDistribution { requires_python, }); } + self.hashes.push(hash); } /// Insert the given source distribution into the [`PrioritizedDistribution`]. - fn insert_source(&mut self, dist: Dist, requires_python: Option) { + fn insert_source( + &mut self, + dist: Dist, + requires_python: Option, + hash: Hashes, + ) { if self.source.is_none() { self.source = Some(DistRequiresPython { dist, requires_python, }); } + self.hashes.push(hash); } /// Return the highest-priority distribution for the package version, if any. diff --git a/crates/puffin-resolver/tests/resolver.rs b/crates/puffin-resolver/tests/resolver.rs index ae2c51e5c..0815ed226 100644 --- a/crates/puffin-resolver/tests/resolver.rs +++ b/crates/puffin-resolver/tests/resolver.rs @@ -18,7 +18,8 @@ use puffin_cache::Cache; use puffin_client::RegistryClientBuilder; use puffin_interpreter::{Interpreter, Virtualenv}; use puffin_resolver::{ - Manifest, PreReleaseMode, ResolutionGraph, ResolutionMode, ResolutionOptions, Resolver, + DisplayResolutionGraph, Manifest, PreReleaseMode, ResolutionGraph, ResolutionMode, + ResolutionOptions, Resolver, }; 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?; - assert_snapshot!(resolution, @r###" + assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###" black==23.9.1 click==8.1.7 # via black @@ -170,7 +171,7 @@ async fn black_colorama() -> Result<()> { let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?; - assert_snapshot!(resolution, @r###" + assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###" black==23.9.1 click==8.1.7 # via black @@ -202,7 +203,7 @@ async fn black_tensorboard() -> Result<()> { let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?; - assert_snapshot!(resolution, @r###" + assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###" black==23.9.1 click==8.1.7 # via black @@ -230,7 +231,7 @@ async fn black_python_310() -> Result<()> { let resolution = resolve(manifest, options, &MARKERS_310, &TAGS_310).await?; - assert_snapshot!(resolution, @r###" + assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###" black==23.9.1 click==8.1.7 # via black @@ -271,7 +272,7 @@ async fn black_mypy_extensions() -> Result<()> { let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?; - assert_snapshot!(resolution, @r###" + assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###" black==23.9.1 click==8.1.7 # via black @@ -308,7 +309,7 @@ async fn black_mypy_extensions_extra() -> Result<()> { let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?; - assert_snapshot!(resolution, @r###" + assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###" black==23.9.1 click==8.1.7 # via black @@ -345,7 +346,7 @@ async fn black_flake8() -> Result<()> { let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?; - assert_snapshot!(resolution, @r###" + assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###" black==23.9.1 click==8.1.7 # via black @@ -373,7 +374,7 @@ async fn black_lowest() -> Result<()> { let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?; - assert_snapshot!(resolution, @r###" + assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###" black==22.1.0 click==8.0.0 # via black @@ -401,7 +402,7 @@ async fn black_lowest_direct() -> Result<()> { let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?; - assert_snapshot!(resolution, @r###" + assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###" black==22.1.0 click==8.1.7 # via black @@ -436,7 +437,7 @@ async fn black_respect_preference() -> Result<()> { let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?; - assert_snapshot!(resolution, @r###" + assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###" black==23.9.0 click==8.1.7 # via black @@ -471,7 +472,7 @@ async fn black_ignore_preference() -> Result<()> { let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?; - assert_snapshot!(resolution, @r###" + assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###" black==23.9.1 click==8.1.7 # via black @@ -543,7 +544,7 @@ async fn pylint_disallow_prerelease() -> Result<()> { let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?; - assert_snapshot!(resolution, @r###" + assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###" astroid==3.0.1 # via pylint isort==5.12.0 @@ -567,7 +568,7 @@ async fn pylint_allow_prerelease() -> Result<()> { let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?; - assert_snapshot!(resolution, @r###" + assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###" astroid==3.0.1 # via pylint 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?; - assert_snapshot!(resolution, @r###" + assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###" astroid==3.0.1 # via pylint 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?; - assert_snapshot!(resolution, @r###" + assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###" astroid==3.0.1 # via pylint isort==6.0.0b2 diff --git a/crates/pypi-types/src/simple_json.rs b/crates/pypi-types/src/simple_json.rs index 2832fcb2b..d7c989d76 100644 --- a/crates/pypi-types/src/simple_json.rs +++ b/crates/pypi-types/src/simple_json.rs @@ -85,8 +85,14 @@ impl Yanked { /// /// PEP 691 says multiple hashes can be included and the interpretation is left to the client, we /// only support SHA 256 atm. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct Hashes { // TODO(charlie): Hashes should be optional. 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) + } +}