Skip to content

Commit 5d655ec

Browse files
committed
Allow dependency metadata entires for direct URL requirements
1 parent 21b9254 commit 5d655ec

File tree

5 files changed

+193
-23
lines changed

5 files changed

+193
-23
lines changed

crates/uv-distribution-types/src/dependency_metadata.rs

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use rustc_hash::FxHashMap;
22
use serde::{Deserialize, Serialize};
3+
use tracing::debug;
34
use uv_normalize::{ExtraName, PackageName};
45
use uv_pep440::{Version, VersionSpecifiers};
56
use uv_pep508::Requirement;
@@ -20,22 +21,56 @@ 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+
.inspect(|_| {
37+
debug!("Found dependency metadata entry for `{package}=={version}`",);
38+
})
39+
.or_else(|| versions.iter().find(|v| v.version.is_none()))
40+
.inspect(|_| {
41+
debug!("Found global metadata entry for `{package}`",);
42+
});
43+
let Some(metadata) = metadata else {
44+
debug!("No dependency metadata entry found for `{package}=={version}`");
45+
return None;
46+
};
47+
Some(ResolutionMetadata {
48+
name: metadata.name.clone(),
49+
version: version.clone(),
50+
requires_dist: metadata.requires_dist.clone(),
51+
requires_python: metadata.requires_python.clone(),
52+
provides_extras: metadata.provides_extras.clone(),
53+
})
54+
} else {
55+
// If no version was requested (i.e., it's a direct URL dependency), allow a single
56+
// versioned match.
57+
let [metadata] = versions.as_slice() else {
58+
debug!("Multiple dependency metadata entries found for `{package}`");
59+
return None;
60+
};
61+
let Some(version) = metadata.version.clone() else {
62+
debug!("No version found in dependency metadata entry for `{package}`");
63+
return None;
64+
};
65+
debug!("Found dependency metadata entry for `{package}` (assuming: `{version}`)");
66+
Some(ResolutionMetadata {
67+
name: metadata.name.clone(),
68+
version,
69+
requires_dist: metadata.requires_dist.clone(),
70+
requires_python: metadata.requires_python.clone(),
71+
provides_extras: metadata.provides_extras.clone(),
72+
})
73+
}
3974
}
4075

4176
/// 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/it/lock.rs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14533,6 +14533,131 @@ fn lock_dependency_metadata() -> Result<()> {
1453314533
Ok(())
1453414534
}
1453514535

14536+
#[test]
14537+
fn lock_dependency_metadata_url() -> Result<()> {
14538+
let context = TestContext::new("3.12");
14539+
14540+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
14541+
pyproject_toml.write_str(
14542+
r#"
14543+
[project]
14544+
name = "project"
14545+
version = "0.1.0"
14546+
requires-python = ">=3.12"
14547+
dependencies = ["anyio"]
14548+
14549+
[tool.uv.sources]
14550+
anyio = { git = "https://github.com/agronholm/anyio", rev = "bd36d3a5e5319dec71728c517d5e04cefb95650e" }
14551+
14552+
[build-system]
14553+
requires = ["setuptools>=42"]
14554+
build-backend = "setuptools.build_meta"
14555+
14556+
[[tool.uv.dependency-metadata]]
14557+
name = "anyio"
14558+
version = "4.6.0.post2"
14559+
requires-dist = ["iniconfig"]
14560+
"#,
14561+
)?;
14562+
14563+
uv_snapshot!(context.filters(), context.lock(), @r###"
14564+
success: true
14565+
exit_code: 0
14566+
----- stdout -----
14567+
14568+
----- stderr -----
14569+
Resolved 3 packages in [TIME]
14570+
"###);
14571+
14572+
let lock = context.read("uv.lock");
14573+
14574+
insta::with_settings!({
14575+
filters => context.filters(),
14576+
}, {
14577+
assert_snapshot!(
14578+
lock, @r###"
14579+
version = 1
14580+
requires-python = ">=3.12"
14581+
14582+
[options]
14583+
exclude-newer = "2024-03-25T00:00:00Z"
14584+
14585+
[manifest]
14586+
14587+
[[manifest.dependency-metadata]]
14588+
name = "anyio"
14589+
version = "4.6.0.post2"
14590+
requires-dist = ["iniconfig"]
14591+
14592+
[[package]]
14593+
name = "anyio"
14594+
version = "4.6.0.post2"
14595+
source = { git = "https://github.com/agronholm/anyio?rev=bd36d3a5e5319dec71728c517d5e04cefb95650e#bd36d3a5e5319dec71728c517d5e04cefb95650e" }
14596+
dependencies = [
14597+
{ name = "iniconfig" },
14598+
]
14599+
14600+
[[package]]
14601+
name = "iniconfig"
14602+
version = "2.0.0"
14603+
source = { registry = "https://pypi.org/simple" }
14604+
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
14605+
wheels = [
14606+
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
14607+
]
14608+
14609+
[[package]]
14610+
name = "project"
14611+
version = "0.1.0"
14612+
source = { editable = "." }
14613+
dependencies = [
14614+
{ name = "anyio" },
14615+
]
14616+
14617+
[package.metadata]
14618+
requires-dist = [{ name = "anyio", git = "https://github.com/agronholm/anyio?rev=bd36d3a5e5319dec71728c517d5e04cefb95650e" }]
14619+
"###
14620+
);
14621+
});
14622+
14623+
// Re-run with `--locked`.
14624+
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
14625+
success: true
14626+
exit_code: 0
14627+
----- stdout -----
14628+
14629+
----- stderr -----
14630+
Resolved 3 packages in [TIME]
14631+
"###);
14632+
14633+
// Re-run with `--offline`. We shouldn't need a network connection to validate an
14634+
// already-correct lockfile with immutable metadata.
14635+
uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###"
14636+
success: true
14637+
exit_code: 0
14638+
----- stdout -----
14639+
14640+
----- stderr -----
14641+
Resolved 3 packages in [TIME]
14642+
"###);
14643+
14644+
// Install from the lockfile.
14645+
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
14646+
success: true
14647+
exit_code: 0
14648+
----- stdout -----
14649+
14650+
----- stderr -----
14651+
Prepared 3 packages in [TIME]
14652+
Installed 3 packages in [TIME]
14653+
+ anyio==4.6.0.post2 (from git+https://github.com/agronholm/anyio@bd36d3a5e5319dec71728c517d5e04cefb95650e)
14654+
+ iniconfig==2.0.0
14655+
+ project==0.1.0 (from file://[TEMP_DIR]/)
14656+
"###);
14657+
14658+
Ok(())
14659+
}
14660+
1453614661
#[test]
1453714662
fn lock_strip_fragment() -> Result<()> {
1453814663
let context = TestContext::new("3.12");

docs/concepts/projects.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -855,3 +855,9 @@ You could run the following sequence of commands to sync `flash-attn`:
855855
$ uv sync --extra build
856856
$ uv sync --extra build --extra compile
857857
```
858+
859+
!!! note
860+
861+
The `version` field in `tool.uv.dependency-metadata` is optional for registry-based
862+
dependencies (when omitted, uv will assume the metadata applies to all versions of the package),
863+
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)