Skip to content

Commit 4cc91cc

Browse files
Add convenience methods to Manifest to iterate over requirements (#2701)
## Summary These are repeated a bunch. It's nice to DRY them up and ensure the ordering is consistent.
1 parent b6ab919 commit 4cc91cc

File tree

7 files changed

+168
-92
lines changed

7 files changed

+168
-92
lines changed

crates/uv-resolver/src/manifest.rs

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use distribution_types::LocalEditable;
2-
use pep508_rs::Requirement;
2+
use pep508_rs::{MarkerEnvironment, Requirement};
33
use pypi_types::Metadata23;
44
use uv_normalize::PackageName;
55
use uv_types::RequestedRequirements;
@@ -74,4 +74,78 @@ impl Manifest {
7474
lookaheads: Vec::new(),
7575
}
7676
}
77+
78+
/// Return an iterator over all requirements, constraints, and overrides, in priority order,
79+
/// such that requirements come first, followed by constraints, followed by overrides.
80+
///
81+
/// At time of writing, this is used for:
82+
/// - Determining which requirements should allow yanked versions.
83+
/// - Determining which requirements should allow pre-release versions (e.g., `torch>=2.2.0a1`).
84+
/// - Determining which requirements should allow direct URLs (e.g., `torch @ https://...`).
85+
/// - Determining which requirements should allow local version specifiers (e.g., `torch==2.2.0+cpu`).
86+
pub fn requirements<'a>(
87+
&'a self,
88+
markers: &'a MarkerEnvironment,
89+
) -> impl Iterator<Item = &Requirement> {
90+
self.lookaheads
91+
.iter()
92+
.flat_map(|lookahead| {
93+
lookahead
94+
.requirements()
95+
.iter()
96+
.filter(|requirement| requirement.evaluate_markers(markers, lookahead.extras()))
97+
})
98+
.chain(self.editables.iter().flat_map(|(editable, metadata)| {
99+
metadata
100+
.requires_dist
101+
.iter()
102+
.filter(|requirement| requirement.evaluate_markers(markers, &editable.extras))
103+
}))
104+
.chain(
105+
self.requirements
106+
.iter()
107+
.filter(|requirement| requirement.evaluate_markers(markers, &[])),
108+
)
109+
.chain(
110+
self.constraints
111+
.iter()
112+
.filter(|requirement| requirement.evaluate_markers(markers, &[])),
113+
)
114+
.chain(
115+
self.overrides
116+
.iter()
117+
.filter(|requirement| requirement.evaluate_markers(markers, &[])),
118+
)
119+
}
120+
121+
/// Return an iterator over the names of all direct dependency requirements.
122+
///
123+
/// At time of writing, this is used for:
124+
/// - Determining which packages should use the "lowest-compatible version" of a package, when
125+
/// the `lowest-direct` strategy is in use.
126+
pub fn direct_dependencies<'a>(
127+
&'a self,
128+
markers: &'a MarkerEnvironment,
129+
) -> impl Iterator<Item = &PackageName> {
130+
self.lookaheads
131+
.iter()
132+
.flat_map(|lookahead| {
133+
lookahead
134+
.requirements()
135+
.iter()
136+
.filter(|requirement| requirement.evaluate_markers(markers, lookahead.extras()))
137+
})
138+
.chain(self.editables.iter().flat_map(|(editable, metadata)| {
139+
metadata
140+
.requires_dist
141+
.iter()
142+
.filter(|requirement| requirement.evaluate_markers(markers, &editable.extras))
143+
}))
144+
.chain(
145+
self.requirements
146+
.iter()
147+
.filter(|requirement| requirement.evaluate_markers(markers, &[])),
148+
)
149+
.map(|requirement| &requirement.name)
150+
}
77151
}

crates/uv-resolver/src/prerelease_mode.rs

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -61,21 +61,7 @@ impl PreReleaseStrategy {
6161
PreReleaseMode::IfNecessary => Self::IfNecessary,
6262
PreReleaseMode::Explicit => Self::Explicit(
6363
manifest
64-
.requirements
65-
.iter()
66-
.chain(manifest.constraints.iter())
67-
.chain(manifest.overrides.iter())
68-
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
69-
.chain(manifest.lookaheads.iter().flat_map(|lookahead| {
70-
lookahead.requirements().iter().filter(|requirement| {
71-
requirement.evaluate_markers(markers, lookahead.extras())
72-
})
73-
}))
74-
.chain(manifest.editables.iter().flat_map(|(editable, metadata)| {
75-
metadata.requires_dist.iter().filter(|requirement| {
76-
requirement.evaluate_markers(markers, &editable.extras)
77-
})
78-
}))
64+
.requirements(markers)
7965
.filter(|requirement| {
8066
let Some(version_or_url) = &requirement.version_or_url else {
8167
return false;
@@ -95,21 +81,7 @@ impl PreReleaseStrategy {
9581
),
9682
PreReleaseMode::IfNecessaryOrExplicit => Self::IfNecessaryOrExplicit(
9783
manifest
98-
.requirements
99-
.iter()
100-
.chain(manifest.constraints.iter())
101-
.chain(manifest.overrides.iter())
102-
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
103-
.chain(manifest.lookaheads.iter().flat_map(|lookahead| {
104-
lookahead.requirements().iter().filter(|requirement| {
105-
requirement.evaluate_markers(markers, lookahead.extras())
106-
})
107-
}))
108-
.chain(manifest.editables.iter().flat_map(|(editable, metadata)| {
109-
metadata.requires_dist.iter().filter(|requirement| {
110-
requirement.evaluate_markers(markers, &editable.extras)
111-
})
112-
}))
84+
.requirements(markers)
11385
.filter(|requirement| {
11486
let Some(version_or_url) = &requirement.version_or_url else {
11587
return false;

crates/uv-resolver/src/resolution_mode.rs

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -40,25 +40,9 @@ impl ResolutionStrategy {
4040
match mode {
4141
ResolutionMode::Highest => Self::Highest,
4242
ResolutionMode::Lowest => Self::Lowest,
43-
ResolutionMode::LowestDirect => Self::LowestDirect(
44-
// Consider `requirements` and dependencies of any local requirements to be "direct" dependencies.
45-
manifest
46-
.requirements
47-
.iter()
48-
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
49-
.chain(manifest.lookaheads.iter().flat_map(|lookahead| {
50-
lookahead.requirements().iter().filter(|requirement| {
51-
requirement.evaluate_markers(markers, lookahead.extras())
52-
})
53-
}))
54-
.chain(manifest.editables.iter().flat_map(|(editable, metadata)| {
55-
metadata.requires_dist.iter().filter(|requirement| {
56-
requirement.evaluate_markers(markers, &editable.extras)
57-
})
58-
}))
59-
.map(|requirement| requirement.name.clone())
60-
.collect(),
61-
),
43+
ResolutionMode::LowestDirect => {
44+
Self::LowestDirect(manifest.direct_dependencies(markers).cloned().collect())
45+
}
6246
}
6347
}
6448
}

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

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,8 @@ impl Locals {
2323
let mut required: FxHashMap<PackageName, Version> = FxHashMap::default();
2424

2525
// Add all direct requirements and constraints. There's no need to look for conflicts,
26-
// since conflicting versions will be tracked upstream.
27-
for requirement in
28-
manifest
29-
.requirements
30-
.iter()
31-
.chain(manifest.constraints.iter())
32-
.chain(manifest.overrides.iter())
33-
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
34-
.chain(manifest.lookaheads.iter().flat_map(|lookahead| {
35-
lookahead.requirements().iter().filter(|requirement| {
36-
requirement.evaluate_markers(markers, lookahead.extras())
37-
})
38-
}))
39-
.chain(manifest.editables.iter().flat_map(|(editable, metadata)| {
40-
metadata.requires_dist.iter().filter(|requirement| {
41-
requirement.evaluate_markers(markers, &editable.extras)
42-
})
43-
}))
44-
{
26+
// since conflicts will be enforced by the solver.
27+
for requirement in manifest.requirements(markers) {
4528
if let Some(version_or_url) = requirement.version_or_url.as_ref() {
4629
for local in iter_locals(version_or_url) {
4730
required.insert(requirement.name.clone(), local);

crates/uv-resolver/src/version_map.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,10 @@ impl VersionMap {
136136
/// stored in this map. For example, the versions `1.2.0` and `1.2` are
137137
/// semantically equivalent, but when converted to strings, they are
138138
/// distinct.
139-
pub(crate) fn get_with_version<'a>(
140-
&'a self,
139+
pub(crate) fn get_with_version(
140+
&self,
141141
version: &Version,
142-
) -> Option<(&'a Version, &'a PrioritizedDist)> {
142+
) -> Option<(&Version, &PrioritizedDist)> {
143143
match self.inner {
144144
VersionMapInner::Eager(ref map) => map.get_key_value(version),
145145
VersionMapInner::Lazy(ref lazy) => lazy.get_with_version(version),

crates/uv-resolver/src/yanks.rs

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ use pep440_rs::Version;
44
use pep508_rs::{MarkerEnvironment, VersionOrUrl};
55
use uv_normalize::PackageName;
66

7-
use crate::preferences::Preference;
8-
use crate::Manifest;
7+
use crate::{Manifest, Preference};
98

109
/// A set of package versions that are permitted, even if they're marked as yanked by the
1110
/// relevant index.
@@ -15,24 +14,9 @@ pub struct AllowedYanks(FxHashMap<PackageName, FxHashSet<Version>>);
1514
impl AllowedYanks {
1615
pub fn from_manifest(manifest: &Manifest, markers: &MarkerEnvironment) -> Self {
1716
let mut allowed_yanks = FxHashMap::<PackageName, FxHashSet<Version>>::default();
18-
for requirement in
19-
manifest
20-
.requirements
21-
.iter()
22-
.chain(manifest.constraints.iter())
23-
.chain(manifest.overrides.iter())
24-
.chain(manifest.preferences.iter().map(Preference::requirement))
25-
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
26-
.chain(manifest.lookaheads.iter().flat_map(|lookahead| {
27-
lookahead.requirements().iter().filter(|requirement| {
28-
requirement.evaluate_markers(markers, lookahead.extras())
29-
})
30-
}))
31-
.chain(manifest.editables.iter().flat_map(|(editable, metadata)| {
32-
metadata.requires_dist.iter().filter(|requirement| {
33-
requirement.evaluate_markers(markers, &editable.extras)
34-
})
35-
}))
17+
for requirement in manifest
18+
.requirements(markers)
19+
.chain(manifest.preferences.iter().map(Preference::requirement))
3620
{
3721
let Some(VersionOrUrl::VersionSpecifier(specifiers)) = &requirement.version_or_url
3822
else {

crates/uv/tests/pip_compile.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1815,6 +1815,85 @@ fn allowed_transitive_url_path_dependency() -> Result<()> {
18151815
Ok(())
18161816
}
18171817

1818+
/// A dependency with conflicting URLs in `requirements.in` and `constraints.txt` should arguably
1819+
/// be ignored if the dependency has an override. However, we currently error in this case.
1820+
#[test]
1821+
fn requirement_constraint_override_url() -> Result<()> {
1822+
let context = TestContext::new("3.12");
1823+
1824+
let requirements_in = context.temp_dir.child("requirements.in");
1825+
requirements_in.write_str("anyio @ https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz")?;
1826+
1827+
let constraints_txt = context.temp_dir.child("constraints.txt");
1828+
constraints_txt.write_str("anyio @ https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl")?;
1829+
1830+
let overrides_txt = context.temp_dir.child("overrides.txt");
1831+
overrides_txt.write_str("anyio==3.7.0")?;
1832+
1833+
uv_snapshot!(context.compile()
1834+
.arg("requirements.in")
1835+
.arg("--constraint")
1836+
.arg("constraints.txt")
1837+
.arg("--override")
1838+
.arg("overrides.txt"), @r###"
1839+
success: false
1840+
exit_code: 2
1841+
----- stdout -----
1842+
1843+
----- stderr -----
1844+
error: Requirements contain conflicting URLs for package `anyio`:
1845+
- https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz
1846+
- https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl
1847+
"###
1848+
);
1849+
1850+
Ok(())
1851+
}
1852+
1853+
/// A dependency that uses a pre-release marker in `requirements.in` should be overridden by a
1854+
/// non-pre-release version in `overrides.txt`. We currently allow Flask to use a pre-release below,
1855+
/// but probably shouldn't.
1856+
#[test]
1857+
fn requirement_override_prerelease() -> Result<()> {
1858+
let context = TestContext::new("3.12");
1859+
1860+
let requirements_in = context.temp_dir.child("requirements.in");
1861+
requirements_in.write_str("flask<2.0.0rc4")?;
1862+
1863+
let overrides_txt = context.temp_dir.child("overrides.txt");
1864+
overrides_txt.write_str("flask<2.0.1,!=2.0.0")?;
1865+
1866+
uv_snapshot!(context.compile()
1867+
.arg("requirements.in")
1868+
.arg("--override")
1869+
.arg("overrides.txt"), @r###"
1870+
success: true
1871+
exit_code: 0
1872+
----- stdout -----
1873+
# This file was autogenerated by uv via the following command:
1874+
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --override overrides.txt
1875+
click==8.1.7
1876+
# via flask
1877+
flask==2.0.0rc2
1878+
itsdangerous==2.1.2
1879+
# via flask
1880+
jinja2==3.1.3
1881+
# via flask
1882+
markupsafe==2.1.5
1883+
# via
1884+
# jinja2
1885+
# werkzeug
1886+
werkzeug==3.0.1
1887+
# via flask
1888+
1889+
----- stderr -----
1890+
Resolved 6 packages in [TIME]
1891+
"###
1892+
);
1893+
1894+
Ok(())
1895+
}
1896+
18181897
/// Resolve packages from all optional dependency groups in a `pyproject.toml` file.
18191898
#[test]
18201899
fn compile_pyproject_toml_all_extras() -> Result<()> {

0 commit comments

Comments
 (0)