Skip to content

Commit 19b25f5

Browse files
committed
Allow multiple disjoint URLs in overrides
1 parent 0652800 commit 19b25f5

File tree

3 files changed

+118
-40
lines changed

3 files changed

+118
-40
lines changed

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -2245,10 +2245,10 @@ impl ForkState {
22452245
// requirement was a URL requirement. `Urls` applies canonicalization to this and
22462246
// override URLs to both URL and registry requirements, which we then check for
22472247
// conflicts using [`ForkUrl`].
2248-
if let Some(url) = urls.get_url(&self.env, name, url.as_ref(), git)? {
2248+
for url in urls.get_url(&self.env, name, url.as_ref(), git)? {
22492249
self.fork_urls.insert(name, url, &self.env)?;
22502250
has_url = true;
2251-
};
2251+
}
22522252

22532253
// If the package is pinned to an exact index, add it to the fork.
22542254
for index in indexes.get(name, &self.env) {

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

+38-38
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
use std::iter;
2-
1+
use either::Either;
32
use rustc_hash::FxHashMap;
43
use same_file::is_same_file;
54
use tracing::debug;
@@ -8,7 +7,7 @@ use uv_cache_key::CanonicalUrl;
87
use uv_distribution_types::Verbatim;
98
use uv_git::GitResolver;
109
use uv_normalize::PackageName;
11-
use uv_pep508::VerbatimUrl;
10+
use uv_pep508::{MarkerTree, VerbatimUrl};
1211
use uv_pypi_types::{ParsedDirectoryUrl, ParsedUrl, VerbatimParsedUrl};
1312

1413
use crate::{DependencyMode, Manifest, ResolveError, ResolverEnvironment};
@@ -24,10 +23,10 @@ use crate::{DependencyMode, Manifest, ResolveError, ResolverEnvironment};
2423
/// [`crate::fork_urls::ForkUrls`].
2524
#[derive(Debug, Default)]
2625
pub(crate) struct Urls {
27-
/// URL requirements in overrides. There can only be a single URL per package in overrides
28-
/// (since it replaces all other URLs), and an override URL replaces all requirements and
29-
/// constraints URLs.
30-
overrides: FxHashMap<PackageName, VerbatimParsedUrl>,
26+
/// URL requirements in overrides. An override URL replaces all requirements and constraints
27+
/// URLs. There can be multiple URLs for the same package as long as they are in different
28+
/// forks.
29+
overrides: FxHashMap<PackageName, Vec<(MarkerTree, VerbatimParsedUrl)>>,
3130
/// URLs from regular requirements or from constraints. There can be multiple URLs for the same
3231
/// package as long as they are in different forks.
3332
regular: FxHashMap<PackageName, Vec<VerbatimParsedUrl>>,
@@ -41,7 +40,8 @@ impl Urls {
4140
dependencies: DependencyMode,
4241
) -> Result<Self, ResolveError> {
4342
let mut urls: FxHashMap<PackageName, Vec<VerbatimParsedUrl>> = FxHashMap::default();
44-
let mut overrides: FxHashMap<PackageName, VerbatimParsedUrl> = FxHashMap::default();
43+
let mut overrides: FxHashMap<PackageName, Vec<(MarkerTree, VerbatimParsedUrl)>> =
44+
FxHashMap::default();
4545

4646
// Add all direct regular requirements and constraints URL.
4747
for requirement in manifest.requirements_no_overrides(env, dependencies) {
@@ -86,16 +86,10 @@ impl Urls {
8686
// a requirements.txt entry `./anyio`, we still use the URL. See
8787
// `allow_recursive_url_local_path_override_constraint`.
8888
urls.remove(&requirement.name);
89-
let previous = overrides.insert(requirement.name.clone(), url.clone());
90-
if let Some(previous) = previous {
91-
if !same_resource(&previous.parsed_url, &url.parsed_url, git) {
92-
return Err(ResolveError::ConflictingOverrideUrls(
93-
requirement.name.clone(),
94-
previous.verbatim.verbatim().to_string(),
95-
url.verbatim.verbatim().to_string(),
96-
));
97-
}
98-
}
89+
overrides
90+
.entry(requirement.name.clone())
91+
.or_default()
92+
.push((requirement.marker, url));
9993
}
10094

10195
Ok(Self {
@@ -104,41 +98,47 @@ impl Urls {
10498
})
10599
}
106100

107-
/// Check and canonicalize the URL of a requirement.
101+
/// Return an iterator over the allowed URLs for the given package.
108102
///
109103
/// If we have a URL override, apply it unconditionally for registry and URL requirements.
110-
/// Otherwise, there are two case: For a URL requirement (`url` isn't `None`), check that the
111-
/// URL is allowed and return its canonical form. For registry requirements, we return `None`
112-
/// if there is no override.
104+
/// Otherwise, there are two case: for a URL requirement (`url` isn't `None`), check that the
105+
/// URL is allowed and return its canonical form.
106+
///
107+
/// For registry requirements, we return an empty iterator.
113108
pub(crate) fn get_url<'a>(
114109
&'a self,
115-
env: &ResolverEnvironment,
110+
env: &'a ResolverEnvironment,
116111
name: &'a PackageName,
117112
url: Option<&'a VerbatimParsedUrl>,
118113
git: &'a GitResolver,
119-
) -> Result<Option<&'a VerbatimParsedUrl>, ResolveError> {
120-
if let Some(override_url) = self.get_override(name) {
121-
Ok(Some(override_url))
114+
) -> Result<impl Iterator<Item = &'a VerbatimParsedUrl>, ResolveError> {
115+
if let Some(override_urls) = self.get_overrides(name) {
116+
Ok(Either::Left(Either::Left(
117+
override_urls.into_iter().filter_map(|(marker, url)| {
118+
if env.included_by_marker(*marker) {
119+
Some(url)
120+
} else {
121+
None
122+
}
123+
}),
124+
)))
122125
} else if let Some(url) = url {
123-
Ok(Some(self.canonicalize_allowed_url(
124-
env,
125-
name,
126-
git,
127-
&url.verbatim,
128-
&url.parsed_url,
129-
)?))
126+
let url =
127+
self.canonicalize_allowed_url(env, name, git, &url.verbatim, &url.parsed_url)?;
128+
Ok(Either::Left(Either::Right(std::iter::once(url))))
130129
} else {
131-
Ok(None)
130+
Ok(Either::Right(std::iter::empty()))
132131
}
133132
}
134133

134+
/// Return `true` if the package has any URL (from overrides or regular requirements).
135135
pub(crate) fn any_url(&self, name: &PackageName) -> bool {
136-
self.get_override(name).is_some() || self.get_regular(name).is_some()
136+
self.get_overrides(name).is_some() || self.get_regular(name).is_some()
137137
}
138138

139139
/// Return the [`VerbatimUrl`] override for the given package, if any.
140-
fn get_override(&self, package: &PackageName) -> Option<&VerbatimParsedUrl> {
141-
self.overrides.get(package)
140+
fn get_overrides(&self, package: &PackageName) -> Option<&[(MarkerTree, VerbatimParsedUrl)]> {
141+
self.overrides.get(package).map(Vec::as_slice)
142142
}
143143

144144
/// Return the allowed [`VerbatimUrl`]s for given package from regular requirements and
@@ -174,7 +174,7 @@ impl Urls {
174174
let mut conflicting_urls: Vec<_> = matching_urls
175175
.into_iter()
176176
.map(|parsed_url| parsed_url.verbatim.verbatim().to_string())
177-
.chain(iter::once(verbatim_url.verbatim().to_string()))
177+
.chain(std::iter::once(verbatim_url.verbatim().to_string()))
178178
.collect();
179179
conflicting_urls.sort();
180180
return Err(ResolveError::ConflictingUrls {

crates/uv/tests/it/pip_compile.rs

+78
Original file line numberDiff line numberDiff line change
@@ -13858,6 +13858,84 @@ fn universal_disjoint_deprecated_markers() -> Result<()> {
1385813858
Ok(())
1385913859
}
1386013860

13861+
#[test]
13862+
fn universal_disjoint_override_urls() -> Result<()> {
13863+
let context = TestContext::new("3.12");
13864+
let requirements_in = context.temp_dir.child("requirements.in");
13865+
requirements_in.write_str(indoc::indoc! {r"
13866+
anyio
13867+
"})?;
13868+
13869+
let overrides_txt = context.temp_dir.child("overrides.txt");
13870+
overrides_txt.write_str(indoc::indoc! {r"
13871+
sniffio @ https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl ; sys_platform == 'win32'
13872+
sniffio @ https://files.pythonhosted.org/packages/c3/a0/5dba8ed157b0136607c7f2151db695885606968d1fae123dc3391e0cfdbf/sniffio-1.3.0-py3-none-any.whl ; sys_platform == 'darwin'
13873+
"})?;
13874+
13875+
uv_snapshot!(context.filters(), context.pip_compile()
13876+
.arg("requirements.in")
13877+
.arg("--overrides")
13878+
.arg("overrides.txt")
13879+
.arg("--universal"), @r###"
13880+
success: true
13881+
exit_code: 0
13882+
----- stdout -----
13883+
# This file was autogenerated by uv via the following command:
13884+
# uv pip compile --cache-dir [CACHE_DIR] requirements.in --overrides overrides.txt --universal
13885+
anyio==4.3.0
13886+
# via -r requirements.in
13887+
idna==3.6
13888+
# via anyio
13889+
sniffio @ https://files.pythonhosted.org/packages/c3/a0/5dba8ed157b0136607c7f2151db695885606968d1fae123dc3391e0cfdbf/sniffio-1.3.0-py3-none-any.whl ; sys_platform == 'darwin'
13890+
# via
13891+
# --override overrides.txt
13892+
# anyio
13893+
sniffio @ https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl ; sys_platform == 'win32'
13894+
# via
13895+
# --override overrides.txt
13896+
# anyio
13897+
13898+
----- stderr -----
13899+
Resolved 4 packages in [TIME]
13900+
"###
13901+
);
13902+
13903+
Ok(())
13904+
}
13905+
13906+
#[test]
13907+
fn universal_conflicting_override_urls() -> Result<()> {
13908+
let context = TestContext::new("3.12");
13909+
let requirements_in = context.temp_dir.child("requirements.in");
13910+
requirements_in.write_str(indoc::indoc! {r"
13911+
anyio
13912+
"})?;
13913+
13914+
let overrides_txt = context.temp_dir.child("overrides.txt");
13915+
overrides_txt.write_str(indoc::indoc! {r"
13916+
sniffio @ https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl ; sys_platform == 'win32'
13917+
sniffio @ https://files.pythonhosted.org/packages/c3/a0/5dba8ed157b0136607c7f2151db695885606968d1fae123dc3391e0cfdbf/sniffio-1.3.0-py3-none-any.whl ; sys_platform == 'darwin' or sys_platform == 'win32'
13918+
"})?;
13919+
13920+
uv_snapshot!(context.filters(), context.pip_compile()
13921+
.arg("requirements.in")
13922+
.arg("--overrides")
13923+
.arg("overrides.txt")
13924+
.arg("--universal"), @r###"
13925+
success: false
13926+
exit_code: 2
13927+
----- stdout -----
13928+
13929+
----- stderr -----
13930+
error: Requirements contain conflicting URLs for package `sniffio` in split `sys_platform == 'win32'`:
13931+
- https://files.pythonhosted.org/packages/c3/a0/5dba8ed157b0136607c7f2151db695885606968d1fae123dc3391e0cfdbf/sniffio-1.3.0-py3-none-any.whl
13932+
- https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl
13933+
"###
13934+
);
13935+
13936+
Ok(())
13937+
}
13938+
1386113939
#[test]
1386213940
fn compile_lowest_extra_unpinned_warning() -> Result<()> {
1386313941
let context = TestContext::new("3.12");

0 commit comments

Comments
 (0)