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

@ -111,10 +111,10 @@ impl InstalledDist {
}
/// Read the `METADATA` file from a `.dist-info` directory.
pub fn metadata(&self) -> Result<pypi_types::Metadata21> {
pub fn metadata(&self) -> Result<pypi_types::Metadata23> {
let path = self.path().join("METADATA");
let contents = fs::read(&path)?;
pypi_types::Metadata21::parse(&contents).with_context(|| {
pypi_types::Metadata23::parse_metadata(&contents).with_context(|| {
format!(
"Failed to parse METADATA file at: {}",
path.simplified_display()

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()]);
}
}

View file

@ -21,7 +21,7 @@ use distribution_filename::{DistFilename, SourceDistFilename, WheelFilename};
use distribution_types::{BuiltDist, File, FileLocation, IndexUrl, IndexUrls, Name};
use install_wheel_rs::{find_dist_info, is_metadata_entry};
use pep440_rs::Version;
use pypi_types::{Metadata21, SimpleJson};
use pypi_types::{Metadata23, SimpleJson};
use uv_auth::safe_copy_url_auth;
use uv_cache::{Cache, CacheBucket, WheelCache};
use uv_normalize::PackageName;
@ -343,7 +343,7 @@ impl RegistryClient {
/// 2. From a remote wheel by partial zip reading
/// 3. From a (temp) download of a remote wheel (this is a fallback, the webserver should support range requests)
#[instrument(skip_all, fields(% built_dist))]
pub async fn wheel_metadata(&self, built_dist: &BuiltDist) -> Result<Metadata21, Error> {
pub async fn wheel_metadata(&self, built_dist: &BuiltDist) -> Result<Metadata23, Error> {
let metadata = match &built_dist {
BuiltDist::Registry(wheel) => match &wheel.file.url {
FileLocation::RelativeUrl(base, url) => {
@ -399,7 +399,7 @@ impl RegistryClient {
index: &IndexUrl,
file: &File,
url: &Url,
) -> Result<Metadata21, Error> {
) -> Result<Metadata23, Error> {
// If the metadata file is available at its own url (PEP 658), download it from there.
let filename = WheelFilename::from_str(&file.filename).map_err(ErrorKind::WheelFilename)?;
if file
@ -428,7 +428,7 @@ impl RegistryClient {
let bytes = response.bytes().await.map_err(ErrorKind::from)?;
info_span!("parse_metadata21")
.in_scope(|| Metadata21::parse(bytes.as_ref()))
.in_scope(|| Metadata23::parse_metadata(bytes.as_ref()))
.map_err(|err| {
Error::from(ErrorKind::MetadataParseError(
filename,
@ -462,7 +462,7 @@ impl RegistryClient {
filename: &'data WheelFilename,
url: &'data Url,
cache_shard: WheelCache<'data>,
) -> Result<Metadata21, Error> {
) -> Result<Metadata23, Error> {
let cache_entry = self.cache.entry(
CacheBucket::Wheels,
cache_shard.remote_wheel_dir(filename.name.as_ref()),
@ -507,14 +507,14 @@ impl RegistryClient {
.map_err(ErrorKind::AsyncHttpRangeReader)?;
trace!("Getting metadata for {filename} by range request");
let text = wheel_metadata_from_remote_zip(filename, &mut reader).await?;
let metadata = Metadata21::parse(text.as_bytes()).map_err(|err| {
let metadata = Metadata23::parse_metadata(text.as_bytes()).map_err(|err| {
Error::from(ErrorKind::MetadataParseError(
filename.clone(),
url.to_string(),
Box::new(err),
))
})?;
Ok::<Metadata21, CachedClientError<Error>>(metadata)
Ok::<Metadata23, CachedClientError<Error>>(metadata)
}
.boxed()
.instrument(info_span!("read_metadata_range_request", wheel = %filename))
@ -591,7 +591,7 @@ async fn read_metadata_async_seek(
filename: &WheelFilename,
debug_source: String,
reader: impl tokio::io::AsyncRead + tokio::io::AsyncSeek + Unpin,
) -> Result<Metadata21, Error> {
) -> Result<Metadata23, Error> {
let mut zip_reader = async_zip::tokio::read::seek::ZipFileReader::with_tokio(reader)
.await
.map_err(|err| ErrorKind::Zip(filename.clone(), err))?;
@ -617,7 +617,7 @@ async fn read_metadata_async_seek(
.await
.map_err(|err| ErrorKind::Zip(filename.clone(), err))?;
let metadata = Metadata21::parse(&contents).map_err(|err| {
let metadata = Metadata23::parse_metadata(&contents).map_err(|err| {
ErrorKind::MetadataParseError(filename.clone(), debug_source, Box::new(err))
})?;
Ok(metadata)
@ -628,7 +628,7 @@ async fn read_metadata_async_stream<R: futures::AsyncRead + Unpin>(
filename: &WheelFilename,
debug_source: String,
reader: R,
) -> Result<Metadata21, Error> {
) -> Result<Metadata23, Error> {
let mut zip = async_zip::base::read::stream::ZipFileReader::new(reader);
while let Some(mut entry) = zip
@ -649,7 +649,7 @@ async fn read_metadata_async_stream<R: futures::AsyncRead + Unpin>(
let mut contents = Vec::new();
reader.read_to_end(&mut contents).await.unwrap();
let metadata = Metadata21::parse(&contents).map_err(|err| {
let metadata = Metadata23::parse_metadata(&contents).map_err(|err| {
ErrorKind::MetadataParseError(filename.clone(), debug_source, Box::new(err))
})?;
return Ok(metadata);

View file

@ -12,7 +12,7 @@ use distribution_types::{
BuiltDist, DirectGitUrl, Dist, FileLocation, IndexLocations, LocalEditable, Name, SourceDist,
};
use platform_tags::Tags;
use pypi_types::Metadata21;
use pypi_types::Metadata23;
use uv_cache::{ArchiveTarget, ArchiveTimestamp, Cache, CacheBucket, WheelCache};
use uv_client::{CacheControl, CachedClientError, Connectivity, RegistryClient};
@ -344,14 +344,14 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context>
/// Either fetch the only wheel metadata (directly from the index or with range requests) or
/// fetch and build the source distribution.
///
/// Returns the [`Metadata21`], along with a "precise" URL for the source distribution, if
/// Returns the [`Metadata23`], along with a "precise" URL for the source distribution, if
/// possible. For example, given a Git dependency with a reference to a branch or tag, return a
/// URL with a precise reference to the current commit of that branch or tag.
#[instrument(skip_all, fields(%dist))]
pub async fn get_or_build_wheel_metadata(
&self,
dist: &Dist,
) -> Result<(Metadata21, Option<Url>), Error> {
) -> Result<(Metadata23, Option<Url>), Error> {
match dist {
Dist::Built(built_dist) => {
Ok((self.client.wheel_metadata(built_dist).boxed().await?, None))
@ -393,7 +393,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context>
&self,
editable: &LocalEditable,
editable_wheel_dir: &Path,
) -> Result<(LocalWheel, Metadata21), Error> {
) -> Result<(LocalWheel, Metadata23), Error> {
let (dist, disk_filename, filename, metadata) = self
.builder
.build_editable(editable, editable_wheel_dir)

View file

@ -59,6 +59,10 @@ pub enum Error {
Extract(#[from] uv_extract::Error),
#[error("Source distribution not found at: {0}")]
NotFound(PathBuf),
#[error("The source distribution is missing a `PKG-INFO` file")]
MissingPkgInfo,
#[error("The source distribution does not support static metadata")]
DynamicPkgInfo(#[source] pypi_types::Error),
/// Should not occur; only seen when another task panicked.
#[error("The task executor is broken, did some other task panic?")]

View file

@ -22,7 +22,7 @@ use distribution_types::{
use install_wheel_rs::read_dist_info;
use pep508_rs::VerbatimUrl;
use platform_tags::Tags;
use pypi_types::Metadata21;
use pypi_types::Metadata23;
use uv_cache::{
ArchiveTimestamp, CacheBucket, CacheEntry, CacheShard, CachedByTimestamp, Freshness, WheelCache,
};
@ -174,7 +174,7 @@ impl<'a, T: BuildContext> SourceDistCachedBuilder<'a, T> {
pub async fn download_and_build_metadata(
&self,
source_dist: &SourceDist,
) -> Result<Metadata21, Error> {
) -> Result<Metadata23, Error> {
let metadata = match &source_dist {
SourceDist::DirectUrl(direct_url_source_dist) => {
let filename = direct_url_source_dist
@ -376,7 +376,7 @@ impl<'a, T: BuildContext> SourceDistCachedBuilder<'a, T> {
url: &'data Url,
cache_shard: &CacheShard,
subdirectory: Option<&'data Path>,
) -> Result<Metadata21, Error> {
) -> Result<Metadata23, Error> {
let cache_entry = cache_shard.entry(MANIFEST);
let cache_control = match self.client.connectivity() {
Connectivity::Online => CacheControl::from(
@ -564,7 +564,7 @@ impl<'a, T: BuildContext> SourceDistCachedBuilder<'a, T> {
source_dist: &SourceDist,
path_source_dist: &PathSourceDist,
source_root: &Path,
) -> Result<Metadata21, Error> {
) -> Result<Metadata23, Error> {
let cache_shard = self.build_context.cache().shard(
CacheBucket::BuiltWheels,
WheelCache::Path(&path_source_dist.url)
@ -712,7 +712,7 @@ impl<'a, T: BuildContext> SourceDistCachedBuilder<'a, T> {
&self,
source_dist: &SourceDist,
git_source_dist: &GitSourceDist,
) -> Result<Metadata21, Error> {
) -> Result<Metadata23, Error> {
let (fetch, subdirectory) = self.download_source_dist_git(&git_source_dist.url).await?;
let git_sha = fetch.git().precise().expect("Exact commit after checkout");
@ -913,7 +913,7 @@ impl<'a, T: BuildContext> SourceDistCachedBuilder<'a, T> {
source_dist: &Path,
subdirectory: Option<&Path>,
cache_shard: &CacheShard,
) -> Result<(String, WheelFilename, Metadata21), Error> {
) -> Result<(String, WheelFilename, Metadata23), Error> {
debug!("Building: {dist}");
// Guard against build of source distributions when disabled
@ -966,16 +966,37 @@ impl<'a, T: BuildContext> SourceDistCachedBuilder<'a, T> {
async fn build_source_dist_metadata(
&self,
dist: &SourceDist,
source_dist: &Path,
source_tree: &Path,
subdirectory: Option<&Path>,
) -> Result<Option<Metadata21>, Error> {
) -> Result<Option<Metadata23>, Error> {
debug!("Preparing metadata for: {dist}");
// Attempt to read static metadata from the source distribution.
match read_pkg_info(source_tree).await {
Ok(metadata) => {
debug!("Found static metadata for: {dist}");
// Validate the metadata.
if &metadata.name != dist.name() {
return Err(Error::NameMismatch {
metadata: metadata.name,
given: dist.name().clone(),
});
}
return Ok(Some(metadata));
}
Err(err @ (Error::MissingPkgInfo | Error::DynamicPkgInfo(_))) => {
debug!("No static metadata available for: {dist} ({err:?})");
}
Err(err) => return Err(err),
}
// Setup the builder.
let mut builder = self
.build_context
.setup_build(
source_dist,
source_tree,
subdirectory,
&dist.to_string(),
Some(dist),
@ -998,7 +1019,7 @@ impl<'a, T: BuildContext> SourceDistCachedBuilder<'a, T> {
let content = fs::read(dist_info.join("METADATA"))
.await
.map_err(Error::CacheRead)?;
let metadata = Metadata21::parse(&content)?;
let metadata = Metadata23::parse_metadata(&content)?;
// Validate the metadata.
if &metadata.name != dist.name() {
@ -1016,7 +1037,7 @@ impl<'a, T: BuildContext> SourceDistCachedBuilder<'a, T> {
&self,
editable: &LocalEditable,
editable_wheel_dir: &Path,
) -> Result<(Dist, String, WheelFilename, Metadata21), Error> {
) -> Result<(Dist, String, WheelFilename, Metadata23), Error> {
debug!("Building (editable) {editable}");
// Verify that the editable exists.
@ -1074,6 +1095,25 @@ impl ExtractedSource<'_> {
}
}
/// 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(crate) async fn read_pkg_info(source_tree: &Path) -> Result<Metadata23, Error> {
// Read the `PKG-INFO` file.
let content = match fs::read(source_tree.join("PKG-INFO")).await {
Ok(content) => content,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return Err(Error::MissingPkgInfo);
}
Err(err) => return Err(Error::CacheRead(err)),
};
// Parse the metadata.
let metadata = Metadata23::parse_pkg_info(&content).map_err(Error::DynamicPkgInfo)?;
Ok(metadata)
}
/// Read an existing HTTP-cached [`Manifest`], if it exists.
pub(crate) fn read_http_manifest(cache_entry: &CacheEntry) -> Result<Option<Manifest>, Error> {
match fs_err::File::open(cache_entry.path()) {
@ -1139,25 +1179,25 @@ pub(crate) async fn refresh_timestamp_manifest(
Ok(manifest)
}
/// Read an existing cached [`Metadata21`], if it exists.
/// Read an existing cached [`Metadata23`], if it exists.
pub(crate) async fn read_cached_metadata(
cache_entry: &CacheEntry,
) -> Result<Option<Metadata21>, Error> {
) -> Result<Option<Metadata23>, Error> {
match fs::read(&cache_entry.path()).await {
Ok(cached) => Ok(Some(rmp_serde::from_slice::<Metadata21>(&cached)?)),
Ok(cached) => Ok(Some(rmp_serde::from_slice::<Metadata23>(&cached)?)),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(err) => Err(Error::CacheRead(err)),
}
}
/// Read the [`Metadata21`] from a built wheel.
/// Read the [`Metadata23`] from a built wheel.
fn read_wheel_metadata(
filename: &WheelFilename,
wheel: impl Into<PathBuf>,
) -> Result<Metadata21, Error> {
) -> Result<Metadata23, Error> {
let file = fs_err::File::open(wheel).map_err(Error::CacheRead)?;
let reader = std::io::BufReader::new(file);
let mut archive = ZipArchive::new(reader)?;
let dist_info = read_dist_info(filename, &mut archive)?;
Ok(Metadata21::parse(&dist_info)?)
Ok(Metadata23::parse_metadata(&dist_info)?)
}

View file

@ -4,7 +4,7 @@ use serde::Deserialize;
use distribution_types::{
CachedDist, InstalledDist, InstalledMetadata, InstalledVersion, LocalEditable, Name,
};
use pypi_types::Metadata21;
use pypi_types::Metadata23;
use requirements_txt::EditableRequirement;
use uv_normalize::PackageName;
@ -14,7 +14,7 @@ use uv_normalize::PackageName;
pub struct BuiltEditable {
pub editable: LocalEditable,
pub wheel: CachedDist,
pub metadata: Metadata21,
pub metadata: Metadata23,
}
/// An editable distribution that has been resolved to a concrete distribution.

View file

@ -3,16 +3,16 @@ use std::hash::BuildHasherDefault;
use rustc_hash::FxHashMap;
use distribution_types::LocalEditable;
use pypi_types::Metadata21;
use pypi_types::Metadata23;
use uv_normalize::PackageName;
/// A set of editable packages, indexed by package name.
#[derive(Debug, Default, Clone)]
pub(crate) struct Editables(FxHashMap<PackageName, (LocalEditable, Metadata21)>);
pub(crate) struct Editables(FxHashMap<PackageName, (LocalEditable, Metadata23)>);
impl Editables {
/// Create a new set of editables from a set of requirements.
pub(crate) fn from_requirements(requirements: Vec<(LocalEditable, Metadata21)>) -> Self {
pub(crate) fn from_requirements(requirements: Vec<(LocalEditable, Metadata23)>) -> Self {
let mut editables =
FxHashMap::with_capacity_and_hasher(requirements.len(), BuildHasherDefault::default());
for (editable_requirement, metadata) in requirements {
@ -22,12 +22,12 @@ impl Editables {
}
/// Get the editable for a package.
pub(crate) fn get(&self, name: &PackageName) -> Option<&(LocalEditable, Metadata21)> {
pub(crate) fn get(&self, name: &PackageName) -> Option<&(LocalEditable, Metadata23)> {
self.0.get(name)
}
/// Iterate over all editables.
pub(crate) fn iter(&self) -> impl Iterator<Item = &(LocalEditable, Metadata21)> {
pub(crate) fn iter(&self) -> impl Iterator<Item = &(LocalEditable, Metadata23)> {
self.0.values()
}
}

View file

@ -1,6 +1,6 @@
use distribution_types::LocalEditable;
use pep508_rs::Requirement;
use pypi_types::Metadata21;
use pypi_types::Metadata23;
use uv_normalize::PackageName;
/// A manifest of requirements, constraints, and preferences.
@ -11,7 +11,7 @@ pub struct Manifest {
pub(crate) overrides: Vec<Requirement>,
pub(crate) preferences: Vec<Requirement>,
pub(crate) project: Option<PackageName>,
pub(crate) editables: Vec<(LocalEditable, Metadata21)>,
pub(crate) editables: Vec<(LocalEditable, Metadata23)>,
}
impl Manifest {
@ -21,7 +21,7 @@ impl Manifest {
overrides: Vec<Requirement>,
preferences: Vec<Requirement>,
project: Option<PackageName>,
editables: Vec<(LocalEditable, Metadata21)>,
editables: Vec<(LocalEditable, Metadata23)>,
) -> Self {
Self {
requirements,

View file

@ -15,7 +15,7 @@ use url::Url;
use distribution_types::{Dist, DistributionMetadata, LocalEditable, Name, PackageId, Verbatim};
use once_map::OnceMap;
use pep440_rs::Version;
use pypi_types::{Hashes, Metadata21};
use pypi_types::{Hashes, Metadata23};
use uv_normalize::{ExtraName, PackageName};
use crate::editables::Editables;
@ -57,7 +57,7 @@ impl ResolutionGraph {
selection: &SelectedDependencies<PubGrubPackage, Version>,
pins: &FilePins,
packages: &OnceMap<PackageName, VersionsResponse>,
distributions: &OnceMap<PackageId, Metadata21>,
distributions: &OnceMap<PackageId, Metadata23>,
redirects: &DashMap<Url, Url>,
state: &State<PubGrubPackage, Range<Version>, PubGrubPriority>,
editables: Editables,

View file

@ -3,7 +3,7 @@ use url::Url;
use distribution_types::PackageId;
use once_map::OnceMap;
use pypi_types::Metadata21;
use pypi_types::Metadata23;
use uv_normalize::PackageName;
use super::provider::VersionsResponse;
@ -16,7 +16,7 @@ pub struct InMemoryIndex {
pub(crate) packages: OnceMap<PackageName, VersionsResponse>,
/// A map from package ID to metadata for that distribution.
pub(crate) distributions: OnceMap<PackageId, Metadata21>,
pub(crate) distributions: OnceMap<PackageId, Metadata23>,
/// A map from source URL to precise URL. For example, the source URL
/// `git+https://github.com/pallets/flask.git` could be redirected to

View file

@ -25,7 +25,7 @@ use distribution_types::{
use pep440_rs::{Version, VersionSpecifiers, MIN_VERSION};
use pep508_rs::{MarkerEnvironment, Requirement};
use platform_tags::{IncompatibleTag, Tags};
use pypi_types::{Metadata21, Yanked};
use pypi_types::{Metadata23, Yanked};
pub(crate) use urls::Urls;
use uv_client::{FlatIndex, RegistryClient};
use uv_distribution::DistributionDatabase;
@ -1133,7 +1133,7 @@ enum Response {
/// The returned metadata for a distribution.
Dist {
dist: Dist,
metadata: Metadata21,
metadata: Metadata23,
precise: Option<Url>,
},
}

View file

@ -6,7 +6,7 @@ use url::Url;
use distribution_types::{Dist, IndexLocations};
use platform_tags::Tags;
use pypi_types::Metadata21;
use pypi_types::Metadata23;
use uv_client::{FlatIndex, RegistryClient};
use uv_distribution::DistributionDatabase;
use uv_normalize::PackageName;
@ -16,7 +16,7 @@ use crate::python_requirement::PythonRequirement;
use crate::version_map::VersionMap;
pub type PackageVersionsResult = Result<VersionsResponse, uv_client::Error>;
pub type WheelMetadataResult = Result<(Metadata21, Option<Url>), uv_distribution::Error>;
pub type WheelMetadataResult = Result<(Metadata23, Option<Url>), uv_distribution::Error>;
/// The response when requesting versions for a package
#[derive(Debug)]

View file

@ -4952,3 +4952,29 @@ dev = ["setuptools"]
Ok(())
}
/// Resolve a source distribution that leverages Metadata 2.2.
#[test]
fn metadata_2_2() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str("pyo3-mixed @ https://files.pythonhosted.org/packages/2b/b8/e04b783d3569d5b61b1dcdfda683ac2e3617340539aecd0f099fbade0b4a/pyo3_mixed-2.1.5.tar.gz")?;
uv_snapshot!(context.compile()
.arg("requirements.in"), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2023-11-18T12:00:00Z requirements.in
boltons==23.1.1
# via pyo3-mixed
pyo3-mixed @ https://files.pythonhosted.org/packages/2b/b8/e04b783d3569d5b61b1dcdfda683ac2e3617340539aecd0f099fbade0b4a/pyo3_mixed-2.1.5.tar.gz
----- stderr -----
Resolved 2 packages in [TIME]
"###
);
Ok(())
}