diff --git a/crates/uv-client/src/flat_index.rs b/crates/uv-client/src/flat_index.rs index 91668c5c4..c30df5199 100644 --- a/crates/uv-client/src/flat_index.rs +++ b/crates/uv-client/src/flat_index.rs @@ -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; diff --git a/crates/uv-client/src/html.rs b/crates/uv-client/src/html.rs index 1b6b01347..d335f38c5 100644 --- a/crates/uv-client/src/html.rs +++ b/crates/uv-client/src/html.rs @@ -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#" + + + +

Links for jinja2

+jinja2-3.1.2-variants.json
+ + + + "#; + 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, + }, + ], + } + "#); + } } diff --git a/crates/uv-configuration/src/name_specifiers.rs b/crates/uv-configuration/src/name_specifiers.rs index 1fd25ea0b..3efeee1f2 100644 --- a/crates/uv-configuration/src/name_specifiers.rs +++ b/crates/uv-configuration/src/name_specifiers.rs @@ -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; diff --git a/crates/uv-configuration/src/required_version.rs b/crates/uv-configuration/src/required_version.rs index 7135dfdab..93327a0ac 100644 --- a/crates/uv-configuration/src/required_version.rs +++ b/crates/uv-configuration/src/required_version.rs @@ -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}; diff --git a/crates/uv-configuration/src/trusted_host.rs b/crates/uv-configuration/src/trusted_host.rs index 9f3efb6fc..07ff2998a 100644 --- a/crates/uv-configuration/src/trusted_host.rs +++ b/crates/uv-configuration/src/trusted_host.rs @@ -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 diff --git a/crates/uv-distribution-types/src/dist_or_variant_filename.rs b/crates/uv-distribution-types/src/dist_or_variant_filename.rs index bb8c4f536..69e2058b2 100644 --- a/crates/uv-distribution-types/src/dist_or_variant_filename.rs +++ b/crates/uv-distribution-types/src/dist_or_variant_filename.rs @@ -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 { + 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 + } + } } diff --git a/crates/uv-distribution-types/src/pip_index.rs b/crates/uv-distribution-types/src/pip_index.rs index 888c0df0d..18671e42f 100644 --- a/crates/uv-distribution-types/src/pip_index.rs +++ b/crates/uv-distribution-types/src/pip_index.rs @@ -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}; diff --git a/crates/uv-distribution-types/src/prioritized_distribution.rs b/crates/uv-distribution-types/src/prioritized_distribution.rs index 847140b22..d68568eea 100644 --- a/crates/uv-distribution-types/src/prioritized_distribution.rs +++ b/crates/uv-distribution-types/src/prioritized_distribution.rs @@ -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, + variants_json: Option, /// The hashes for each distribution. hashes: Vec, /// 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); } diff --git a/crates/uv-distribution-types/src/status_code_strategy.rs b/crates/uv-distribution-types/src/status_code_strategy.rs index 709f68be1..b019d0329 100644 --- a/crates/uv-distribution-types/src/status_code_strategy.rs +++ b/crates/uv-distribution-types/src/status_code_strategy.rs @@ -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; diff --git a/crates/uv-distribution-types/src/variant_json.rs b/crates/uv-distribution-types/src/variant_json.rs index cf2d2ae56..4ecf55193 100644 --- a/crates/uv-distribution-types/src/variant_json.rs +++ b/crates/uv-distribution-types/src/variant_json.rs @@ -1,11 +1,39 @@ -use crate::FileLocation; +use std::str::FromStr; + use uv_normalize::PackageName; use uv_pep440::Version; -/// A `--variant.json` file. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct VariantJson { +/// A `--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 `--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 { + 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"); + } } diff --git a/crates/uv-resolver/src/flat_index.rs b/crates/uv-resolver/src/flat_index.rs index 1d3b1b454..0de5f189d 100644 --- a/crates/uv-resolver/src/flat_index.rs +++ b/crates/uv-resolver/src/flat_index.rs @@ -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);