Skip to content

Commit 2db2a56

Browse files
committed
Allow dependency metadata entires for direct URL requirements
1 parent bd3c462 commit 2db2a56

File tree

5 files changed

+195
-23
lines changed

5 files changed

+195
-23
lines changed

crates/distribution-types/src/dependency_metadata.rs

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use pep508_rs::Requirement;
33
use pypi_types::{ResolutionMetadata, VerbatimParsedUrl};
44
use rustc_hash::FxHashMap;
55
use serde::{Deserialize, Serialize};
6+
use tracing::debug;
67
use uv_normalize::{ExtraName, PackageName};
78

89
/// Pre-defined [`StaticMetadata`] entries, indexed by [`PackageName`] and [`Version`].
@@ -20,22 +21,58 @@ impl DependencyMetadata {
2021
}
2122

2223
/// Retrieve a [`StaticMetadata`] entry by [`PackageName`] and [`Version`].
23-
pub fn get(&self, package: &PackageName, version: &Version) -> Option<ResolutionMetadata> {
24+
pub fn get(
25+
&self,
26+
package: &PackageName,
27+
version: Option<&Version>,
28+
) -> Option<ResolutionMetadata> {
2429
let versions = self.0.get(package)?;
2530

26-
// Search for an exact, then a global match.
27-
let metadata = versions
28-
.iter()
29-
.find(|v| v.version.as_ref() == Some(version))
30-
.or_else(|| versions.iter().find(|v| v.version.is_none()))?;
31-
32-
Some(ResolutionMetadata {
33-
name: metadata.name.clone(),
34-
version: version.clone(),
35-
requires_dist: metadata.requires_dist.clone(),
36-
requires_python: metadata.requires_python.clone(),
37-
provides_extras: metadata.provides_extras.clone(),
38-
})
31+
if let Some(version) = version {
32+
// If a specific version was requested, search for an exact match, then a global match.
33+
let metadata = versions
34+
.iter()
35+
.find(|v| v.version.as_ref() == Some(version))
36+
.map(|metadata| {
37+
debug!("Found dependency metadata entry for `{package}=={version}`",);
38+
metadata
39+
})
40+
.or_else(|| versions.iter().find(|v| v.version.is_none()))
41+
.map(|metadata| {
42+
debug!("Found global metadata entry for `{package}`",);
43+
metadata
44+
});
45+
let Some(metadata) = metadata else {
46+
debug!("No dependency metadata entry found for `{package}=={version}`");
47+
return None;
48+
};
49+
Some(ResolutionMetadata {
50+
name: metadata.name.clone(),
51+
version: version.clone(),
52+
requires_dist: metadata.requires_dist.clone(),
53+
requires_python: metadata.requires_python.clone(),
54+
provides_extras: metadata.provides_extras.clone(),
55+
})
56+
} else {
57+
// If no version was requested (i.e., it's a direct URL dependency), allow a single
58+
// versioned match.
59+
let [metadata] = versions.as_slice() else {
60+
debug!("Multiple dependency metadata entries found for `{package}`");
61+
return None;
62+
};
63+
let Some(version) = metadata.version.clone() else {
64+
debug!("No version found in dependency metadata entry for `{package}`");
65+
return None;
66+
};
67+
debug!("Found dependency metadata entry for `{package}` (assuming: `{version}`)");
68+
Some(ResolutionMetadata {
69+
name: metadata.name.clone(),
70+
version,
71+
requires_dist: metadata.requires_dist.clone(),
72+
requires_python: metadata.requires_python.clone(),
73+
provides_extras: metadata.provides_extras.clone(),
74+
})
75+
}
3976
}
4077

4178
/// Retrieve all [`StaticMetadata`] entries.

crates/uv-distribution/src/distribution_database.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
363363
if let Some(metadata) = self
364364
.build_context
365365
.dependency_metadata()
366-
.get(dist.name(), dist.version())
366+
.get(dist.name(), Some(dist.version()))
367367
{
368368
return Ok(ArchiveMetadata::from_metadata23(metadata.clone()));
369369
}
@@ -425,14 +425,12 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
425425
) -> Result<ArchiveMetadata, Error> {
426426
// If the metadata was provided by the user directly, prefer it.
427427
if let Some(dist) = source.as_dist() {
428-
if let Some(version) = dist.version() {
429-
if let Some(metadata) = self
430-
.build_context
431-
.dependency_metadata()
432-
.get(dist.name(), version)
433-
{
434-
return Ok(ArchiveMetadata::from_metadata23(metadata.clone()));
435-
}
428+
if let Some(metadata) = self
429+
.build_context
430+
.dependency_metadata()
431+
.get(dist.name(), dist.version())
432+
{
433+
return Ok(ArchiveMetadata::from_metadata23(metadata.clone()));
436434
}
437435
}
438436

crates/uv/tests/lock.rs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13007,6 +13007,131 @@ fn lock_dependency_metadata() -> Result<()> {
1300713007
Ok(())
1300813008
}
1300913009

13010+
#[test]
13011+
fn lock_dependency_metadata_url() -> Result<()> {
13012+
let context = TestContext::new("3.12");
13013+
13014+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
13015+
pyproject_toml.write_str(
13016+
r#"
13017+
[project]
13018+
name = "project"
13019+
version = "0.1.0"
13020+
requires-python = ">=3.12"
13021+
dependencies = ["anyio"]
13022+
13023+
[tool.uv.sources]
13024+
anyio = { git = "https://github.com/agronholm/anyio", rev = "bd36d3a5e5319dec71728c517d5e04cefb95650e" }
13025+
13026+
[build-system]
13027+
requires = ["setuptools>=42"]
13028+
build-backend = "setuptools.build_meta"
13029+
13030+
[[tool.uv.dependency-metadata]]
13031+
name = "anyio"
13032+
version = "4.6.0.post2"
13033+
requires-dist = ["iniconfig"]
13034+
"#,
13035+
)?;
13036+
13037+
uv_snapshot!(context.filters(), context.lock(), @r###"
13038+
success: true
13039+
exit_code: 0
13040+
----- stdout -----
13041+
13042+
----- stderr -----
13043+
Resolved 3 packages in [TIME]
13044+
"###);
13045+
13046+
let lock = context.read("uv.lock");
13047+
13048+
insta::with_settings!({
13049+
filters => context.filters(),
13050+
}, {
13051+
assert_snapshot!(
13052+
lock, @r###"
13053+
version = 1
13054+
requires-python = ">=3.12"
13055+
13056+
[options]
13057+
exclude-newer = "2024-03-25T00:00:00Z"
13058+
13059+
[manifest]
13060+
13061+
[[manifest.dependency-metadata]]
13062+
name = "anyio"
13063+
version = "4.6.0.post2"
13064+
requires-dist = ["iniconfig"]
13065+
13066+
[[package]]
13067+
name = "anyio"
13068+
version = "4.6.0.post2"
13069+
source = { git = "https://github.com/agronholm/anyio?rev=bd36d3a5e5319dec71728c517d5e04cefb95650e#bd36d3a5e5319dec71728c517d5e04cefb95650e" }
13070+
dependencies = [
13071+
{ name = "iniconfig" },
13072+
]
13073+
13074+
[[package]]
13075+
name = "iniconfig"
13076+
version = "2.0.0"
13077+
source = { registry = "https://pypi.org/simple" }
13078+
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
13079+
wheels = [
13080+
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
13081+
]
13082+
13083+
[[package]]
13084+
name = "project"
13085+
version = "0.1.0"
13086+
source = { editable = "." }
13087+
dependencies = [
13088+
{ name = "anyio" },
13089+
]
13090+
13091+
[package.metadata]
13092+
requires-dist = [{ name = "anyio", git = "https://github.com/agronholm/anyio?rev=bd36d3a5e5319dec71728c517d5e04cefb95650e" }]
13093+
"###
13094+
);
13095+
});
13096+
13097+
// Re-run with `--locked`.
13098+
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
13099+
success: true
13100+
exit_code: 0
13101+
----- stdout -----
13102+
13103+
----- stderr -----
13104+
Resolved 3 packages in [TIME]
13105+
"###);
13106+
13107+
// Re-run with `--offline`. We shouldn't need a network connection to validate an
13108+
// already-correct lockfile with immutable metadata.
13109+
uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###"
13110+
success: true
13111+
exit_code: 0
13112+
----- stdout -----
13113+
13114+
----- stderr -----
13115+
Resolved 3 packages in [TIME]
13116+
"###);
13117+
13118+
// Install from the lockfile.
13119+
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
13120+
success: true
13121+
exit_code: 0
13122+
----- stdout -----
13123+
13124+
----- stderr -----
13125+
Prepared 3 packages in [TIME]
13126+
Installed 3 packages in [TIME]
13127+
+ anyio==4.6.0.post2 (from git+https://github.com/agronholm/anyio@bd36d3a5e5319dec71728c517d5e04cefb95650e)
13128+
+ iniconfig==2.0.0
13129+
+ project==0.1.0 (from file://[TEMP_DIR]/)
13130+
"###);
13131+
13132+
Ok(())
13133+
}
13134+
1301013135
#[test]
1301113136
fn lock_strip_fragment() -> Result<()> {
1301213137
let context = TestContext::new("3.12");

docs/concepts/projects.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,3 +797,9 @@ You could run the following sequence of commands to sync `flash-attn`:
797797
$ uv sync --extra build
798798
$ uv sync --extra build --extra compile
799799
```
800+
801+
!!! note
802+
803+
The `version` field in `tool.uv.dependency-metadata` is optional for registry-based
804+
dependencies (when omitted, uv will assume the metadata applies to all versions of the package),
805+
but _required_ for direct URL dependencies.

docs/concepts/resolution.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,12 @@ package's metadata is incorrect or incomplete, or when a package is not availabl
313313
index. While dependency overrides allow overriding the allowed versions of a package globally,
314314
metadata overrides allow overriding the declared metadata of a _specific package_.
315315

316+
!!! note
317+
318+
The `version` field in `tool.uv.dependency-metadata` is optional for registry-based
319+
dependencies (when omitted, uv will assume the metadata applies to all versions of the package),
320+
but _required_ for direct URL dependencies.
321+
316322
Entries in the `tool.uv.dependency-metadata` table follow the
317323
[Metadata 2.3](https://packaging.python.org/en/latest/specifications/core-metadata/) specification,
318324
though only `name`, `version`, `requires-dist`, `requires-python`, and `provides-extra` are read by

0 commit comments

Comments
 (0)