Add rkyv implementation for Core Metadata (#15532)

## Summary

Enables us to store Core Metadata in zero-copy format.
This commit is contained in:
Charlie Marsh 2025-08-26 10:22:50 -04:00 committed by GitHub
parent 615e076beb
commit b2c8f5ef68
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 94 additions and 4 deletions

View file

@ -97,8 +97,21 @@ impl Default for DefaultExtras {
/// See:
/// - <https://peps.python.org/pep-0685/#specification/>
/// - <https://packaging.python.org/en/latest/specifications/name-normalization/>
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
#[derive(
Debug,
Clone,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
rkyv::Archive,
rkyv::Deserialize,
rkyv::Serialize,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[rkyv(derive(Debug))]
pub struct ExtraName(SmallString);
impl ExtraName {

View file

@ -17,8 +17,20 @@ use crate::{
/// See:
/// - <https://peps.python.org/pep-0735/>
/// - <https://packaging.python.org/en/latest/specifications/name-normalization/>
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[derive(
Debug,
Clone,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
rkyv::Archive,
rkyv::Deserialize,
rkyv::Serialize,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[rkyv(derive(Debug))]
pub struct GroupName(SmallString);
impl GroupName {

View file

@ -30,6 +30,7 @@ boxcar = { workspace = true }
indexmap = { workspace = true }
itertools = { workspace = true }
regex = { workspace = true }
rkyv = { workspace = true, optional = true }
rustc-hash = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive", "rc"] }
@ -56,5 +57,6 @@ schemars = ["dep:schemars"]
# be supported.
non-pep508-extensions = []
default = []
rkyv = ["dep:rkyv"]
# Match the API of the published crate, for compatibility.
serde = []

View file

@ -1045,6 +1045,41 @@ fn parse_pep508_requirement<T: Pep508Url>(
})
}
#[cfg(feature = "rkyv")]
/// An [`rkyv`] implementation for [`Requirement`].
impl<T: Pep508Url + Display> rkyv::Archive for Requirement<T> {
type Archived = rkyv::string::ArchivedString;
type Resolver = rkyv::string::StringResolver;
#[inline]
fn resolve(&self, resolver: Self::Resolver, out: rkyv::Place<Self::Archived>) {
let as_str = self.to_string();
rkyv::string::ArchivedString::resolve_from_str(&as_str, resolver, out);
}
}
#[cfg(feature = "rkyv")]
impl<T: Pep508Url + Display, S> rkyv::Serialize<S> for Requirement<T>
where
S: rkyv::rancor::Fallible + rkyv::ser::Allocator + rkyv::ser::Writer + ?Sized,
S::Error: rkyv::rancor::Source,
{
fn serialize(&self, serializer: &mut S) -> Result<Self::Resolver, S::Error> {
let as_str = self.to_string();
rkyv::string::ArchivedString::serialize_from_str(&as_str, serializer)
}
}
#[cfg(feature = "rkyv")]
impl<T: Pep508Url + Display, D: rkyv::rancor::Fallible + ?Sized>
rkyv::Deserialize<Requirement<T>, D> for rkyv::string::ArchivedString
{
fn deserialize(&self, _deserializer: &mut D) -> Result<Requirement<T>, D::Error> {
// SAFETY: We only serialize valid requirements.
Ok(Requirement::<T>::from_str(self.as_str()).unwrap())
}
}
#[cfg(test)]
mod tests {
//! Half of these tests are copied from <https://github.com/pypa/packaging/pull/624>

View file

@ -21,7 +21,7 @@ uv-distribution-filename = { workspace = true }
uv-git-types = { workspace = true }
uv-normalize = { workspace = true }
uv-pep440 = { workspace = true }
uv-pep508 = { workspace = true }
uv-pep508 = { workspace = true, features = ["rkyv"] }
uv-redacted = { workspace = true }
uv-small-str = { workspace = true }

View file

@ -19,8 +19,11 @@ use crate::{LenientVersionSpecifiers, MetadataError, VerbatimParsedUrl, metadata
/// fields that are relevant to dependency resolution.
///
/// Core Metadata 2.3 is specified in <https://packaging.python.org/specifications/core-metadata/>.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[derive(
Serialize, Deserialize, Debug, Clone, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize,
)]
#[serde(rename_all = "kebab-case")]
#[rkyv(derive(Debug))]
pub struct ResolutionMetadata {
// Mandatory fields
pub name: PackageName,

View file

@ -141,6 +141,18 @@ impl<'de> Deserialize<'de> for CoreMetadata {
}
}
impl Serialize for CoreMetadata {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::Bool(is_available) => serializer.serialize_bool(*is_available),
Self::Hashes(hashes) => hashes.serialize(serializer),
}
}
}
impl CoreMetadata {
pub fn is_available(&self) -> bool {
match self {
@ -169,6 +181,18 @@ impl<'de> Deserialize<'de> for Yanked {
}
}
impl Serialize for Yanked {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::Bool(is_yanked) => serializer.serialize_bool(*is_yanked),
Self::Reason(reason) => serializer.serialize_str(reason.as_ref()),
}
}
}
impl Yanked {
pub fn is_yanked(&self) -> bool {
match self {