mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 10:58:28 +00:00
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:
parent
41c911fc41
commit
2e9678e5d3
14 changed files with 263 additions and 70 deletions
|
@ -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()
|
||||
|
|
|
@ -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()]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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?")]
|
||||
|
|
|
@ -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)?)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue