Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 63 additions & 33 deletions crates/uv-configuration/src/overrides.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::borrow::Cow;

use either::Either;
use rustc_hash::{FxBuildHasher, FxHashMap};
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
use serde::de::IntoDeserializer;

use uv_distribution_types::{Requirement, RequirementSource};
Expand Down Expand Up @@ -288,42 +288,72 @@ impl Overrides {
I: IntoIterator<Item = &'a Requirement>,
{
let scoped = package.and_then(|(package, version)| self.scoped_for(package, version));
if self.global.is_empty() && scoped.is_none() {
if let Some(scoped) = scoped {
let requirements = requirements.into_iter().collect::<Vec<_>>();
let names = requirements
.iter()
.map(|requirement| requirement.name.clone())
.collect::<FxHashSet<_>>();
let mut additions = scoped
.overrides
.iter()
.filter(|(name, _)| !names.contains(*name))
.flat_map(|(_, requirements)| requirements)
.collect::<Vec<_>>();
additions.sort_unstable();

return Either::Left(
requirements
.into_iter()
.flat_map(move |requirement| self.apply_requirement(requirement, Some(scoped)))
.chain(additions.into_iter().map(Cow::Borrowed)),
);
}

if self.global.is_empty() {
// Fast path: There are no overrides.
return Either::Left(requirements.into_iter().map(Cow::Borrowed));
return Either::Right(Either::Left(requirements.into_iter().map(Cow::Borrowed)));
}

Either::Right(requirements.into_iter().flat_map(move |requirement| {
let overrides = scoped
.and_then(|scoped| scoped.overrides.get(&requirement.name))
.or_else(|| self.get(&requirement.name));
let Some(overrides) = overrides else {
// Case 1: No override(s).
return Either::Left(std::iter::once(Cow::Borrowed(requirement)));
};
Either::Right(Either::Right(requirements.into_iter().flat_map(
move |requirement| self.apply_requirement(requirement, None),
)))
}

fn apply_requirement<'a>(
&'a self,
requirement: &'a Requirement,
scoped: Option<&'a ScopedOverrides>,
) -> impl Iterator<Item = Cow<'a, Requirement>> {
let overrides = scoped
.and_then(|scoped| scoped.overrides.get(&requirement.name))
.or_else(|| self.get(&requirement.name));
let Some(overrides) = overrides else {
// Case 1: No override(s).
return Either::Left(std::iter::once(Cow::Borrowed(requirement)));
};

// ASSUMPTION: There is one `extra = "..."`, and it's either the only marker or part
// of the main conjunction.
let Some(extra_expression) = requirement.marker.top_level_extra() else {
// Case 2: A non-optional dependency with override(s).
return Either::Right(Either::Right(overrides.iter().map(Cow::Borrowed)));
};
// ASSUMPTION: There is one `extra = "..."`, and it's either the only marker or part
// of the main conjunction.
let Some(extra_expression) = requirement.marker.top_level_extra() else {
// Case 2: A non-optional dependency with override(s).
return Either::Right(Either::Right(overrides.iter().map(Cow::Borrowed)));
};

// Case 3: An optional dependency with override(s).
//
// When the original requirement is an optional dependency, the override(s) need to
// be optional for the same extra, otherwise we activate extras that should be inactive.
Either::Right(Either::Left(overrides.iter().map(
move |override_requirement| {
// Add the extra to the override marker.
let mut joint_marker = MarkerTree::expression(extra_expression.clone());
joint_marker.and(override_requirement.marker);
Cow::Owned(Requirement {
marker: joint_marker,
..override_requirement.clone()
})
},
)))
}))
// Case 3: An optional dependency with override(s).
//
// When the original requirement is an optional dependency, the override(s) need to
// be optional for the same extra, otherwise we activate extras that should be inactive.
Either::Right(Either::Left(overrides.iter().map(
move |override_requirement| {
// Add the extra to the override marker.
let mut joint_marker = MarkerTree::expression(extra_expression.clone());
joint_marker.and(override_requirement.marker);
Cow::Owned(Requirement {
marker: joint_marker,
..override_requirement.clone()
})
},
)))
}
}
5 changes: 3 additions & 2 deletions crates/uv-workspace/src/pyproject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -464,8 +464,9 @@ pub struct ToolUv {
/// Overrides can be limited to the dependencies declared by a specific package version by
/// using a table with `package` and `dependencies`. The `package` table identifies the package
/// whose dependencies will be overridden by `name` and, optionally, `version`. If `version` is
/// omitted, the overrides apply to all versions of that package. Dependencies not listed in
/// `dependencies` are left unchanged.
/// omitted, the overrides apply to all versions of that package. Requirements in `dependencies`
/// replace dependencies with the same name and add dependencies that are not declared by the
/// package. Dependencies not listed in `dependencies` are left unchanged.
///
/// Scoped overrides currently support registry version specifiers only. Direct URL and path
/// sources, including Git sources, and explicit indexes are not supported.
Expand Down
8 changes: 6 additions & 2 deletions crates/uv/tests/pip_compile/pip_compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4290,7 +4290,7 @@ fn scoped_override_dependency_from_pyproject() -> Result<()> {

[tool.uv]
override-dependencies = [
{ package = { name = "anyio", version = "3.7.0" }, dependencies = ["idna==3.2"] }
{ package = { name = "anyio", version = "3.7.0" }, dependencies = ["idna==3.2", "typing-extensions==4.10.0"] }
]
"#,
)?;
Expand All @@ -4312,9 +4312,13 @@ fn scoped_override_dependency_from_pyproject() -> Result<()> {
# anyio
sniffio==1.3.1
# via anyio
typing-extensions==4.10.0
# via
# --override (workspace)
# anyio

----- stderr -----
Resolved 3 packages in [TIME]
Resolved 4 packages in [TIME]
");

pyproject_toml.write_str(
Expand Down
13 changes: 7 additions & 6 deletions docs/concepts/resolution.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,8 +405,8 @@ requirements on `pydantic`, replacing them with the override. In the above examp

While constraints can only _reduce_ the set of acceptable versions for a package, overrides can
_expand_ the set of acceptable versions, providing an escape hatch for erroneous upper version
bounds. As with constraints, overrides do not add a dependency on the package and only take effect
if the package is requested in a direct or transitive dependency.
bounds. As with constraints, global overrides do not add a dependency on the package and only take
effect if the package is requested in a direct or transitive dependency.

In a `pyproject.toml`, use `tool.uv.override-dependencies` to define a list of overrides. In the
pip-compatible interface, the `--override` option can be used to pass files with the same format as
Expand All @@ -425,10 +425,11 @@ override-dependencies = [
```

In this example, `foo>1` is the global override, while `foo>2` replaces requirements for `foo`
declared by `bar==0.0.5`. Other dependencies declared by `bar` are unchanged. The `version` field in
`package` can be omitted to apply the scoped overrides to all versions of `bar`. A version-specific
entry takes precedence over an all-versions entry, and a scoped override takes precedence over a
global override for the same dependency.
declared by `bar==0.0.5`. If `bar` does not declare a dependency on `foo`, the scoped override adds
it. Other dependencies declared by `bar` are unchanged. The `version` field in `package` can be
omitted to apply the scoped overrides to all versions of `bar`. A version-specific entry takes
precedence over an all-versions entry, and a scoped override takes precedence over a global override
for the same dependency.

Scoped overrides currently support registry version specifiers only. Direct URL and path sources,
including Git sources, and explicit indexes are not supported.
Expand Down
2 changes: 1 addition & 1 deletion uv.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading