Skip to content

Commit 7d49fbc

Browse files
Skip metadata fetch for --no-deps and pip sync (#7127)
## Summary I think a better tradeoff here is to skip fetching metadata, even though we can't validate the extras. It will help with situations like #5073 (comment) in which, otherwise, we have to download the wheels twice.
1 parent e96eb94 commit 7d49fbc

File tree

8 files changed

+88
-78
lines changed

8 files changed

+88
-78
lines changed

crates/uv-resolver/src/lock/mod.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1459,6 +1459,8 @@ impl Package {
14591459
} else {
14601460
annotated_dist
14611461
.metadata
1462+
.as_ref()
1463+
.expect("metadata is present")
14621464
.requires_dist
14631465
.iter()
14641466
.cloned()
@@ -1471,6 +1473,8 @@ impl Package {
14711473
} else {
14721474
annotated_dist
14731475
.metadata
1476+
.as_ref()
1477+
.expect("metadata is present")
14741478
.dev_dependencies
14751479
.iter()
14761480
.map(|(group, requirements)| {
@@ -2168,8 +2172,8 @@ impl PackageId {
21682172
annotated_dist: &AnnotatedDist,
21692173
root: &Path,
21702174
) -> Result<PackageId, LockError> {
2171-
let name = annotated_dist.metadata.name.clone();
2172-
let version = annotated_dist.metadata.version.clone();
2175+
let name = annotated_dist.name.clone();
2176+
let version = annotated_dist.version.clone();
21732177
let source = Source::from_resolved_dist(&annotated_dist.dist, root)?;
21742178
Ok(Self {
21752179
name,

crates/uv-resolver/src/resolution/graph.rs

Lines changed: 36 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -300,29 +300,32 @@ impl ResolutionGraph {
300300
git,
301301
)?;
302302

303-
// Validate the extra.
304-
if let Some(extra) = extra {
305-
if !metadata.provides_extras.contains(extra) {
306-
diagnostics.push(ResolutionDiagnostic::MissingExtra {
307-
dist: dist.clone(),
308-
extra: extra.clone(),
309-
});
303+
if let Some(metadata) = metadata.as_ref() {
304+
// Validate the extra.
305+
if let Some(extra) = extra {
306+
if !metadata.provides_extras.contains(extra) {
307+
diagnostics.push(ResolutionDiagnostic::MissingExtra {
308+
dist: dist.clone(),
309+
extra: extra.clone(),
310+
});
311+
}
310312
}
311-
}
312313

313-
// Validate the development dependency group.
314-
if let Some(dev) = dev {
315-
if !metadata.dev_dependencies.contains_key(dev) {
316-
diagnostics.push(ResolutionDiagnostic::MissingDev {
317-
dist: dist.clone(),
318-
dev: dev.clone(),
319-
});
314+
// Validate the development dependency group.
315+
if let Some(dev) = dev {
316+
if !metadata.dev_dependencies.contains_key(dev) {
317+
diagnostics.push(ResolutionDiagnostic::MissingDev {
318+
dist: dist.clone(),
319+
dev: dev.clone(),
320+
});
321+
}
320322
}
321323
}
322324

323325
// Add the distribution to the graph.
324326
let index = petgraph.add_node(ResolutionGraphNode::Dist(AnnotatedDist {
325327
dist,
328+
name: name.clone(),
326329
version: version.clone(),
327330
extra: extra.clone(),
328331
dev: dev.clone(),
@@ -351,7 +354,7 @@ impl ResolutionGraph {
351354
preferences: &Preferences,
352355
index: &InMemoryIndex,
353356
git: &GitResolver,
354-
) -> Result<(ResolvedDist, Vec<HashDigest>, Metadata), ResolveError> {
357+
) -> Result<(ResolvedDist, Vec<HashDigest>, Option<Metadata>), ResolveError> {
355358
Ok(if let Some(url) = url {
356359
// Create the distribution.
357360
let dist = Dist::from_url(name.clone(), url_to_precise(url.clone(), git))?;
@@ -365,17 +368,17 @@ impl ResolutionGraph {
365368
// Extract the metadata.
366369
let metadata = {
367370
let response = index.distributions().get(&version_id).unwrap_or_else(|| {
368-
panic!("Every package should have metadata: {version_id:?}")
371+
panic!("Every URL distribution should have metadata: {version_id:?}")
369372
});
370373

371374
let MetadataResponse::Found(archive) = &*response else {
372-
panic!("Every package should have metadata: {version_id:?}")
375+
panic!("Every URL distribution should have metadata: {version_id:?}")
373376
};
374377

375378
archive.metadata.clone()
376379
};
377380

378-
(dist.into(), hashes, metadata)
381+
(dist.into(), hashes, Some(metadata))
379382
} else {
380383
let dist = pins
381384
.get(name, version)
@@ -406,15 +409,13 @@ impl ResolutionGraph {
406409

407410
// Extract the metadata.
408411
let metadata = {
409-
let response = index.distributions().get(&version_id).unwrap_or_else(|| {
410-
panic!("Every package should have metadata: {version_id:?}")
411-
});
412-
413-
let MetadataResponse::Found(archive) = &*response else {
414-
panic!("Every package should have metadata: {version_id:?}")
415-
};
416-
417-
archive.metadata.clone()
412+
index.distributions().get(&version_id).and_then(|response| {
413+
if let MetadataResponse::Found(archive) = &*response {
414+
Some(archive.metadata.clone())
415+
} else {
416+
None
417+
}
418+
})
418419
};
419420

420421
(dist, hashes, metadata)
@@ -726,13 +727,17 @@ fn has_lower_bound(
726727
return true;
727728
}
728729

730+
let Some(metadata) = neighbor_dist.metadata.as_ref() else {
731+
// We can't check for lower bounds if we lack metadata.
732+
return true;
733+
};
734+
729735
// Get all individual specifier for the current package and check if any has a lower
730736
// bound.
731-
for requirement in neighbor_dist
732-
.metadata
737+
for requirement in metadata
733738
.requires_dist
734739
.iter()
735-
.chain(neighbor_dist.metadata.dev_dependencies.values().flatten())
740+
.chain(metadata.dev_dependencies.values().flatten())
736741
{
737742
if requirement.name != *package_name {
738743
continue;

crates/uv-resolver/src/resolution/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ mod requirements_txt;
2424
#[derive(Debug, Clone)]
2525
pub(crate) struct AnnotatedDist {
2626
pub(crate) dist: ResolvedDist,
27+
pub(crate) name: PackageName,
2728
pub(crate) version: Version,
2829
pub(crate) extra: Option<ExtraName>,
2930
pub(crate) dev: Option<GroupName>,
3031
pub(crate) hashes: Vec<HashDigest>,
31-
pub(crate) metadata: Metadata,
32+
pub(crate) metadata: Option<Metadata>,
3233
}
3334

3435
impl AnnotatedDist {

crates/uv-resolver/src/resolver/mod.rs

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -331,12 +331,14 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
331331
}
332332

333333
// Pre-visit all candidate packages, to allow metadata to be fetched in parallel.
334-
Self::pre_visit(
335-
state.pubgrub.partial_solution.prioritized_packages(),
336-
&self.urls,
337-
&state.python_requirement,
338-
&request_sink,
339-
)?;
334+
if self.dependency_mode.is_transitive() {
335+
Self::pre_visit(
336+
state.pubgrub.partial_solution.prioritized_packages(),
337+
&self.urls,
338+
&state.python_requirement,
339+
&request_sink,
340+
)?;
341+
}
340342

341343
// Choose a package version.
342344
let Some(highest_priority_pkg) = state
@@ -1109,17 +1111,19 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
11091111

11101112
// Emit a request to fetch the metadata for this version.
11111113
if matches!(&**package, PubGrubPackageInner::Package { .. }) {
1112-
if self.index.distributions().register(candidate.version_id()) {
1113-
// Verify that the package is allowed under the hash-checking policy.
1114-
if !self
1115-
.hasher
1116-
.allows_package(candidate.name(), candidate.version())
1117-
{
1118-
return Err(ResolveError::UnhashedPackage(candidate.name().clone()));
1119-
}
1114+
if self.dependency_mode.is_transitive() {
1115+
if self.index.distributions().register(candidate.version_id()) {
1116+
// Verify that the package is allowed under the hash-checking policy.
1117+
if !self
1118+
.hasher
1119+
.allows_package(candidate.name(), candidate.version())
1120+
{
1121+
return Err(ResolveError::UnhashedPackage(candidate.name().clone()));
1122+
}
11201123

1121-
let request = Request::from(dist.for_resolution());
1122-
request_sink.blocking_send(request)?;
1124+
let request = Request::from(dist.for_resolution());
1125+
request_sink.blocking_send(request)?;
1126+
}
11231127
}
11241128
}
11251129

@@ -1186,6 +1190,11 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
11861190
dev,
11871191
marker,
11881192
} => {
1193+
// If we're excluding transitive dependencies, short-circuit.
1194+
if self.dependency_mode.is_direct() {
1195+
return Ok(Dependencies::Unforkable(Vec::default()));
1196+
}
1197+
11891198
// Determine the distribution to lookup.
11901199
let dist = match url {
11911200
Some(url) => PubGrubDistribution::from_url(name, url),
@@ -1273,12 +1282,6 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
12731282
}
12741283
};
12751284

1276-
// If we're excluding transitive dependencies, short-circuit. (It's important that
1277-
// we fetched the metadata, though, since we need it to validate extras.)
1278-
if self.dependency_mode.is_direct() {
1279-
return Ok(Dependencies::Unforkable(Vec::default()));
1280-
}
1281-
12821285
let requirements = self.flatten_requirements(
12831286
&metadata.requires_dist,
12841287
&metadata.dev_dependencies,

crates/uv/tests/cache_clean.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ fn clean_package_pypi() -> Result<()> {
8585
----- stderr -----
8686
DEBUG uv [VERSION] ([COMMIT] DATE)
8787
DEBUG Removing dangling cache entry: [CACHE_DIR]/archive-v0/[ENTRY]
88-
Removed 13 files for iniconfig ([SIZE])
88+
Removed 12 files for iniconfig ([SIZE])
8989
"###);
9090

9191
// Assert that the `.rkyv` file is removed for `iniconfig`.
@@ -158,7 +158,7 @@ fn clean_package_index() -> Result<()> {
158158
----- stderr -----
159159
DEBUG uv [VERSION] ([COMMIT] DATE)
160160
DEBUG Removing dangling cache entry: [CACHE_DIR]/archive-v0/[ENTRY]
161-
Removed 13 files for iniconfig ([SIZE])
161+
Removed 12 files for iniconfig ([SIZE])
162162
"###);
163163

164164
// Assert that the `.rkyv` file is removed for `iniconfig`.

crates/uv/tests/cache_prune.rs

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ fn prune_unzipped() -> Result<()> {
204204
205205
----- stderr -----
206206
Pruning cache at: [CACHE_DIR]/
207-
Removed 163 files ([SIZE])
207+
Removed 162 files ([SIZE])
208208
"###);
209209

210210
// Reinstalling the source distribution should not require re-downloading the source
@@ -226,28 +226,20 @@ fn prune_unzipped() -> Result<()> {
226226
~ source-distribution==0.0.1
227227
"###);
228228

229+
// But reinstalling the other package should require a download, since we pruned the wheel.
229230
requirements_txt.write_str(indoc! { r"
230231
iniconfig
231232
" })?;
232233
uv_snapshot!(context.filters(), context.pip_sync().env_remove("UV_EXCLUDE_NEWER").arg("requirements.txt").arg("--reinstall").arg("--offline"), @r###"
233234
success: false
234-
exit_code: 1
235+
exit_code: 2
235236
----- stdout -----
236237
237238
----- stderr -----
238-
× No solution found when resolving dependencies:
239-
╰─▶ Because only the following versions of iniconfig are available:
240-
iniconfig<=0.1
241-
iniconfig>=1.0.0
242-
and all of:
243-
iniconfig<=0.1
244-
iniconfig>=1.0.0
245-
need to be downloaded from a registry, we can conclude that iniconfig<1.0.0 cannot be used.
246-
And because you require iniconfig, we can conclude that your requirements are unsatisfiable.
247-
248-
hint: Pre-releases are available for iniconfig in the requested range (e.g., 0.2.dev0), but pre-releases weren't enabled (try: `--prerelease=allow`)
249-
250-
hint: Packages were unavailable because the network was disabled. When the network is disabled, registry packages may only be read from the cache.
239+
Resolved 1 package in [TIME]
240+
error: Failed to prepare distributions
241+
Caused by: Failed to fetch wheel: iniconfig==2.0.0
242+
Caused by: Network connectivity is disabled, but the requested data wasn't found in the cache for: `https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl`
251243
"###);
252244

253245
Ok(())

crates/uv/tests/pip_compile.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6151,7 +6151,7 @@ fn no_deps_valid_extra() -> Result<()> {
61516151
Ok(())
61526152
}
61536153

6154-
/// Resolve a package with `--no-deps`, including an invalid extra.
6154+
/// Resolve a package with `--no-deps`, including an invalid extra. We don't warn here.
61556155
#[test]
61566156
fn no_deps_invalid_extra() -> Result<()> {
61576157
let context = TestContext::new("3.12");
@@ -6171,7 +6171,6 @@ fn no_deps_invalid_extra() -> Result<()> {
61716171
61726172
----- stderr -----
61736173
Resolved 1 package in [TIME]
6174-
warning: The package `flask==3.0.2` does not have an extra named `empty`
61756174
"###
61766175
);
61776176

crates/uv/tests/pip_sync.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4783,7 +4783,9 @@ fn require_hashes_find_links_invalid_hash() -> Result<()> {
47834783
----- stdout -----
47844784
47854785
----- stderr -----
4786-
error: Failed to download and build `example-a-961b4c22==1.0.0`
4786+
Resolved 1 package in [TIME]
4787+
error: Failed to prepare distributions
4788+
Caused by: Failed to fetch wheel: example-a-961b4c22==1.0.0
47874789
Caused by: Hash mismatch for `example-a-961b4c22==1.0.0`
47884790
47894791
Expected:
@@ -4991,7 +4993,9 @@ fn require_hashes_registry_invalid_hash() -> Result<()> {
49914993
----- stdout -----
49924994
49934995
----- stderr -----
4994-
error: Failed to download and build `example-a-961b4c22==1.0.0`
4996+
Resolved 1 package in [TIME]
4997+
error: Failed to prepare distributions
4998+
Caused by: Failed to fetch wheel: example-a-961b4c22==1.0.0
49954999
Caused by: Hash mismatch for `example-a-961b4c22==1.0.0`
49965000
49975001
Expected:
@@ -5472,7 +5476,9 @@ fn incompatible_build_constraint() -> Result<()> {
54725476
----- stdout -----
54735477
54745478
----- stderr -----
5475-
error: Failed to download and build `requests==1.2.0`
5479+
Resolved 1 package in [TIME]
5480+
error: Failed to prepare distributions
5481+
Caused by: Failed to fetch wheel: requests==1.2.0
54765482
Caused by: Failed to install requirements from `setup.py` build (resolve)
54775483
Caused by: No solution found when resolving: setuptools>=40.8.0
54785484
Caused by: Because you require setuptools>=40.8.0 and setuptools==1, we can conclude that your requirements are unsatisfiable.

0 commit comments

Comments
 (0)