uv_distribution_types/
dependency_metadata.rs

1use rustc_hash::FxHashMap;
2use serde::{Deserialize, Serialize};
3use tracing::{debug, warn};
4use uv_normalize::{ExtraName, PackageName};
5use uv_pep440::{Version, VersionSpecifiers};
6use uv_pep508::Requirement;
7use uv_pypi_types::{ResolutionMetadata, VerbatimParsedUrl};
8
9/// Pre-defined [`StaticMetadata`] entries, indexed by [`PackageName`] and [`Version`].
10#[derive(Debug, Clone, Default)]
11pub struct DependencyMetadata(FxHashMap<PackageName, Vec<StaticMetadata>>);
12
13impl DependencyMetadata {
14    /// Index a set of [`StaticMetadata`] entries by [`PackageName`] and [`Version`].
15    pub fn from_entries(entries: impl IntoIterator<Item = StaticMetadata>) -> Self {
16        let mut map = Self::default();
17        for entry in entries {
18            map.0.entry(entry.name.clone()).or_default().push(entry);
19        }
20        map
21    }
22
23    /// Retrieve a [`StaticMetadata`] entry by [`PackageName`] and [`Version`].
24    pub fn get(
25        &self,
26        package: &PackageName,
27        version: Option<&Version>,
28    ) -> Option<ResolutionMetadata> {
29        let versions = self.0.get(package)?;
30
31        if let Some(version) = version {
32            // If a specific version was requested, search for an exact match, then a global match.
33            let metadata = if let Some(metadata) = versions
34                .iter()
35                .find(|entry| entry.version.as_ref() == Some(version))
36            {
37                debug!("Found dependency metadata entry for `{package}=={version}`");
38                metadata
39            } else if let Some(metadata) = versions.iter().find(|entry| entry.version.is_none()) {
40                debug!("Found global metadata entry for `{package}`");
41                metadata
42            } else {
43                warn!("No dependency metadata entry found for `{package}=={version}`");
44                return None;
45            };
46
47            Some(ResolutionMetadata {
48                name: metadata.name.clone(),
49                version: version.clone(),
50                requires_dist: metadata.requires_dist.clone(),
51                requires_python: metadata.requires_python.clone(),
52                provides_extra: metadata.provides_extra.clone(),
53                dynamic: false,
54            })
55        } else {
56            // If no version was requested (i.e., it's a direct URL dependency), allow a single
57            // versioned match.
58            let [metadata] = versions.as_slice() else {
59                warn!("Multiple dependency metadata entries found for `{package}`");
60                return None;
61            };
62            let Some(version) = metadata.version.clone() else {
63                warn!("No version found in dependency metadata entry for `{package}`");
64                return None;
65            };
66            debug!("Found dependency metadata entry for `{package}` (assuming: `{version}`)");
67
68            Some(ResolutionMetadata {
69                name: metadata.name.clone(),
70                version,
71                requires_dist: metadata.requires_dist.clone(),
72                requires_python: metadata.requires_python.clone(),
73                provides_extra: metadata.provides_extra.clone(),
74                dynamic: false,
75            })
76        }
77    }
78
79    /// Retrieve all [`StaticMetadata`] entries.
80    pub fn values(&self) -> impl Iterator<Item = &StaticMetadata> {
81        self.0.values().flatten()
82    }
83}
84
85/// A subset of the Python Package Metadata 2.3 standard as specified in
86/// <https://packaging.python.org/specifications/core-metadata/>.
87#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
88#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
89#[serde(rename_all = "kebab-case", deny_unknown_fields)]
90pub struct StaticMetadata {
91    // Mandatory fields
92    pub name: PackageName,
93    #[cfg_attr(
94        feature = "schemars",
95        schemars(
96            with = "Option<String>",
97            description = "PEP 440-style package version, e.g., `1.2.3`"
98        )
99    )]
100    pub version: Option<Version>,
101    // Optional fields
102    #[serde(default)]
103    pub requires_dist: Box<[Requirement<VerbatimParsedUrl>]>,
104    #[cfg_attr(
105        feature = "schemars",
106        schemars(
107            with = "Option<String>",
108            description = "PEP 508-style Python requirement, e.g., `>=3.10`"
109        )
110    )]
111    pub requires_python: Option<VersionSpecifiers>,
112    #[serde(default, alias = "provides-extras")]
113    pub provides_extra: Box<[ExtraName]>,
114}