A variants.json support to flat indexes

This commit is contained in:
konstin 2025-07-03 15:46:32 +02:00
parent 224e639332
commit ab3e837a72
11 changed files with 156 additions and 34 deletions

View file

@ -7,8 +7,7 @@ use url::Url;
use uv_cache::{Cache, CacheBucket};
use uv_cache_key::cache_digest;
use uv_distribution_filename::DistFilename;
use uv_distribution_types::{File, FileLocation, IndexUrl, UrlString};
use uv_distribution_types::{File, FileLocation, IndexEntryFilename, IndexUrl, UrlString};
use uv_pypi_types::HashDigests;
use uv_redacted::DisplaySafeUrl;
use uv_small_str::SmallString;
@ -40,7 +39,7 @@ pub enum FindLinksDirectoryError {
/// An entry in a `--find-links` index.
#[derive(Debug, Clone)]
pub struct FlatIndexEntry {
pub filename: DistFilename,
pub filename: IndexEntryFilename,
pub file: File,
pub index: IndexUrl,
}
@ -238,7 +237,9 @@ impl<'a> FlatIndexClient<'a> {
})
.filter_map(|file| {
Some(FlatIndexEntry {
filename: DistFilename::try_from_normalized_filename(&file.filename)?,
filename: IndexEntryFilename::try_from_normalized_filename(
&file.filename,
)?,
file,
index: flat_index.clone(),
})
@ -307,9 +308,10 @@ impl<'a> FlatIndexClient<'a> {
yanked: None,
};
let Some(filename) = DistFilename::try_from_normalized_filename(filename) else {
// Try to parse as a distribution filename first
let Some(filename) = IndexEntryFilename::try_from_normalized_filename(filename) else {
debug!(
"Ignoring `--find-links` entry (expected a wheel or source distribution filename): {}",
"Ignoring `--find-links` entry (expected a wheel, source distribution, or variants.json filename): {}",
entry.path().display()
);
continue;

View file

@ -1374,4 +1374,63 @@ mod tests {
}
"#);
}
#[test]
fn parse_variants_json() {
// A variants.json without wheels doesn't make much sense, but it's sufficient to test
// parsing.
let text = r#"
<!DOCTYPE html>
<html>
<body>
<h1>Links for jinja2</h1>
<a href="/whl/jinja2-3.1.2-variants.json#sha256=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61">jinja2-3.1.2-variants.json</a><br/>
</body>
</html>
<!--TIMESTAMP 1703347410-->
"#;
let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap();
let result = SimpleHtml::parse(text, &base).unwrap();
insta::assert_debug_snapshot!(result, @r#"
SimpleHtml {
base: BaseUrl(
DisplaySafeUrl {
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"download.pytorch.org",
),
),
port: None,
path: "/whl/jinja2/",
query: None,
fragment: None,
},
),
files: [
File {
core_metadata: None,
filename: "jinja2-3.1.2-variants.json",
hashes: Hashes {
md5: None,
sha256: Some(
"6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61",
),
sha384: None,
sha512: None,
blake2b: None,
},
requires_python: None,
size: None,
upload_time: None,
url: "/whl/jinja2-3.1.2-variants.json",
yanked: None,
},
],
}
"#);
}
}

View file

@ -1,4 +1,6 @@
use std::{borrow::Cow, str::FromStr};
#[cfg(feature = "schemars")]
use std::borrow::Cow;
use std::str::FromStr;
use uv_pep508::PackageName;

View file

@ -1,5 +1,7 @@
#[cfg(feature = "schemars")]
use std::borrow::Cow;
use std::fmt::Formatter;
use std::str::FromStr;
use std::{borrow::Cow, fmt::Formatter};
use uv_pep440::{Version, VersionSpecifier, VersionSpecifiers, VersionSpecifiersParseError};

View file

@ -1,5 +1,7 @@
use serde::{Deserialize, Deserializer};
use std::{borrow::Cow, str::FromStr};
#[cfg(feature = "schemars")]
use std::borrow::Cow;
use std::str::FromStr;
use url::Url;
/// A host specification (wildcard, or host, with optional scheme and/or port) for which

View file

@ -1,9 +1,32 @@
use crate::VariantJson;
use uv_distribution_filename::DistFilename;
use uv_normalize::PackageName;
/// On an index page, there can be wheels, source distributions and `variant.json` files.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DistOrVariantFilename {
use crate::VariantsJson;
/// On an index page, there can be wheels, source distributions and `variants.json` files.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum IndexEntryFilename {
DistFilename(DistFilename),
VariantJson(VariantJson),
VariantJson(VariantsJson),
}
impl IndexEntryFilename {
pub fn name(&self) -> &PackageName {
match self {
Self::DistFilename(filename) => filename.name(),
Self::VariantJson(variant_json) => &variant_json.name,
}
}
/// Parse a filename as either a distribution filename or a `variants.json` filename.
#[allow(clippy::manual_map)]
pub fn try_from_normalized_filename(filename: &str) -> Option<Self> {
if let Some(dist_filename) = DistFilename::try_from_normalized_filename(filename) {
Some(Self::DistFilename(dist_filename))
} else if let Some(variant_json) = VariantsJson::try_from_normalized_filename(filename) {
Some(Self::VariantJson(variant_json))
} else {
None
}
}
}

View file

@ -3,7 +3,9 @@
//! flags set.
use serde::{Deserialize, Deserializer, Serialize};
use std::{borrow::Cow, path::Path};
#[cfg(feature = "schemars")]
use std::borrow::Cow;
use std::path::Path;
use crate::{Index, IndexUrl};

View file

@ -13,7 +13,7 @@ use uv_variants::VariantPriority;
use crate::{
File, InstalledDist, KnownPlatform, RegistryBuiltDist, RegistryBuiltWheel, RegistrySourceDist,
ResolvedDistRef, VariantJson,
ResolvedDistRef, VariantsJson,
};
/// A collection of distributions that have been filtered by relevance.
@ -31,7 +31,7 @@ struct PrioritizedDistInner {
/// The set of all wheels associated with this distribution.
wheels: Vec<(RegistryBuiltWheel, WheelCompatibility)>,
/// The `variants.json` file associated with the package version.
variants_json: Option<VariantJson>,
variants_json: Option<VariantsJson>,
/// The hashes for each distribution.
hashes: Vec<HashDigest>,
/// The set of supported platforms for the distribution, described in terms of their markers.
@ -375,8 +375,8 @@ impl PrioritizedDist {
}))
}
/// Create a new [`PrioritizedDist`] from the `variant.json`.
pub fn from_variant_json(variant_json: VariantJson) -> Self {
/// Create a new [`PrioritizedDist`] from the `variants.json`.
pub fn from_variant_json(variant_json: VariantsJson) -> Self {
Self(Box::new(PrioritizedDistInner {
markers: MarkerTree::TRUE,
best_wheel_index: None,
@ -440,10 +440,10 @@ impl PrioritizedDist {
}
}
pub fn insert_variant_json(&mut self, variant_json: VariantJson) {
pub fn insert_variant_json(&mut self, variant_json: VariantsJson) {
debug_assert!(
self.0.variants_json.is_none(),
"The variant.json filename is unique"
"The variants.json filename is unique"
);
self.0.variants_json = Some(variant_json);
}

View file

@ -1,4 +1,6 @@
use std::{borrow::Cow, ops::Deref};
#[cfg(feature = "schemars")]
use std::borrow::Cow;
use std::ops::Deref;
use http::StatusCode;
use rustc_hash::FxHashSet;

View file

@ -1,11 +1,39 @@
use crate::FileLocation;
use std::str::FromStr;
use uv_normalize::PackageName;
use uv_pep440::Version;
/// A `<name>-<version>-variant.json` file.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VariantJson {
/// A `<name>-<version>-variants.json` file.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct VariantsJson {
pub name: PackageName,
pub version: Version,
pub file_location: FileLocation,
}
impl VariantsJson {
/// Parse a `<name>-<version>-variants.json` filename.
///
/// name and version must be normalized, i.e., they don't contain dashes.
pub fn try_from_normalized_filename(filename: &str) -> Option<Self> {
let stem = filename.strip_suffix("-variants.json")?;
let (name, version) = stem.split_once('-')?;
let name = PackageName::from_str(name).ok()?;
let version = Version::from_str(version).ok()?;
Some(VariantsJson { name, version })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn variants_json_parsing() {
let variant =
VariantsJson::try_from_normalized_filename("numpy-1.21.0-variants.json").unwrap();
assert_eq!(variant.name.as_str(), "numpy");
assert_eq!(variant.version.to_string(), "1.21.0");
}
}

View file

@ -8,7 +8,7 @@ use uv_client::{FlatIndexEntries, FlatIndexEntry};
use uv_configuration::BuildOptions;
use uv_distribution_filename::{DistFilename, SourceDistFilename, WheelFilename};
use uv_distribution_types::{
DistOrVariantFilename, File, HashComparison, HashPolicy, IncompatibleSource, IncompatibleWheel,
File, HashComparison, HashPolicy, IncompatibleSource, IncompatibleWheel, IndexEntryFilename,
IndexUrl, PrioritizedDist, RegistryBuiltWheel, RegistrySourceDist, SourceDistCompatibility,
WheelCompatibility,
};
@ -46,7 +46,7 @@ impl FlatIndex {
let distributions = index.entry(entry.filename.name().clone()).or_default();
distributions.add_file(
entry.file,
DistOrVariantFilename::DistFilename(entry.filename),
entry.filename,
tags,
variants,
hasher,
@ -92,7 +92,7 @@ impl FlatDistributions {
for entry in entries {
distributions.add_file(
entry.file,
DistOrVariantFilename::DistFilename(entry.filename),
entry.filename,
tags,
variants,
hasher,
@ -117,7 +117,7 @@ impl FlatDistributions {
fn add_file(
&mut self,
file: File,
filename: DistOrVariantFilename,
filename: IndexEntryFilename,
tags: Option<&Tags>,
variants: Option<&VariantSet>,
hasher: &HashStrategy,
@ -127,7 +127,7 @@ impl FlatDistributions {
// No `requires-python` here: for source distributions, we don't have that information;
// for wheels, we read it lazily only when selected.
match filename {
DistOrVariantFilename::DistFilename(DistFilename::WheelFilename(filename)) => {
IndexEntryFilename::DistFilename(DistFilename::WheelFilename(filename)) => {
let version = filename.version.clone();
let compatibility = Self::wheel_compatibility(
@ -152,7 +152,7 @@ impl FlatDistributions {
}
}
}
DistOrVariantFilename::DistFilename(DistFilename::SourceDistFilename(filename)) => {
IndexEntryFilename::DistFilename(DistFilename::SourceDistFilename(filename)) => {
let compatibility = Self::source_dist_compatibility(
&filename,
file.hashes.as_slice(),
@ -176,7 +176,7 @@ impl FlatDistributions {
}
}
}
DistOrVariantFilename::VariantJson(variant_json) => {
IndexEntryFilename::VariantJson(variant_json) => {
match self.0.entry(variant_json.version.clone()) {
Entry::Occupied(mut entry) => {
entry.get_mut().insert_variant_json(variant_json);