Add support for Metadata 2.2 (#2293)

## Summary

PyPI now supports Metadata 2.2, which means distributions with Metadata
2.2-compliant metadata will start to appear. The upside is that if a
source distribution includes a `PKG-INFO` file with (1) a metadata
version of 2.2 or greater, and (2) no dynamic fields (at least, of the
fields we rely on), we can read the metadata from the `PKG-INFO` file
directly rather than running _any_ of the PEP 517 build hooks.

Closes https://github.com/astral-sh/uv/issues/2009.
This commit is contained in:
Charlie Marsh 2024-03-08 08:02:32 -08:00 committed by GitHub
parent 41c911fc41
commit 2e9678e5d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 263 additions and 70 deletions

View file

@ -15,14 +15,16 @@ use uv_normalize::{ExtraName, InvalidNameError, PackageName};
use crate::lenient_requirement::LenientRequirement;
use crate::LenientVersionSpecifiers;
/// Python Package Metadata 2.1 as specified in
/// Python Package Metadata 2.3 as specified in
/// <https://packaging.python.org/specifications/core-metadata/>.
///
/// This is a subset of the full metadata specification, and only includes the
/// fields that are relevant to dependency resolution.
///
/// At present, we support up to version 2.3 of the metadata specification.
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub struct Metadata21 {
pub struct Metadata23 {
// Mandatory fields
pub metadata_version: String,
pub name: PackageName,
@ -70,12 +72,18 @@ pub enum Error {
Pep508Error(#[from] Pep508Error),
#[error(transparent)]
InvalidName(#[from] InvalidNameError),
#[error("Invalid `Metadata-Version` field: {0}")]
InvalidMetadataVersion(String),
#[error("Reading metadata from `PKG-INFO` requires Metadata 2.2 or later (found: {0})")]
UnsupportedMetadataVersion(String),
#[error("The following field was marked as dynamic: {0}")]
DynamicField(&'static str),
}
/// From <https://github.com/PyO3/python-pkginfo-rs/blob/d719988323a0cfea86d4737116d7917f30e819e2/src/metadata.rs#LL78C2-L91C26>
impl Metadata21 {
/// Parse distribution metadata from metadata bytes
pub fn parse(content: &[u8]) -> Result<Self, Error> {
impl Metadata23 {
/// Parse the [`Metadata23`] from a `METADATA` file, as included in a built distribution (wheel).
pub fn parse_metadata(content: &[u8]) -> Result<Self, Error> {
let headers = Headers::parse(content)?;
let metadata_version = headers
@ -124,13 +132,96 @@ impl Metadata21 {
provides_extras,
})
}
/// Read the [`Metadata23`] from a source distribution's `PKG-INFO` file, if it uses Metadata 2.2
/// or later _and_ none of the required fields (`Requires-Python`, `Requires-Dist`, and
/// `Provides-Extra`) are marked as dynamic.
pub fn parse_pkg_info(content: &[u8]) -> Result<Self, Error> {
let headers = Headers::parse(content)?;
// To rely on a source distribution's `PKG-INFO` file, the `Metadata-Version` field must be
// present and set to a value of at least `2.2`.
let metadata_version = headers
.get_first_value("Metadata-Version")
.ok_or(Error::FieldNotFound("Metadata-Version"))?;
// Parse the version into (major, minor).
let (major, minor) = parse_version(&metadata_version)?;
if (major, minor) < (2, 2) || (major, minor) >= (3, 0) {
return Err(Error::UnsupportedMetadataVersion(metadata_version));
}
// If any of the fields we need are marked as dynamic, we can't use the `PKG-INFO` file.
let dynamic = headers.get_all_values("Dynamic").collect::<Vec<_>>();
for field in dynamic {
match field.as_str() {
"Requires-Python" => return Err(Error::DynamicField("Requires-Python")),
"Requires-Dist" => return Err(Error::DynamicField("Requires-Dist")),
"Provides-Extra" => return Err(Error::DynamicField("Provides-Extra")),
_ => (),
}
}
// The `Name` and `Version` fields are required, and can't be dynamic.
let name = PackageName::new(
headers
.get_first_value("Name")
.ok_or(Error::FieldNotFound("Name"))?,
)?;
let version = Version::from_str(
&headers
.get_first_value("Version")
.ok_or(Error::FieldNotFound("Version"))?,
)
.map_err(Error::Pep440VersionError)?;
// The remaining fields are required to be present.
let requires_dist = headers
.get_all_values("Requires-Dist")
.map(|requires_dist| {
LenientRequirement::from_str(&requires_dist).map(Requirement::from)
})
.collect::<Result<Vec<_>, _>>()?;
let requires_python = headers
.get_first_value("Requires-Python")
.map(|requires_python| {
LenientVersionSpecifiers::from_str(&requires_python).map(VersionSpecifiers::from)
})
.transpose()?;
let provides_extras = headers
.get_all_values("Provides-Extra")
.filter_map(|provides_extra| match ExtraName::new(provides_extra) {
Ok(extra_name) => Some(extra_name),
Err(err) => {
warn!("Ignoring invalid extra: {err}");
None
}
})
.collect::<Vec<_>>();
Ok(Self {
metadata_version,
name,
version,
requires_dist,
requires_python,
provides_extras,
})
}
}
impl FromStr for Metadata21 {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s.as_bytes())
}
/// Parse a `Metadata-Version` field into a (major, minor) tuple.
fn parse_version(metadata_version: &str) -> Result<(u8, u8), Error> {
let (major, minor) = metadata_version
.split_once('.')
.ok_or(Error::InvalidMetadataVersion(metadata_version.to_string()))?;
let major = major
.parse::<u8>()
.map_err(|_| Error::InvalidMetadataVersion(metadata_version.to_string()))?;
let minor = minor
.parse::<u8>()
.map_err(|_| Error::InvalidMetadataVersion(metadata_version.to_string()))?;
Ok((major, minor))
}
/// The headers of a distribution metadata file.
@ -174,38 +265,70 @@ mod tests {
use crate::Error;
use super::Metadata21;
use super::Metadata23;
#[test]
fn test_parse_from_str() {
fn test_parse_metadata() {
let s = "Metadata-Version: 1.0";
let meta: Result<Metadata21, Error> = s.parse();
let meta = Metadata23::parse_metadata(s.as_bytes());
assert!(matches!(meta, Err(Error::FieldNotFound("Name"))));
let s = "Metadata-Version: 1.0\nName: asdf";
let meta = Metadata21::parse(s.as_bytes());
let meta = Metadata23::parse_metadata(s.as_bytes());
assert!(matches!(meta, Err(Error::FieldNotFound("Version"))));
let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0";
let meta = Metadata21::parse(s.as_bytes()).unwrap();
let meta = Metadata23::parse_metadata(s.as_bytes()).unwrap();
assert_eq!(meta.metadata_version, "1.0");
assert_eq!(meta.name, PackageName::from_str("asdf").unwrap());
assert_eq!(meta.version, Version::new([1, 0]));
let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0\nAuthor: 中文\n\n一个 Python 包";
let meta = Metadata21::parse(s.as_bytes()).unwrap();
let meta = Metadata23::parse_metadata(s.as_bytes()).unwrap();
assert_eq!(meta.metadata_version, "1.0");
assert_eq!(meta.name, PackageName::from_str("asdf").unwrap());
assert_eq!(meta.version, Version::new([1, 0]));
let s = "Metadata-Version: 1.0\nName: =?utf-8?q?foobar?=\nVersion: 1.0";
let meta = Metadata21::parse(s.as_bytes()).unwrap();
let meta = Metadata23::parse_metadata(s.as_bytes()).unwrap();
assert_eq!(meta.metadata_version, "1.0");
assert_eq!(meta.name, PackageName::from_str("foobar").unwrap());
assert_eq!(meta.version, Version::new([1, 0]));
let s = "Metadata-Version: 1.0\nName: =?utf-8?q?=C3=A4_space?= <x@y.org>\nVersion: 1.0";
let meta = Metadata21::parse(s.as_bytes());
let meta = Metadata23::parse_metadata(s.as_bytes());
assert!(matches!(meta, Err(Error::InvalidName(_))));
}
#[test]
fn test_parse_pkg_info() {
let s = "Metadata-Version: 2.1";
let meta = Metadata23::parse_pkg_info(s.as_bytes());
assert!(matches!(meta, Err(Error::UnsupportedMetadataVersion(_))));
let s = "Metadata-Version: 2.2\nName: asdf";
let meta = Metadata23::parse_pkg_info(s.as_bytes());
assert!(matches!(meta, Err(Error::FieldNotFound("Version"))));
let s = "Metadata-Version: 2.3\nName: asdf";
let meta = Metadata23::parse_pkg_info(s.as_bytes());
assert!(matches!(meta, Err(Error::FieldNotFound("Version"))));
let s = "Metadata-Version: 2.3\nName: asdf\nVersion: 1.0";
let meta = Metadata23::parse_pkg_info(s.as_bytes()).unwrap();
assert_eq!(meta.metadata_version, "2.3");
assert_eq!(meta.name, PackageName::from_str("asdf").unwrap());
assert_eq!(meta.version, Version::new([1, 0]));
let s = "Metadata-Version: 2.3\nName: asdf\nVersion: 1.0\nDynamic: Requires-Dist";
let meta = Metadata23::parse_pkg_info(s.as_bytes()).unwrap_err();
assert!(matches!(meta, Error::DynamicField("Requires-Dist")));
let s = "Metadata-Version: 2.3\nName: asdf\nVersion: 1.0\nRequires-Dist: foo";
let meta = Metadata23::parse_pkg_info(s.as_bytes()).unwrap();
assert_eq!(meta.metadata_version, "2.3");
assert_eq!(meta.name, PackageName::from_str("asdf").unwrap());
assert_eq!(meta.version, Version::new([1, 0]));
assert_eq!(meta.requires_dist, vec!["foo".parse().unwrap()]);
}
}