Allow multiple pinned indexes in tool.uv.sources (#7769)

## Summary

This PR lifts the restriction that a package must come from a single
index. For example, you can now do:

```toml
[project]
name = "project"
version = "0.1.0"
readme = "README.md"
requires-python = ">=3.12"
dependencies = ["jinja2"]

[tool.uv.sources]
jinja2 = [
    { index = "torch-cu118", marker = "sys_platform == 'darwin'"},
    { index = "torch-cu124", marker = "sys_platform != 'darwin'"},
]

[[tool.uv.index]]
name = "torch-cu118"
url = "https://download.pytorch.org/whl/cu118"

[[tool.uv.index]]
name = "torch-cu124"
url = "https://download.pytorch.org/whl/cu124"
```

The construction is very similar to the way we handle URLs today: you
can have multiple URLs for a given package, but they must appear in
disjoint forks. So most of the code is just adding that abstraction to
the resolver, following our handling of URLs.

Closes #7761.
This commit is contained in:
Charlie Marsh 2024-10-15 15:58:15 -07:00 committed by GitHub
parent ad24cee7c6
commit 9a76e47888
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 715 additions and 109 deletions

View file

@ -53,6 +53,16 @@ pub enum ResolveError {
fork_markers: MarkerTree,
},
#[error("Requirements contain conflicting indexes for package `{0}`:\n- {}", _1.join("\n- "))]
ConflictingIndexesUniversal(PackageName, Vec<String>),
#[error("Requirements contain conflicting indexes for package `{package_name}` in split `{fork_markers:?}`:\n- {}", indexes.join("\n- "))]
ConflictingIndexesFork {
package_name: PackageName,
indexes: Vec<String>,
fork_markers: MarkerTree,
},
#[error("Requirements contain conflicting indexes for package `{0}`: `{1}` vs. `{2}`")]
ConflictingIndexes(PackageName, String, String),

View file

@ -0,0 +1,48 @@
use rustc_hash::FxHashMap;
use uv_distribution_types::IndexUrl;
use uv_normalize::PackageName;
use crate::resolver::ResolverMarkers;
use crate::ResolveError;
/// See [`crate::resolver::ForkState`].
#[derive(Default, Debug, Clone)]
pub(crate) struct ForkIndexes(FxHashMap<PackageName, IndexUrl>);
impl ForkIndexes {
/// Get the [`IndexUrl`] previously used for a package in this fork.
pub(crate) fn get(&self, package_name: &PackageName) -> Option<&IndexUrl> {
self.0.get(package_name)
}
/// Check that this is the only [`IndexUrl`] used for this package in this fork.
pub(crate) fn insert(
&mut self,
package_name: &PackageName,
index: &IndexUrl,
fork_markers: &ResolverMarkers,
) -> Result<(), ResolveError> {
if let Some(previous) = self.0.insert(package_name.clone(), index.clone()) {
if &previous != index {
let mut conflicts = vec![previous.to_string(), index.to_string()];
conflicts.sort();
return match fork_markers {
ResolverMarkers::Universal { .. } | ResolverMarkers::SpecificEnvironment(_) => {
Err(ResolveError::ConflictingIndexesUniversal(
package_name.clone(),
conflicts,
))
}
ResolverMarkers::Fork(fork_markers) => {
Err(ResolveError::ConflictingIndexesFork {
package_name: package_name.clone(),
indexes: conflicts,
fork_markers: fork_markers.clone(),
})
}
};
}
}
Ok(())
}
}

View file

@ -9,7 +9,7 @@ use uv_pypi_types::VerbatimParsedUrl;
use crate::resolver::ResolverMarkers;
use crate::ResolveError;
/// See [`crate::resolver::SolveState`].
/// See [`crate::resolver::ForkState`].
#[derive(Default, Debug, Clone)]
pub(crate) struct ForkUrls(FxHashMap<PackageName, VerbatimParsedUrl>);

View file

@ -34,6 +34,7 @@ mod error;
mod exclude_newer;
mod exclusions;
mod flat_index;
mod fork_indexes;
mod fork_urls;
mod graph_ops;
mod lock;

View file

@ -10,7 +10,7 @@ use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
use uv_configuration::{Constraints, Overrides};
use uv_distribution::Metadata;
use uv_distribution_types::{
Dist, DistributionMetadata, Name, ResolutionDiagnostic, ResolvedDist, VersionId,
Dist, DistributionMetadata, IndexUrl, Name, ResolutionDiagnostic, ResolvedDist, VersionId,
VersionOrUrlRef,
};
use uv_git::GitResolver;
@ -88,6 +88,7 @@ struct PackageRef<'a> {
package_name: &'a PackageName,
version: &'a Version,
url: Option<&'a VerbatimParsedUrl>,
index: Option<&'a IndexUrl>,
extra: Option<&'a ExtraName>,
group: Option<&'a GroupName>,
}
@ -284,6 +285,7 @@ impl ResolutionGraph {
package_name: from,
version: &edge.from_version,
url: edge.from_url.as_ref(),
index: edge.from_index.as_ref(),
extra: edge.from_extra.as_ref(),
group: edge.from_dev.as_ref(),
}]
@ -292,6 +294,7 @@ impl ResolutionGraph {
package_name: &edge.to,
version: &edge.to_version,
url: edge.to_url.as_ref(),
index: edge.to_index.as_ref(),
extra: edge.to_extra.as_ref(),
group: edge.to_dev.as_ref(),
}];
@ -320,7 +323,7 @@ impl ResolutionGraph {
diagnostics: &mut Vec<ResolutionDiagnostic>,
preferences: &Preferences,
pins: &FilePins,
index: &InMemoryIndex,
in_memory: &InMemoryIndex,
git: &GitResolver,
package: &'a ResolutionPackage,
version: &'a Version,
@ -330,16 +333,18 @@ impl ResolutionGraph {
extra,
dev,
url,
index,
} = &package;
// Map the package to a distribution.
let (dist, hashes, metadata) = Self::parse_dist(
name,
index.as_ref(),
url.as_ref(),
version,
pins,
diagnostics,
preferences,
index,
in_memory,
git,
)?;
@ -366,7 +371,7 @@ impl ResolutionGraph {
}
// Add the distribution to the graph.
let index = petgraph.add_node(ResolutionGraphNode::Dist(AnnotatedDist {
let node = petgraph.add_node(ResolutionGraphNode::Dist(AnnotatedDist {
dist,
name: name.clone(),
version: version.clone(),
@ -381,22 +386,24 @@ impl ResolutionGraph {
package_name: name,
version,
url: url.as_ref(),
index: index.as_ref(),
extra: extra.as_ref(),
group: dev.as_ref(),
},
index,
node,
);
Ok(())
}
fn parse_dist(
name: &PackageName,
index: Option<&IndexUrl>,
url: Option<&VerbatimParsedUrl>,
version: &Version,
pins: &FilePins,
diagnostics: &mut Vec<ResolutionDiagnostic>,
preferences: &Preferences,
index: &InMemoryIndex,
in_memory: &InMemoryIndex,
git: &GitResolver,
) -> Result<(ResolvedDist, Vec<HashDigest>, Option<Metadata>), ResolveError> {
Ok(if let Some(url) = url {
@ -406,14 +413,24 @@ impl ResolutionGraph {
let version_id = VersionId::from_url(&url.verbatim);
// Extract the hashes.
let hashes =
Self::get_hashes(name, Some(url), &version_id, version, preferences, index);
let hashes = Self::get_hashes(
name,
index,
Some(url),
&version_id,
version,
preferences,
in_memory,
);
// Extract the metadata.
let metadata = {
let response = index.distributions().get(&version_id).unwrap_or_else(|| {
panic!("Every URL distribution should have metadata: {version_id:?}")
});
let response = in_memory
.distributions()
.get(&version_id)
.unwrap_or_else(|| {
panic!("Every URL distribution should have metadata: {version_id:?}")
});
let MetadataResponse::Found(archive) = &*response else {
panic!("Every URL distribution should have metadata: {version_id:?}")
@ -449,17 +466,28 @@ impl ResolutionGraph {
}
// Extract the hashes.
let hashes = Self::get_hashes(name, None, &version_id, version, preferences, index);
let hashes = Self::get_hashes(
name,
index,
None,
&version_id,
version,
preferences,
in_memory,
);
// Extract the metadata.
let metadata = {
index.distributions().get(&version_id).and_then(|response| {
if let MetadataResponse::Found(archive) = &*response {
Some(archive.metadata.clone())
} else {
None
}
})
in_memory
.distributions()
.get(&version_id)
.and_then(|response| {
if let MetadataResponse::Found(archive) = &*response {
Some(archive.metadata.clone())
} else {
None
}
})
};
(dist, hashes, metadata)
@ -470,11 +498,12 @@ impl ResolutionGraph {
/// lockfile.
fn get_hashes(
name: &PackageName,
index: Option<&IndexUrl>,
url: Option<&VerbatimParsedUrl>,
version_id: &VersionId,
version: &Version,
preferences: &Preferences,
index: &InMemoryIndex,
in_memory: &InMemoryIndex,
) -> Vec<HashDigest> {
// 1. Look for hashes from the lockfile.
if let Some(digests) = preferences.match_hashes(name, version) {
@ -484,7 +513,7 @@ impl ResolutionGraph {
}
// 2. Look for hashes for the distribution (i.e., the specific wheel or source distribution).
if let Some(metadata_response) = index.distributions().get(version_id) {
if let Some(metadata_response) = in_memory.distributions().get(version_id) {
if let MetadataResponse::Found(ref archive) = *metadata_response {
let mut digests = archive.hashes.clone();
digests.sort_unstable();
@ -496,7 +525,13 @@ impl ResolutionGraph {
// 3. Look for hashes from the registry, which are served at the package level.
if url.is_none() {
if let Some(versions_response) = index.packages().get(name) {
let versions_response = if let Some(index) = index {
in_memory.explicit().get(&(name.clone(), index.clone()))
} else {
in_memory.implicit().get(name)
};
if let Some(versions_response) = versions_response {
if let VersionsResponse::Found(ref version_maps) = *versions_response {
if let Some(digests) = version_maps
.iter()

View file

@ -6,13 +6,12 @@ use rustc_hash::FxHashMap;
use tokio::sync::mpsc::Sender;
use tracing::{debug, trace};
use uv_distribution_types::{CompatibleDist, DistributionMetadata, IndexCapabilities};
use uv_pep440::Version;
use crate::candidate_selector::CandidateSelector;
use crate::pubgrub::{PubGrubPackage, PubGrubPackageInner};
use crate::resolver::Request;
use crate::{InMemoryIndex, PythonRequirement, ResolveError, ResolverMarkers, VersionsResponse};
use uv_distribution_types::{CompatibleDist, DistributionMetadata, IndexCapabilities, IndexUrl};
use uv_pep440::Version;
enum BatchPrefetchStrategy {
/// Go through the next versions assuming the existing selection and its constraints
@ -47,11 +46,12 @@ impl BatchPrefetcher {
pub(crate) fn prefetch_batches(
&mut self,
next: &PubGrubPackage,
index: Option<&IndexUrl>,
version: &Version,
current_range: &Range<Version>,
python_requirement: &PythonRequirement,
request_sink: &Sender<Request>,
index: &InMemoryIndex,
in_memory: &InMemoryIndex,
capabilities: &IndexCapabilities,
selector: &CandidateSelector,
markers: &ResolverMarkers,
@ -73,10 +73,17 @@ impl BatchPrefetcher {
let total_prefetch = min(num_tried, 50);
// This is immediate, we already fetched the version map.
let versions_response = index
.packages()
.wait_blocking(name)
.ok_or_else(|| ResolveError::UnregisteredTask(name.to_string()))?;
let versions_response = if let Some(index) = index {
in_memory
.explicit()
.wait_blocking(&(name.clone(), index.clone()))
.ok_or_else(|| ResolveError::UnregisteredTask(name.to_string()))?
} else {
in_memory
.implicit()
.wait_blocking(name)
.ok_or_else(|| ResolveError::UnregisteredTask(name.to_string()))?
};
let VersionsResponse::Found(ref version_map) = *versions_response else {
return Ok(());
@ -191,7 +198,7 @@ impl BatchPrefetcher {
);
prefetch_count += 1;
if index.distributions().register(candidate.version_id()) {
if in_memory.distributions().register(candidate.version_id()) {
let request = Request::from(dist);
request_sink.blocking_send(request)?;
}

View file

@ -44,6 +44,11 @@ impl<T> ForkMap<T> {
!self.get(package_name, markers).is_empty()
}
/// Returns `true` if the map contains any values for a package.
pub(crate) fn contains_key(&self, package_name: &PackageName) -> bool {
self.0.contains_key(package_name)
}
/// Returns a list of values associated with a package that are compatible with the given fork.
///
/// Compatibility implies that the markers on the requirement that contained this value

View file

@ -2,7 +2,7 @@ use std::hash::BuildHasherDefault;
use std::sync::Arc;
use rustc_hash::FxHasher;
use uv_distribution_types::VersionId;
use uv_distribution_types::{IndexUrl, VersionId};
use uv_normalize::PackageName;
use uv_once_map::OnceMap;
@ -16,7 +16,9 @@ pub struct InMemoryIndex(Arc<SharedInMemoryIndex>);
struct SharedInMemoryIndex {
/// A map from package name to the metadata for that package and the index where the metadata
/// came from.
packages: FxOnceMap<PackageName, Arc<VersionsResponse>>,
implicit: FxOnceMap<PackageName, Arc<VersionsResponse>>,
explicit: FxOnceMap<(PackageName, IndexUrl), Arc<VersionsResponse>>,
/// A map from package ID to metadata for that distribution.
distributions: FxOnceMap<VersionId, Arc<MetadataResponse>>,
@ -26,8 +28,13 @@ pub(crate) type FxOnceMap<K, V> = OnceMap<K, V, BuildHasherDefault<FxHasher>>;
impl InMemoryIndex {
/// Returns a reference to the package metadata map.
pub fn packages(&self) -> &FxOnceMap<PackageName, Arc<VersionsResponse>> {
&self.0.packages
pub fn implicit(&self) -> &FxOnceMap<PackageName, Arc<VersionsResponse>> {
&self.0.implicit
}
/// Returns a reference to the package metadata map.
pub fn explicit(&self) -> &FxOnceMap<(PackageName, IndexUrl), Arc<VersionsResponse>> {
&self.0.explicit
}
/// Returns a reference to the distribution metadata map.

View file

@ -1,12 +1,11 @@
use crate::{DependencyMode, Manifest, ResolveError, ResolverMarkers};
use rustc_hash::FxHashMap;
use std::collections::hash_map::Entry;
use uv_distribution_types::IndexUrl;
use uv_normalize::PackageName;
use uv_pep508::VerbatimUrl;
use uv_pypi_types::RequirementSource;
use crate::resolver::ForkMap;
use crate::{DependencyMode, Manifest, ResolverMarkers};
/// A map of package names to their explicit index.
///
/// For example, given:
@ -21,7 +20,7 @@ use uv_pypi_types::RequirementSource;
///
/// [`Indexes`] would contain a single entry mapping `torch` to `https://download.pytorch.org/whl/cu121`.
#[derive(Debug, Default, Clone)]
pub(crate) struct Indexes(FxHashMap<PackageName, IndexUrl>);
pub(crate) struct Indexes(ForkMap<IndexUrl>);
impl Indexes {
/// Determine the set of explicit, pinned indexes in the [`Manifest`].
@ -29,8 +28,8 @@ impl Indexes {
manifest: &Manifest,
markers: &ResolverMarkers,
dependencies: DependencyMode,
) -> Result<Self, ResolveError> {
let mut indexes = FxHashMap::<PackageName, IndexUrl>::default();
) -> Self {
let mut indexes = ForkMap::default();
for requirement in manifest.requirements(markers, dependencies) {
let RequirementSource::Registry {
@ -40,28 +39,23 @@ impl Indexes {
continue;
};
let index = IndexUrl::from(VerbatimUrl::from_url(index.clone()));
match indexes.entry(requirement.name.clone()) {
Entry::Occupied(entry) => {
let existing = entry.get();
if *existing != index {
return Err(ResolveError::ConflictingIndexes(
requirement.name.clone(),
existing.to_string(),
index.to_string(),
));
}
}
Entry::Vacant(entry) => {
entry.insert(index);
}
}
indexes.add(&requirement, index);
}
Ok(Self(indexes))
Self(indexes)
}
/// Return the explicit index for a given [`PackageName`].
pub(crate) fn get(&self, package_name: &PackageName) -> Option<&IndexUrl> {
self.0.get(package_name)
/// Returns `true` if the map contains any indexes for a package.
pub(crate) fn contains_key(&self, name: &PackageName) -> bool {
self.0.contains_key(name)
}
/// Return the explicit index used for a package in the given fork.
pub(crate) fn get(
&self,
package_name: &PackageName,
markers: &ResolverMarkers,
) -> Vec<&IndexUrl> {
self.0.get(package_name, markers)
}
}

View file

@ -44,6 +44,7 @@ use uv_warnings::warn_user_once;
use crate::candidate_selector::{CandidateDist, CandidateSelector};
use crate::dependency_provider::UvDependencyProvider;
use crate::error::{NoSolutionError, ResolveError};
use crate::fork_indexes::ForkIndexes;
use crate::fork_urls::ForkUrls;
use crate::manifest::Manifest;
use crate::pins::FilePins;
@ -208,7 +209,7 @@ impl<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider>
dependency_mode: options.dependency_mode,
urls: Urls::from_manifest(&manifest, &markers, git, options.dependency_mode)?,
locals: Locals::from_manifest(&manifest, &markers, options.dependency_mode),
indexes: Indexes::from_manifest(&manifest, &markers, options.dependency_mode)?,
indexes: Indexes::from_manifest(&manifest, &markers, options.dependency_mode),
groups: Groups::from_manifest(&manifest, &markers),
project: manifest.project,
workspace_members: manifest.workspace_members,
@ -334,6 +335,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
return Err(self.convert_no_solution_err(
err,
state.fork_urls,
&state.fork_indexes,
state.markers,
&visited,
&self.locations,
@ -345,6 +347,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
Self::pre_visit(
state.pubgrub.partial_solution.prioritized_packages(),
&self.urls,
&self.indexes,
&state.python_requirement,
&request_sink,
)?;
@ -384,7 +387,10 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
state.next = highest_priority_pkg;
let url = state.next.name().and_then(|name| state.fork_urls.get(name));
let index = state.next.name().and_then(|name| self.indexes.get(name));
let index = state
.next
.name()
.and_then(|name| state.fork_indexes.get(name));
// Consider:
// ```toml
@ -408,6 +414,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
.expect("a package was chosen but we don't have a term");
let decision = self.choose_version(
&state.next,
index,
term_intersection.unwrap_positive(),
&mut state.pins,
&preferences,
@ -465,6 +472,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
if url.is_none() {
prefetcher.prefetch_batches(
&state.next,
index,
&version,
term_intersection.unwrap_positive(),
&state.python_requirement,
@ -521,6 +529,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
for_package.as_deref(),
&version,
&self.urls,
&self.indexes,
&self.locals,
dependencies.clone(),
&self.git,
@ -536,7 +545,8 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
url: _,
} = dependency;
let url = package.name().and_then(|name| state.fork_urls.get(name));
let index = package.name().and_then(|name| self.indexes.get(name));
let index =
package.name().and_then(|name| state.fork_indexes.get(name));
self.visit_package(package, url, index, &request_sink)?;
}
}
@ -689,6 +699,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
for_package,
version,
&self.urls,
&self.indexes,
&self.locals,
fork.dependencies.clone(),
&self.git,
@ -705,7 +716,9 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
let url = package
.name()
.and_then(|name| forked_state.fork_urls.get(name));
let index = package.name().and_then(|name| self.indexes.get(name));
let index = package
.name()
.and_then(|name| forked_state.fork_indexes.get(name));
self.visit_package(package, url, index, request_sink)?;
}
Ok(forked_state)
@ -768,10 +781,19 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
if self.index.distributions().register(dist.version_id()) {
request_sink.blocking_send(Request::Dist(dist))?;
}
} else if let Some(index) = index {
// Emit a request to fetch the metadata for this package on the index.
if self
.index
.explicit()
.register((name.clone(), index.clone()))
{
request_sink.blocking_send(Request::Package(name.clone(), Some(index.clone())))?;
}
} else {
// Emit a request to fetch the metadata for this package.
if self.index.packages().register(name.clone()) {
request_sink.blocking_send(Request::Package(name.clone(), index.cloned()))?;
if self.index.implicit().register(name.clone()) {
request_sink.blocking_send(Request::Package(name.clone(), None))?;
}
}
Ok(())
@ -782,6 +804,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
fn pre_visit<'data>(
packages: impl Iterator<Item = (&'data PubGrubPackage, &'data Range<Version>)>,
urls: &Urls,
indexes: &Indexes,
python_requirement: &PythonRequirement,
request_sink: &Sender<Request>,
) -> Result<(), ResolveError> {
@ -802,6 +825,10 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
if urls.any_url(name) {
continue;
}
// Avoid visiting packages that may use an explicit index.
if indexes.contains_key(name) {
continue;
}
request_sink.blocking_send(Request::Prefetch(
name.clone(),
range.clone(),
@ -819,6 +846,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
fn choose_version(
&self,
package: &PubGrubPackage,
index: Option<&IndexUrl>,
range: &Range<Version>,
pins: &mut FilePins,
preferences: &Preferences,
@ -849,6 +877,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
} else {
self.choose_version_registry(
name,
index,
range,
package,
preferences,
@ -964,6 +993,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
fn choose_version_registry(
&self,
name: &PackageName,
index: Option<&IndexUrl>,
range: &Range<Version>,
package: &PubGrubPackage,
preferences: &Preferences,
@ -974,11 +1004,17 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
request_sink: &Sender<Request>,
) -> Result<Option<ResolverVersion>, ResolveError> {
// Wait for the metadata to be available.
let versions_response = self
.index
.packages()
.wait_blocking(name)
.ok_or_else(|| ResolveError::UnregisteredTask(name.to_string()))?;
let versions_response = if let Some(index) = index {
self.index
.explicit()
.wait_blocking(&(name.clone(), index.clone()))
.ok_or_else(|| ResolveError::UnregisteredTask(name.to_string()))?
} else {
self.index
.implicit()
.wait_blocking(name)
.ok_or_else(|| ResolveError::UnregisteredTask(name.to_string()))?
};
visited.insert(name.clone());
let version_maps = match *versions_response {
@ -1654,11 +1690,15 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
while let Some(response) = response_stream.next().await {
match response? {
Some(Response::Package(package_name, version_map)) => {
trace!("Received package metadata for: {package_name}");
self.index
.packages()
.done(package_name, Arc::new(version_map));
Some(Response::Package(name, index, version_map)) => {
trace!("Received package metadata for: {name}");
if let Some(index) = index {
self.index
.explicit()
.done((name, index), Arc::new(version_map));
} else {
self.index.implicit().done(name, Arc::new(version_map));
}
}
Some(Response::Installed { dist, metadata }) => {
trace!("Received installed distribution metadata for: {dist}");
@ -1727,7 +1767,11 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
.await
.map_err(ResolveError::Client)?;
Ok(Some(Response::Package(package_name, package_versions)))
Ok(Some(Response::Package(
package_name,
index,
package_versions,
)))
}
// Fetch distribution metadata from the distribution database.
@ -1771,7 +1815,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
// Wait for the package metadata to become available.
let versions_response = self
.index
.packages()
.implicit()
.wait(&package_name)
.await
.ok_or_else(|| ResolveError::UnregisteredTask(package_name.to_string()))?;
@ -1924,6 +1968,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
&self,
mut err: pubgrub::NoSolutionError<UvDependencyProvider>,
fork_urls: ForkUrls,
fork_indexes: &ForkIndexes,
markers: ResolverMarkers,
visited: &FxHashSet<PackageName>,
index_locations: &IndexLocations,
@ -1965,7 +2010,12 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
// we represent the self of the resolver at the time of failure.
continue;
}
if let Some(response) = self.index.packages().get(name) {
let versions_response = if let Some(index) = fork_indexes.get(name) {
self.index.explicit().get(&(name.clone(), index.clone()))
} else {
self.index.implicit().get(name)
};
if let Some(response) = versions_response {
if let VersionsResponse::Found(ref version_maps) = *response {
// Track the available versions, across all indexes.
for version_map in version_maps {
@ -2049,12 +2099,17 @@ struct ForkState {
/// After resolution is finished, this maps is consulted in order to select
/// the wheel chosen during resolution.
pins: FilePins,
/// Ensure we don't have duplicate urls in any branch.
/// Ensure we don't have duplicate URLs in any branch.
///
/// Unlike [`Urls`], we add only the URLs we have seen in this branch, and there can be only
/// one URL per package. By prioritizing direct URL dependencies over registry dependencies,
/// this map is populated for all direct URL packages before we look at any registry packages.
fork_urls: ForkUrls,
/// Ensure we don't have duplicate indexes in any branch.
///
/// Unlike [`Indexes`], we add only the indexes we have seen in this branch, and there can be
/// only one index per package.
fork_indexes: ForkIndexes,
/// When dependencies for a package are retrieved, this map of priorities
/// is updated based on how each dependency was specified. Certain types
/// of dependencies have more "priority" than others (like direct URL
@ -2106,6 +2161,7 @@ impl ForkState {
next: root,
pins: FilePins::default(),
fork_urls: ForkUrls::default(),
fork_indexes: ForkIndexes::default(),
priorities: PubGrubPriorities::default(),
added_dependencies: FxHashMap::default(),
markers,
@ -2120,6 +2176,7 @@ impl ForkState {
for_package: Option<&str>,
version: &Version,
urls: &Urls,
indexes: &Indexes,
locals: &Locals,
mut dependencies: Vec<PubGrubDependency>,
git: &GitResolver,
@ -2170,6 +2227,11 @@ impl ForkState {
*version = version.union(&local);
}
}
// If the package is pinned to an exact index, add it to the fork.
for index in indexes.get(name, &self.markers) {
self.fork_indexes.insert(name, index, &self.markers)?;
}
}
if let Some(for_package) = for_package {
@ -2319,6 +2381,9 @@ impl ForkState {
_ => continue,
};
let self_url = self_name.as_ref().and_then(|name| self.fork_urls.get(name));
let self_index = self_name
.as_ref()
.and_then(|name| self.fork_indexes.get(name));
match **dependency_package {
PubGrubPackageInner::Package {
@ -2331,15 +2396,18 @@ impl ForkState {
continue;
}
let to_url = self.fork_urls.get(dependency_name);
let to_index = self.fork_indexes.get(dependency_name);
let edge = ResolutionDependencyEdge {
from: self_name.cloned(),
from_version: self_version.clone(),
from_url: self_url.cloned(),
from_index: self_index.cloned(),
from_extra: self_extra.cloned(),
from_dev: self_dev.cloned(),
to: dependency_name.clone(),
to_version: dependency_version.clone(),
to_url: to_url.cloned(),
to_index: to_index.cloned(),
to_extra: dependency_extra.clone(),
to_dev: dependency_dev.clone(),
marker: MarkerTree::TRUE,
@ -2356,15 +2424,18 @@ impl ForkState {
continue;
}
let to_url = self.fork_urls.get(dependency_name);
let to_index = self.fork_indexes.get(dependency_name);
let edge = ResolutionDependencyEdge {
from: self_name.cloned(),
from_version: self_version.clone(),
from_url: self_url.cloned(),
from_index: self_index.cloned(),
from_extra: self_extra.cloned(),
from_dev: self_dev.cloned(),
to: dependency_name.clone(),
to_version: dependency_version.clone(),
to_url: to_url.cloned(),
to_index: to_index.cloned(),
to_extra: None,
to_dev: None,
marker: dependency_marker.clone(),
@ -2382,15 +2453,18 @@ impl ForkState {
continue;
}
let to_url = self.fork_urls.get(dependency_name);
let to_index = self.fork_indexes.get(dependency_name);
let edge = ResolutionDependencyEdge {
from: self_name.cloned(),
from_version: self_version.clone(),
from_url: self_url.cloned(),
from_index: self_index.cloned(),
from_extra: self_extra.cloned(),
from_dev: self_dev.cloned(),
to: dependency_name.clone(),
to_version: dependency_version.clone(),
to_url: to_url.cloned(),
to_index: to_index.cloned(),
to_extra: Some(dependency_extra.clone()),
to_dev: None,
marker: MarkerTree::from(dependency_marker.clone()),
@ -2408,15 +2482,18 @@ impl ForkState {
continue;
}
let to_url = self.fork_urls.get(dependency_name);
let to_index = self.fork_indexes.get(dependency_name);
let edge = ResolutionDependencyEdge {
from: self_name.cloned(),
from_version: self_version.clone(),
from_url: self_url.cloned(),
from_index: self_index.cloned(),
from_extra: self_extra.cloned(),
from_dev: self_dev.cloned(),
to: dependency_name.clone(),
to_version: dependency_version.clone(),
to_url: to_url.cloned(),
to_index: to_index.cloned(),
to_extra: None,
to_dev: Some(dependency_dev.clone()),
marker: MarkerTree::from(dependency_marker.clone()),
@ -2445,6 +2522,7 @@ impl ForkState {
extra: extra.clone(),
dev: dev.clone(),
url: self.fork_urls.get(name).cloned(),
index: self.fork_indexes.get(name).cloned(),
},
version,
))
@ -2485,6 +2563,9 @@ pub(crate) struct ResolutionPackage {
pub(crate) dev: Option<GroupName>,
/// For index packages, this is `None`.
pub(crate) url: Option<VerbatimParsedUrl>,
/// For URL packages, this is `None`, and is only `Some` for packages that are pinned to a
/// specific index via `tool.uv.sources`.
pub(crate) index: Option<IndexUrl>,
}
/// The `from_` fields and the `to_` fields allow mapping to the originating and target
@ -2495,11 +2576,13 @@ pub(crate) struct ResolutionDependencyEdge {
pub(crate) from: Option<PackageName>,
pub(crate) from_version: Version,
pub(crate) from_url: Option<VerbatimParsedUrl>,
pub(crate) from_index: Option<IndexUrl>,
pub(crate) from_extra: Option<ExtraName>,
pub(crate) from_dev: Option<GroupName>,
pub(crate) to: PackageName,
pub(crate) to_version: Version,
pub(crate) to_url: Option<VerbatimParsedUrl>,
pub(crate) to_index: Option<IndexUrl>,
pub(crate) to_extra: Option<ExtraName>,
pub(crate) to_dev: Option<GroupName>,
pub(crate) marker: MarkerTree,
@ -2585,7 +2668,7 @@ impl Display for Request {
#[allow(clippy::large_enum_variant)]
enum Response {
/// The returned metadata for a package hosted on a registry.
Package(PackageName, VersionsResponse),
Package(PackageName, Option<IndexUrl>, VersionsResponse),
/// The returned metadata for a distribution.
Dist {
dist: Dist,

View file

@ -548,16 +548,6 @@ impl TryFrom<SourcesWire> for Sources {
return Err(SourceError::EmptySources);
}
// Ensure that there is at most one registry source.
if sources
.iter()
.filter(|source| matches!(source, Source::Registry { .. }))
.nth(1)
.is_some()
{
return Err(SourceError::MultipleIndexes);
}
Ok(Self(sources))
}
}
@ -967,8 +957,6 @@ pub enum SourceError {
OverlappingMarkers(String, String, String),
#[error("Must provide at least one source")]
EmptySources,
#[error("Sources can only include a single index source")]
MultipleIndexes,
}
impl Source {

View file

@ -7352,8 +7352,8 @@ fn lock_warn_missing_transitive_lower_bounds() -> Result<()> {
----- stderr -----
Resolved 6 packages in [TIME]
warning: The transitive dependency `packaging` is unpinned. Consider setting a lower bound with a constraint when using `--resolution-strategy lowest` to avoid using outdated versions.
warning: The transitive dependency `colorama` is unpinned. Consider setting a lower bound with a constraint when using `--resolution-strategy lowest` to avoid using outdated versions.
warning: The transitive dependency `packaging` is unpinned. Consider setting a lower bound with a constraint when using `--resolution-strategy lowest` to avoid using outdated versions.
warning: The transitive dependency `iniconfig` is unpinned. Consider setting a lower bound with a constraint when using `--resolution-strategy lowest` to avoid using outdated versions.
"###);
@ -14625,7 +14625,6 @@ fn lock_multiple_sources_conflict() -> Result<()> {
Ok(())
}
/// Multiple `index` entries is not yet supported.
#[test]
fn lock_multiple_sources_index() -> Result<()> {
let context = TestContext::new("3.12");
@ -14637,29 +14636,411 @@ fn lock_multiple_sources_index() -> Result<()> {
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig"]
dependencies = ["jinja2>=3"]
[tool.uv.sources]
iniconfig = [
{ index = "pytorch", marker = "sys_platform != 'win32'" },
{ index = "internal", marker = "sys_platform == 'win32'" },
jinja2 = [
{ index = "torch-cu118", marker = "sys_platform == 'win32'"},
{ index = "torch-cu124", marker = "sys_platform != 'win32'"},
]
[[tool.uv.index]]
name = "torch-cu118"
url = "https://download.pytorch.org/whl/cu118"
[[tool.uv.index]]
name = "torch-cu124"
url = "https://download.pytorch.org/whl/cu124"
"#,
)?;
uv_snapshot!(context.filters(), context.lock(), @r###"
uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
"###);
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r###"
version = 1
requires-python = ">=3.12"
resolution-markers = [
"sys_platform == 'win32'",
"sys_platform != 'win32'",
]
[[package]]
name = "jinja2"
version = "3.1.3"
source = { registry = "https://download.pytorch.org/whl/cu118" }
dependencies = [
{ name = "markupsafe", marker = "sys_platform == 'win32'" },
]
wheels = [
{ url = "https://download.pytorch.org/whl/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa" },
]
[[package]]
name = "jinja2"
version = "3.1.3"
source = { registry = "https://download.pytorch.org/whl/cu124" }
dependencies = [
{ name = "markupsafe", marker = "sys_platform != 'win32'" },
]
wheels = [
{ url = "https://download.pytorch.org/whl/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa" },
]
[[package]]
name = "markupsafe"
version = "2.1.5"
source = { registry = "https://download.pytorch.org/whl/cu118" }
wheels = [
{ url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1" },
{ url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4" },
{ url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee" },
{ url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5" },
{ url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b" },
{ url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb" },
]
[[package]]
name = "project"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "jinja2", version = "3.1.3", source = { registry = "https://download.pytorch.org/whl/cu118" }, marker = "sys_platform == 'win32'" },
{ name = "jinja2", version = "3.1.3", source = { registry = "https://download.pytorch.org/whl/cu124" }, marker = "sys_platform != 'win32'" },
]
[package.metadata]
requires-dist = [
{ name = "jinja2", marker = "sys_platform != 'win32'", specifier = ">=3", index = "https://download.pytorch.org/whl/cu124" },
{ name = "jinja2", marker = "sys_platform == 'win32'", specifier = ">=3", index = "https://download.pytorch.org/whl/cu118" },
]
"###
);
});
// Re-run with `--locked`.
uv_snapshot!(context.filters(), context.lock().arg("--locked").env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
"###);
Ok(())
}
#[test]
fn lock_multiple_sources_index_mixed() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["jinja2>=3"]
[tool.uv.sources]
jinja2 = [
{ index = "torch-cu118", marker = "sys_platform == 'win32'"},
{ url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", marker = "sys_platform != 'win32'"},
]
[[tool.uv.index]]
name = "torch-cu118"
url = "https://download.pytorch.org/whl/cu118"
"#,
)?;
uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
"###);
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r###"
version = 1
requires-python = ">=3.12"
resolution-markers = [
"sys_platform == 'win32'",
"sys_platform != 'win32'",
]
[[package]]
name = "jinja2"
version = "3.1.3"
source = { registry = "https://download.pytorch.org/whl/cu118" }
resolution-markers = [
"sys_platform == 'win32'",
]
dependencies = [
{ name = "markupsafe", marker = "sys_platform == 'win32'" },
]
wheels = [
{ url = "https://download.pytorch.org/whl/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa" },
]
[[package]]
name = "jinja2"
version = "3.1.4"
source = { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl" }
resolution-markers = [
"sys_platform != 'win32'",
]
dependencies = [
{ name = "markupsafe", marker = "sys_platform != 'win32'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" },
]
[package.metadata]
requires-dist = [
{ name = "babel", marker = "extra == 'i18n'", specifier = ">=2.7" },
{ name = "markupsafe", specifier = ">=2.0" },
]
[[package]]
name = "markupsafe"
version = "2.1.5"
source = { registry = "https://download.pytorch.org/whl/cu118" }
wheels = [
{ url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1" },
{ url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4" },
{ url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee" },
{ url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5" },
{ url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b" },
{ url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb" },
]
[[package]]
name = "project"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "jinja2", version = "3.1.3", source = { registry = "https://download.pytorch.org/whl/cu118" }, marker = "sys_platform == 'win32'" },
{ name = "jinja2", version = "3.1.4", source = { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl" }, marker = "sys_platform != 'win32'" },
]
[package.metadata]
requires-dist = [
{ name = "jinja2", marker = "sys_platform != 'win32'", url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl" },
{ name = "jinja2", marker = "sys_platform == 'win32'", specifier = ">=3", index = "https://download.pytorch.org/whl/cu118" },
]
"###
);
});
// Re-run with `--locked`.
uv_snapshot!(context.filters(), context.lock().arg("--locked").env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
"###);
Ok(())
}
#[test]
fn lock_multiple_sources_index_non_total() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["jinja2>=3"]
[tool.uv.sources]
jinja2 = [
{ index = "torch-cu118", marker = "sys_platform == 'win32'"},
]
[[tool.uv.index]]
name = "torch-cu118"
url = "https://download.pytorch.org/whl/cu118"
"#,
)?;
uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to parse: `pyproject.toml`
Caused by: TOML parse error at line 9, column 21
|
9 | iniconfig = [
| ^
Sources can only include a single index source
Resolved 4 packages in [TIME]
error: Found duplicate package `jinja2==3.1.3 @ registry+https://download.pytorch.org/whl/cu118`
"###);
Ok(())
}
#[test]
fn lock_multiple_sources_index_explicit() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["jinja2>=3"]
[tool.uv.sources]
jinja2 = [
{ index = "torch-cu118", marker = "sys_platform == 'win32'"},
]
[[tool.uv.index]]
name = "torch-cu118"
url = "https://download.pytorch.org/whl/cu118"
explicit = true
"#,
)?;
uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
"###);
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap();
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r###"
version = 1
requires-python = ">=3.12"
resolution-markers = [
"sys_platform == 'win32'",
"sys_platform != 'win32'",
]
[[package]]
name = "jinja2"
version = "3.1.3"
source = { registry = "https://download.pytorch.org/whl/cu118" }
dependencies = [
{ name = "markupsafe", marker = "sys_platform == 'win32'" },
]
wheels = [
{ url = "https://download.pytorch.org/whl/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa" },
]
[[package]]
name = "jinja2"
version = "3.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe", marker = "sys_platform != 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b2/5e/3a21abf3cd467d7876045335e681d276ac32492febe6d98ad89562d1a7e1/Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90", size = 268261 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/30/6d/6de6be2d02603ab56e72997708809e8a5b0fbfee080735109b40a3564843/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa", size = 133236 },
]
[[package]]
name = "markupsafe"
version = "3.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b4/d2/38ff920762f2247c3af5cbbbbc40756f575d9692d381d7c520f45deb9b8f/markupsafe-3.0.1.tar.gz", hash = "sha256:3e683ee4f5d0fa2dde4db77ed8dd8a876686e3fc417655c2ece9a90576905344", size = 20249 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/45/6d/72ed58d42a12bd9fc288dbff6dd8d03ea973a232ac0538d7f88d105b5251/MarkupSafe-3.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8ae369e84466aa70f3154ee23c1451fda10a8ee1b63923ce76667e3077f2b0c4", size = 14322 },
{ url = "https://files.pythonhosted.org/packages/86/f5/241238f89cdd6461ac9f521af8389f9a48fab97e4f315c69e9e0d52bc919/MarkupSafe-3.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40f1e10d51c92859765522cbd79c5c8989f40f0419614bcdc5015e7b6bf97fc5", size = 12380 },
{ url = "https://files.pythonhosted.org/packages/27/94/79751928bca5841416d8ca02e22198672e021d5c7120338e2a6e3771f8fc/MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a4cb365cb49b750bdb60b846b0c0bc49ed62e59a76635095a179d440540c346", size = 24099 },
{ url = "https://files.pythonhosted.org/packages/10/6e/1b8070bbfc467429c7983cd5ffd4ec57e1d501763d974c7caaa0a9a79f4c/MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee3941769bd2522fe39222206f6dd97ae83c442a94c90f2b7a25d847d40f4729", size = 23249 },
{ url = "https://files.pythonhosted.org/packages/66/50/9389ae6cdff78d7481a2a2641830b5eb1d1f62177550e73355a810a889c9/MarkupSafe-3.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62fada2c942702ef8952754abfc1a9f7658a4d5460fabe95ac7ec2cbe0d02abc", size = 23149 },
{ url = "https://files.pythonhosted.org/packages/16/02/5dddff5366fde47133186efb847fa88bddef85914bbe623e25cfeccb3517/MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c2d64fdba74ad16138300815cfdc6ab2f4647e23ced81f59e940d7d4a1469d9", size = 23864 },
{ url = "https://files.pythonhosted.org/packages/f3/f1/700ee6655561cfda986e03f7afc309e3738918551afa7dedd99225586227/MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fb532dd9900381d2e8f48172ddc5a59db4c445a11b9fab40b3b786da40d3b56b", size = 23440 },
{ url = "https://files.pythonhosted.org/packages/fb/3e/d26623ac7f16709823b4c80e0b4a1c9196eeb46182a6c1d47b5e0c8434f4/MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0f84af7e813784feb4d5e4ff7db633aba6c8ca64a833f61d8e4eade234ef0c38", size = 23610 },
{ url = "https://files.pythonhosted.org/packages/51/04/1f8da0810c39cb9fcff96b6baed62272c97065e9cf11471965a161439e20/MarkupSafe-3.0.1-cp312-cp312-win32.whl", hash = "sha256:cbf445eb5628981a80f54087f9acdbf84f9b7d862756110d172993b9a5ae81aa", size = 15113 },
{ url = "https://files.pythonhosted.org/packages/eb/24/a36dc37365bdd358b1e583cc40475593e36ab02cb7da6b3d0b9c05b0da7a/MarkupSafe-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:a10860e00ded1dd0a65b83e717af28845bb7bd16d8ace40fe5531491de76b79f", size = 15611 },
{ url = "https://files.pythonhosted.org/packages/b1/60/4572a8aa1beccbc24b133aa0670781a5d2697f4fa3fecf0a87b46383174b/MarkupSafe-3.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e81c52638315ff4ac1b533d427f50bc0afc746deb949210bc85f05d4f15fd772", size = 14325 },
{ url = "https://files.pythonhosted.org/packages/38/42/849915b99a765ec104bfd07ee933de5fc9c58fa9570efa7db81717f495d8/MarkupSafe-3.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:312387403cd40699ab91d50735ea7a507b788091c416dd007eac54434aee51da", size = 12373 },
{ url = "https://files.pythonhosted.org/packages/ef/82/4caaebd963c6d60b28e4445f38841d24f8b49bc10594a09956c9d73bfc08/MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ae99f31f47d849758a687102afdd05bd3d3ff7dbab0a8f1587981b58a76152a", size = 24059 },
{ url = "https://files.pythonhosted.org/packages/20/15/6b319be2f79fcfa3173f479d69f4e950b5c9b642db4f22cf73ae5ade745f/MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c97ff7fedf56d86bae92fa0a646ce1a0ec7509a7578e1ed238731ba13aabcd1c", size = 23211 },
{ url = "https://files.pythonhosted.org/packages/9d/3f/8963bdf4962feb2154475acb7dc350f04217b5e0be7763a39b432291e229/MarkupSafe-3.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7420ceda262dbb4b8d839a4ec63d61c261e4e77677ed7c66c99f4e7cb5030dd", size = 23095 },
{ url = "https://files.pythonhosted.org/packages/af/93/f770bc70953d32de0c6ce4bcb76271512123a1ead91aaef625a020c5bfaf/MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45d42d132cff577c92bfba536aefcfea7e26efb975bd455db4e6602f5c9f45e7", size = 23901 },
{ url = "https://files.pythonhosted.org/packages/11/92/1e5a33aa0a1190161238628fb68eb1bc5e67b56a5c89f0636328704b463a/MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c8817557d0de9349109acb38b9dd570b03cc5014e8aabf1cbddc6e81005becd", size = 23463 },
{ url = "https://files.pythonhosted.org/packages/0d/fe/657efdfe385d2a3a701f2c4fcc9577c63c438aeefdd642d0d956c4ecd225/MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a54c43d3ec4cf2a39f4387ad044221c66a376e58c0d0e971d47c475ba79c6b5", size = 23569 },
{ url = "https://files.pythonhosted.org/packages/cf/24/587dea40304046ace60f846cedaebc0d33d967a3ce46c11395a10e7a78ba/MarkupSafe-3.0.1-cp313-cp313-win32.whl", hash = "sha256:c91b394f7601438ff79a4b93d16be92f216adb57d813a78be4446fe0f6bc2d8c", size = 15117 },
{ url = "https://files.pythonhosted.org/packages/32/8f/d8961d633f26a011b4fe054f3bfff52f673423b8c431553268741dfb089e/MarkupSafe-3.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:fe32482b37b4b00c7a52a07211b479653b7fe4f22b2e481b9a9b099d8a430f2f", size = 15613 },
{ url = "https://files.pythonhosted.org/packages/9e/93/d6367ffbcd0c5c371370767f768eaa32af60bc411245b8517e383c6a2b12/MarkupSafe-3.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:17b2aea42a7280db02ac644db1d634ad47dcc96faf38ab304fe26ba2680d359a", size = 14563 },
{ url = "https://files.pythonhosted.org/packages/4a/37/f813c3835747dec08fe19ac9b9eced01fdf93a4b3e626521675dc7f423a9/MarkupSafe-3.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:852dc840f6d7c985603e60b5deaae1d89c56cb038b577f6b5b8c808c97580f1d", size = 12505 },
{ url = "https://files.pythonhosted.org/packages/72/bf/800b4d1580298ca91ccd6c95915bbd147142dad1b8cf91d57b93b28670dd/MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0778de17cff1acaeccc3ff30cd99a3fd5c50fc58ad3d6c0e0c4c58092b859396", size = 25358 },
{ url = "https://files.pythonhosted.org/packages/fd/78/26e209abc8f0a379f031f0acc151231974e5b153d7eda5759d17d8f329f2/MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:800100d45176652ded796134277ecb13640c1a537cad3b8b53da45aa96330453", size = 23797 },
{ url = "https://files.pythonhosted.org/packages/09/e1/918496a9390891756efee818880e71c1bbaf587f4dc8ede3f3852357310a/MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d06b24c686a34c86c8c1fba923181eae6b10565e4d80bdd7bc1c8e2f11247aa4", size = 23743 },
{ url = "https://files.pythonhosted.org/packages/cd/c6/26f576cd58d6c2decd9045e4e3f3c5dbc01ea6cb710916e7bbb6ebd95b6b/MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:33d1c36b90e570ba7785dacd1faaf091203d9942bc036118fab8110a401eb1a8", size = 25076 },
{ url = "https://files.pythonhosted.org/packages/b5/fa/10b24fb3b0e15fe5389dc88ecc6226ede08297e0ba7130610efbe0cdfb27/MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:beeebf760a9c1f4c07ef6a53465e8cfa776ea6a2021eda0d0417ec41043fe984", size = 24037 },
{ url = "https://files.pythonhosted.org/packages/c8/81/4b3f5537d9f6cc4f5c80d6c4b78af9a5247fd37b5aba95807b2cbc336b9a/MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bbde71a705f8e9e4c3e9e33db69341d040c827c7afa6789b14c6e16776074f5a", size = 24015 },
{ url = "https://files.pythonhosted.org/packages/5f/07/8e8dcecd53216c5e01a51e84c32a2bce166690ed19c184774b38cd41921d/MarkupSafe-3.0.1-cp313-cp313t-win32.whl", hash = "sha256:82b5dba6eb1bcc29cc305a18a3c5365d2af06ee71b123216416f7e20d2a84e5b", size = 15213 },
{ url = "https://files.pythonhosted.org/packages/0d/87/4c364e0f109eea2402079abecbe33fef4f347b551a11423d1f4e187ea497/MarkupSafe-3.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:730d86af59e0e43ce277bb83970530dd223bf7f2a838e086b50affa6ec5f9295", size = 15741 },
]
[[package]]
name = "project"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "jinja2", version = "3.1.3", source = { registry = "https://download.pytorch.org/whl/cu118" }, marker = "sys_platform == 'win32'" },
{ name = "jinja2", version = "3.1.3", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'win32'" },
]
[package.metadata]
requires-dist = [
{ name = "jinja2", marker = "sys_platform != 'win32'", specifier = ">=3" },
{ name = "jinja2", marker = "sys_platform == 'win32'", specifier = ">=3", index = "https://download.pytorch.org/whl/cu118" },
]
"###
);
});
// Re-run with `--locked`.
uv_snapshot!(context.filters(), context.lock().arg("--locked").env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
"###);
Ok(())