From 4accbfd915b9634f4bb775fd76baa84ea85bd86d Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Mon, 24 Jun 2024 15:03:45 -0400 Subject: [PATCH] uv-resolver: support unambiguous omission of 'source' and 'version' When there is only one distribution for a particular package name, any dependencies (the edges in the resolution graph) that reference that package name are completely unambiguous. Therefore, we can actually omit their version and source information and instead derive it from the distribution entry. We add some tests to check the success and error cases. That is, when `source` or `version` are omitted and there are more than one corresponding distribution for the package name (i.e., it's ambiguous), then lock deserialization should fail. --- crates/uv-resolver/src/lock.rs | 284 ++++++++++++++++-- ...__missing_dependency_source_ambiguous.snap | 16 + ...missing_dependency_source_unambiguous.snap | 210 +++++++++++++ ...g_dependency_source_version_ambiguous.snap | 16 + ...dependency_source_version_unambiguous.snap | 210 +++++++++++++ ..._missing_dependency_version_ambiguous.snap | 16 + ...issing_dependency_version_unambiguous.snap | 210 +++++++++++++ 7 files changed, 944 insertions(+), 18 deletions(-) create mode 100644 crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_source_ambiguous.snap create mode 100644 crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap create mode 100644 crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_ambiguous.snap create mode 100644 crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap create mode 100644 crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_version_ambiguous.snap create mode 100644 crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap diff --git a/crates/uv-resolver/src/lock.rs b/crates/uv-resolver/src/lock.rs index 63d0b7b5a..7ca42569f 100644 --- a/crates/uv-resolver/src/lock.rs +++ b/crates/uv-resolver/src/lock.rs @@ -453,6 +453,15 @@ impl Lock { doc.insert("requires-python", value(requires_python.to_string())); } + // Count the number of distributions for each package name. When + // there's only one distribution for a particular package name (the + // overwhelmingly common case), we can omit some data (like source and + // version) on dependency edges since it is strictly redundant. + let mut dist_count_by_name: FxHashMap = FxHashMap::default(); + for dist in &self.distributions { + *dist_count_by_name.entry(dist.id.name.clone()).or_default() += 1; + } + let mut distributions = ArrayOfTables::new(); for dist in &self.distributions { let mut table = Table::new(); @@ -469,7 +478,7 @@ impl Lock { let deps = dist .dependencies .iter() - .map(Dependency::to_toml) + .map(|dep| dep.to_toml(&dist_count_by_name)) .collect::(); table.insert("dependencies", Item::ArrayOfTables(deps)); } @@ -479,7 +488,7 @@ impl Lock { for (extra, deps) in &dist.optional_dependencies { let deps = deps .iter() - .map(Dependency::to_toml) + .map(|dep| dep.to_toml(&dist_count_by_name)) .collect::(); optional_deps.insert(extra.as_ref(), Item::ArrayOfTables(deps)); } @@ -491,7 +500,7 @@ impl Lock { for (extra, deps) in &dist.dev_dependencies { let deps = deps .iter() - .map(Dependency::to_toml) + .map(|dep| dep.to_toml(&dist_count_by_name)) .collect::(); dev_dependencies.insert(extra.as_ref(), Item::ArrayOfTables(deps)); } @@ -532,10 +541,27 @@ impl TryFrom for Lock { type Error = LockError; fn try_from(wire: LockWire) -> Result { + // Count the number of distributions for each package name. When + // there's only one distribution for a particular package name (the + // overwhelmingly common case), we can omit some data (like source and + // version) on dependency edges since it is strictly redundant. + let mut unambiguous_dist_ids: FxHashMap = FxHashMap::default(); + let mut ambiguous = FxHashSet::default(); + for dist in &wire.distributions { + if ambiguous.contains(&dist.id.name) { + continue; + } + if unambiguous_dist_ids.remove(&dist.id.name).is_some() { + ambiguous.insert(dist.id.name.clone()); + continue; + } + unambiguous_dist_ids.insert(dist.id.name.clone(), dist.id.clone()); + } + let distributions = wire .distributions .into_iter() - .map(DistributionWire::unwire) + .map(|dist| dist.unwire(&unambiguous_dist_ids)) .collect::, _>>()?; Lock::new(wire.version, distributions, wire.requires_python) } @@ -844,9 +870,14 @@ struct DistributionWire { } impl DistributionWire { - fn unwire(self) -> Result { + fn unwire( + self, + unambiguous_dist_ids: &FxHashMap, + ) -> Result { let unwire_deps = |deps: Vec| -> Result, LockError> { - deps.into_iter().map(DependencyWire::unwire).collect() + deps.into_iter() + .map(|dep| dep.unwire(unambiguous_dist_ids)) + .collect() }; Ok(Distribution { id: self.id, @@ -922,16 +953,38 @@ impl std::fmt::Display for DistributionId { #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)] struct DistributionIdForDependency { name: PackageName, - version: Version, - source: Source, + version: Option, + source: Option, } impl DistributionIdForDependency { - fn unwire(self) -> Result { + fn unwire( + self, + unambiguous_dist_ids: &FxHashMap, + ) -> Result { + let unambiguous_dist_id = unambiguous_dist_ids.get(&self.name); + let version = self.version.map(Ok::<_, LockError>).unwrap_or_else(|| { + let Some(dist_id) = unambiguous_dist_id else { + return Err(LockErrorKind::MissingDependencyVersion { + name: self.name.clone(), + } + .into()); + }; + Ok(dist_id.version.clone()) + })?; + let source = self.source.map(Ok::<_, LockError>).unwrap_or_else(|| { + let Some(dist_id) = unambiguous_dist_id else { + return Err(LockErrorKind::MissingDependencySource { + name: self.name.clone(), + } + .into()); + }; + Ok(dist_id.source.clone()) + })?; Ok(DistributionId { name: self.name, - version: self.version, - source: self.source, + version, + source, }) } } @@ -940,8 +993,8 @@ impl From for DistributionIdForDependency { fn from(id: DistributionId) -> DistributionIdForDependency { DistributionIdForDependency { name: id.name, - version: id.version, - source: id.source, + version: Some(id.version), + source: Some(id.source), } } } @@ -1762,11 +1815,17 @@ impl Dependency { } /// Returns the TOML representation of this dependency. - fn to_toml(&self) -> Table { + fn to_toml(&self, dist_count_by_name: &FxHashMap) -> Table { + let count = dist_count_by_name + .get(&self.distribution_id.name) + .copied() + .expect("all dependencies have a corresponding distribution"); let mut table = Table::new(); table.insert("name", value(self.distribution_id.name.to_string())); - table.insert("version", value(self.distribution_id.version.to_string())); - table.insert("source", value(self.distribution_id.source.to_string())); + if count > 1 { + table.insert("version", value(self.distribution_id.version.to_string())); + table.insert("source", value(self.distribution_id.source.to_string())); + } if let Some(ref extra) = self.extra { table.insert("extra", value(extra.to_string())); } @@ -1813,9 +1872,12 @@ struct DependencyWire { } impl DependencyWire { - fn unwire(self) -> Result { + fn unwire( + self, + unambiguous_dist_ids: &FxHashMap, + ) -> Result { Ok(Dependency { - distribution_id: self.distribution_id.unwire()?, + distribution_id: self.distribution_id.unwire(unambiguous_dist_ids)?, extra: self.extra, marker: self.marker, }) @@ -2034,6 +2096,26 @@ enum LockErrorKind { #[source] err: VerbatimUrlError, }, + /// An error that occurs when an ambiguous `distribution.dependency` is + /// missing a `version` field. + #[error( + "dependency {name} has missing `version` \ + field but has more than one matching distribution" + )] + MissingDependencyVersion { + /// The name of the dependency that is missing a `version` field. + name: PackageName, + }, + /// An error that occurs when an ambiguous `distribution.dependency` is + /// missing a `source` field. + #[error( + "dependency {name} has missing `source` \ + field but has more than one matching distribution" + )] + MissingDependencySource { + /// The name of the dependency that is missing a `source` field. + name: PackageName, + }, } /// An error that occurs when a source string could not be parsed. @@ -2092,6 +2174,172 @@ impl std::fmt::Display for HashParseError { mod tests { use super::*; + #[test] + fn missing_dependency_source_unambiguous() { + let data = r#" +version = 1 + +[[distribution]] +name = "a" +version = "0.1.0" +source = "registry+https://pypi.org/simple" +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[distribution]] +name = "b" +version = "0.1.0" +source = "registry+https://pypi.org/simple" +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[distribution.dependencies]] +name = "a" +version = "0.1.0" +"#; + let result: Result = toml::from_str(data); + insta::assert_debug_snapshot!(result); + } + + #[test] + fn missing_dependency_version_unambiguous() { + let data = r#" +version = 1 + +[[distribution]] +name = "a" +version = "0.1.0" +source = "registry+https://pypi.org/simple" +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[distribution]] +name = "b" +version = "0.1.0" +source = "registry+https://pypi.org/simple" +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[distribution.dependencies]] +name = "a" +source = "registry+https://pypi.org/simple" +"#; + let result: Result = toml::from_str(data); + insta::assert_debug_snapshot!(result); + } + + #[test] + fn missing_dependency_source_version_unambiguous() { + let data = r#" +version = 1 + +[[distribution]] +name = "a" +version = "0.1.0" +source = "registry+https://pypi.org/simple" +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[distribution]] +name = "b" +version = "0.1.0" +source = "registry+https://pypi.org/simple" +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[distribution.dependencies]] +name = "a" +"#; + let result: Result = toml::from_str(data); + insta::assert_debug_snapshot!(result); + } + + #[test] + fn missing_dependency_source_ambiguous() { + let data = r#" +version = 1 + +[[distribution]] +name = "a" +version = "0.1.0" +source = "registry+https://pypi.org/simple" +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[distribution]] +name = "a" +version = "0.1.1" +source = "registry+https://pypi.org/simple" +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[distribution]] +name = "b" +version = "0.1.0" +source = "registry+https://pypi.org/simple" +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[distribution.dependencies]] +name = "a" +version = "0.1.0" +"#; + let result: Result = toml::from_str(data); + insta::assert_debug_snapshot!(result); + } + + #[test] + fn missing_dependency_version_ambiguous() { + let data = r#" +version = 1 + +[[distribution]] +name = "a" +version = "0.1.0" +source = "registry+https://pypi.org/simple" +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[distribution]] +name = "a" +version = "0.1.1" +source = "registry+https://pypi.org/simple" +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[distribution]] +name = "b" +version = "0.1.0" +source = "registry+https://pypi.org/simple" +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[distribution.dependencies]] +name = "a" +source = "registry+https://pypi.org/simple" +"#; + let result: Result = toml::from_str(data); + insta::assert_debug_snapshot!(result); + } + + #[test] + fn missing_dependency_source_version_ambiguous() { + let data = r#" +version = 1 + +[[distribution]] +name = "a" +version = "0.1.0" +source = "registry+https://pypi.org/simple" +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[distribution]] +name = "a" +version = "0.1.1" +source = "registry+https://pypi.org/simple" +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[distribution]] +name = "b" +version = "0.1.0" +source = "registry+https://pypi.org/simple" +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[distribution.dependencies]] +name = "a" +"#; + let result: Result = toml::from_str(data); + insta::assert_debug_snapshot!(result); + } + #[test] fn hash_required_present() { let data = r#" diff --git a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_source_ambiguous.snap b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_source_ambiguous.snap new file mode 100644 index 000000000..e7b0acc3d --- /dev/null +++ b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_source_ambiguous.snap @@ -0,0 +1,16 @@ +--- +source: crates/uv-resolver/src/lock.rs +expression: result +--- +Err( + Error { + inner: Error { + inner: TomlError { + message: "dependency a has missing `source` field but has more than one matching distribution", + raw: None, + keys: [], + span: None, + }, + }, + }, +) diff --git a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap new file mode 100644 index 000000000..fe09635a4 --- /dev/null +++ b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap @@ -0,0 +1,210 @@ +--- +source: crates/uv-resolver/src/lock.rs +expression: result +--- +Ok( + Lock { + version: 1, + distributions: [ + Distribution { + id: DistributionId { + name: PackageName( + "a", + ), + version: "0.1.0", + source: Registry( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + ), + }, + sdist: Some( + Url { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "example.com", + ), + ), + port: None, + path: "/", + query: None, + fragment: None, + }, + metadata: SourceDistMetadata { + hash: Some( + Hash( + HashDigest { + algorithm: Sha256, + digest: "37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + }, + ), + ), + size: Some( + 0, + ), + }, + }, + ), + wheels: [], + dependencies: [], + optional_dependencies: {}, + dev_dependencies: {}, + }, + Distribution { + id: DistributionId { + name: PackageName( + "b", + ), + version: "0.1.0", + source: Registry( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + ), + }, + sdist: Some( + Url { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "example.com", + ), + ), + port: None, + path: "/", + query: None, + fragment: None, + }, + metadata: SourceDistMetadata { + hash: Some( + Hash( + HashDigest { + algorithm: Sha256, + digest: "37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + }, + ), + ), + size: Some( + 0, + ), + }, + }, + ), + wheels: [], + dependencies: [ + Dependency { + distribution_id: DistributionId { + name: PackageName( + "a", + ), + version: "0.1.0", + source: Registry( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + ), + }, + extra: None, + marker: None, + }, + ], + optional_dependencies: {}, + dev_dependencies: {}, + }, + ], + requires_python: None, + by_id: { + DistributionId { + name: PackageName( + "a", + ), + version: "0.1.0", + source: Registry( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + ), + }: 0, + DistributionId { + name: PackageName( + "b", + ), + version: "0.1.0", + source: Registry( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + ), + }: 1, + }, + }, +) diff --git a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_ambiguous.snap b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_ambiguous.snap new file mode 100644 index 000000000..7ce965a3e --- /dev/null +++ b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_ambiguous.snap @@ -0,0 +1,16 @@ +--- +source: crates/uv-resolver/src/lock.rs +expression: result +--- +Err( + Error { + inner: Error { + inner: TomlError { + message: "dependency a has missing `version` field but has more than one matching distribution", + raw: None, + keys: [], + span: None, + }, + }, + }, +) diff --git a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap new file mode 100644 index 000000000..fe09635a4 --- /dev/null +++ b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap @@ -0,0 +1,210 @@ +--- +source: crates/uv-resolver/src/lock.rs +expression: result +--- +Ok( + Lock { + version: 1, + distributions: [ + Distribution { + id: DistributionId { + name: PackageName( + "a", + ), + version: "0.1.0", + source: Registry( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + ), + }, + sdist: Some( + Url { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "example.com", + ), + ), + port: None, + path: "/", + query: None, + fragment: None, + }, + metadata: SourceDistMetadata { + hash: Some( + Hash( + HashDigest { + algorithm: Sha256, + digest: "37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + }, + ), + ), + size: Some( + 0, + ), + }, + }, + ), + wheels: [], + dependencies: [], + optional_dependencies: {}, + dev_dependencies: {}, + }, + Distribution { + id: DistributionId { + name: PackageName( + "b", + ), + version: "0.1.0", + source: Registry( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + ), + }, + sdist: Some( + Url { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "example.com", + ), + ), + port: None, + path: "/", + query: None, + fragment: None, + }, + metadata: SourceDistMetadata { + hash: Some( + Hash( + HashDigest { + algorithm: Sha256, + digest: "37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + }, + ), + ), + size: Some( + 0, + ), + }, + }, + ), + wheels: [], + dependencies: [ + Dependency { + distribution_id: DistributionId { + name: PackageName( + "a", + ), + version: "0.1.0", + source: Registry( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + ), + }, + extra: None, + marker: None, + }, + ], + optional_dependencies: {}, + dev_dependencies: {}, + }, + ], + requires_python: None, + by_id: { + DistributionId { + name: PackageName( + "a", + ), + version: "0.1.0", + source: Registry( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + ), + }: 0, + DistributionId { + name: PackageName( + "b", + ), + version: "0.1.0", + source: Registry( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + ), + }: 1, + }, + }, +) diff --git a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_version_ambiguous.snap b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_version_ambiguous.snap new file mode 100644 index 000000000..7ce965a3e --- /dev/null +++ b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_version_ambiguous.snap @@ -0,0 +1,16 @@ +--- +source: crates/uv-resolver/src/lock.rs +expression: result +--- +Err( + Error { + inner: Error { + inner: TomlError { + message: "dependency a has missing `version` field but has more than one matching distribution", + raw: None, + keys: [], + span: None, + }, + }, + }, +) diff --git a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap new file mode 100644 index 000000000..fe09635a4 --- /dev/null +++ b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap @@ -0,0 +1,210 @@ +--- +source: crates/uv-resolver/src/lock.rs +expression: result +--- +Ok( + Lock { + version: 1, + distributions: [ + Distribution { + id: DistributionId { + name: PackageName( + "a", + ), + version: "0.1.0", + source: Registry( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + ), + }, + sdist: Some( + Url { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "example.com", + ), + ), + port: None, + path: "/", + query: None, + fragment: None, + }, + metadata: SourceDistMetadata { + hash: Some( + Hash( + HashDigest { + algorithm: Sha256, + digest: "37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + }, + ), + ), + size: Some( + 0, + ), + }, + }, + ), + wheels: [], + dependencies: [], + optional_dependencies: {}, + dev_dependencies: {}, + }, + Distribution { + id: DistributionId { + name: PackageName( + "b", + ), + version: "0.1.0", + source: Registry( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + ), + }, + sdist: Some( + Url { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "example.com", + ), + ), + port: None, + path: "/", + query: None, + fragment: None, + }, + metadata: SourceDistMetadata { + hash: Some( + Hash( + HashDigest { + algorithm: Sha256, + digest: "37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + }, + ), + ), + size: Some( + 0, + ), + }, + }, + ), + wheels: [], + dependencies: [ + Dependency { + distribution_id: DistributionId { + name: PackageName( + "a", + ), + version: "0.1.0", + source: Registry( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + ), + }, + extra: None, + marker: None, + }, + ], + optional_dependencies: {}, + dev_dependencies: {}, + }, + ], + requires_python: None, + by_id: { + DistributionId { + name: PackageName( + "a", + ), + version: "0.1.0", + source: Registry( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + ), + }: 0, + DistributionId { + name: PackageName( + "b", + ), + version: "0.1.0", + source: Registry( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + ), + }: 1, + }, + }, +)