Prevent uv tool upgrade from installing excluded dependencies (#18022)
## Summary <!-- What's the purpose of the change? What does it do, and why? --> Fixes #18021 Store dependencies excluded by uv tool install in the tool receipt and provide these same dependencies to the requirements resolver when the tool is upgraded. ## Test Plan <!-- How was it tested? --> Running the repro commands provided in issue #18021, we can see the excluded dependency does not get reinstalled when the tool is upgraded: ```sh $ cat /tmp/excludes.txt markdown-it-py $ ~/Dev/uv/target/debug/uv install --excludes /tmp/excludes.txt 2048-cli==1.0.2 Resolved 7 packages in 150ms Installed 7 packages in 56ms + 2048-cli==1.0.2 + click==8.3.1 + maturin==1.12.0 + numpy==2.4.2 + pygments==2.19.2 + rich==13.9.4 + rich-menu==0.3.0 Installed 1 executable: 2048-cli $ cat ~/.local/share/uv/tools/2048-cli/uv-receipt.toml [tool] requirements = [{ name = "2048-cli" }] excludes = ["markdown-it-py"] entrypoints = [ { name = "2048-cli", install-path = "/home/brad/.local/bin/2048-cli", from = "2048-cli" }, ] $ ~/Dev/uv/target/debug/uv tool upgrade 2048-cli Updated 2048-cli v1.0.2 -> v1.0.3 - 2048-cli==1.0.2 + 2048-cli==1.0.3 Installed 1 executable: 2048-cli ``` --------- Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
@@ -730,6 +730,32 @@ impl RequirementsSpecification {
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize a [`RequirementsSpecification`] from a list of [`Requirement`], including
|
||||
/// constraints, overrides, and excludes.
|
||||
pub fn from_excludes(
|
||||
requirements: Vec<Requirement>,
|
||||
constraints: Vec<Requirement>,
|
||||
overrides: Vec<Requirement>,
|
||||
excludes: Vec<PackageName>,
|
||||
) -> Self {
|
||||
Self {
|
||||
requirements: requirements
|
||||
.into_iter()
|
||||
.map(UnresolvedRequirementSpecification::from)
|
||||
.collect(),
|
||||
constraints: constraints
|
||||
.into_iter()
|
||||
.map(NameRequirementSpecification::from)
|
||||
.collect(),
|
||||
overrides: overrides
|
||||
.into_iter()
|
||||
.map(UnresolvedRequirementSpecification::from)
|
||||
.collect(),
|
||||
excludes,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if the specification does not include any requirements to install.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.requirements.is_empty() && self.source_trees.is_empty() && self.overrides.is_empty()
|
||||
|
||||
@@ -6,6 +6,7 @@ use toml_edit::{Array, Item, Table, Value, value};
|
||||
|
||||
use uv_distribution_types::Requirement;
|
||||
use uv_fs::{PortablePath, Simplified};
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pypi_types::VerbatimParsedUrl;
|
||||
use uv_python::PythonRequest;
|
||||
use uv_settings::ToolOptions;
|
||||
@@ -20,6 +21,8 @@ pub struct Tool {
|
||||
constraints: Vec<Requirement>,
|
||||
/// The overrides requested by the user during installation.
|
||||
overrides: Vec<Requirement>,
|
||||
/// The excludes requested by the user during installation.
|
||||
excludes: Vec<PackageName>,
|
||||
/// The build constraints requested by the user during installation.
|
||||
build_constraints: Vec<Requirement>,
|
||||
/// The Python requested by the user during installation.
|
||||
@@ -40,6 +43,8 @@ struct ToolWire {
|
||||
#[serde(default)]
|
||||
overrides: Vec<Requirement>,
|
||||
#[serde(default)]
|
||||
excludes: Vec<PackageName>,
|
||||
#[serde(default)]
|
||||
build_constraint_dependencies: Vec<Requirement>,
|
||||
python: Option<PythonRequest>,
|
||||
entrypoints: Vec<ToolEntrypoint>,
|
||||
@@ -67,6 +72,7 @@ impl From<Tool> for ToolWire {
|
||||
.collect(),
|
||||
constraints: tool.constraints,
|
||||
overrides: tool.overrides,
|
||||
excludes: tool.excludes,
|
||||
build_constraint_dependencies: tool.build_constraints,
|
||||
python: tool.python,
|
||||
entrypoints: tool.entrypoints,
|
||||
@@ -90,6 +96,7 @@ impl TryFrom<ToolWire> for Tool {
|
||||
.collect(),
|
||||
constraints: tool.constraints,
|
||||
overrides: tool.overrides,
|
||||
excludes: tool.excludes,
|
||||
build_constraints: tool.build_constraint_dependencies,
|
||||
python: tool.python,
|
||||
entrypoints: tool.entrypoints,
|
||||
@@ -165,6 +172,7 @@ impl Tool {
|
||||
requirements: Vec<Requirement>,
|
||||
constraints: Vec<Requirement>,
|
||||
overrides: Vec<Requirement>,
|
||||
excludes: Vec<PackageName>,
|
||||
build_constraints: Vec<Requirement>,
|
||||
python: Option<PythonRequest>,
|
||||
entrypoints: impl IntoIterator<Item = ToolEntrypoint>,
|
||||
@@ -176,6 +184,7 @@ impl Tool {
|
||||
requirements,
|
||||
constraints,
|
||||
overrides,
|
||||
excludes,
|
||||
build_constraints,
|
||||
python,
|
||||
entrypoints,
|
||||
@@ -259,6 +268,28 @@ impl Tool {
|
||||
});
|
||||
}
|
||||
|
||||
if !self.excludes.is_empty() {
|
||||
table.insert("excludes", {
|
||||
let excludes = self
|
||||
.excludes
|
||||
.iter()
|
||||
.map(|r#exclude| {
|
||||
serde::Serialize::serialize(
|
||||
&r#exclude,
|
||||
toml_edit::ser::ValueSerializer::new(),
|
||||
)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let excludes = match excludes.as_slice() {
|
||||
[] => Array::new(),
|
||||
[r#exclude] => Array::from_iter([r#exclude]),
|
||||
excludes => each_element_on_its_line_array(excludes.iter()),
|
||||
};
|
||||
value(excludes)
|
||||
});
|
||||
}
|
||||
|
||||
if !self.build_constraints.is_empty() {
|
||||
table.insert("build-constraint-dependencies", {
|
||||
let build_constraints = self
|
||||
@@ -331,6 +362,10 @@ impl Tool {
|
||||
&self.overrides
|
||||
}
|
||||
|
||||
pub fn excludes(&self) -> &[PackageName] {
|
||||
&self.excludes
|
||||
}
|
||||
|
||||
pub fn build_constraints(&self) -> &[Requirement] {
|
||||
&self.build_constraints
|
||||
}
|
||||
|
||||
@@ -180,6 +180,7 @@ pub(crate) fn finalize_tool_install(
|
||||
requirements: Vec<Requirement>,
|
||||
constraints: Vec<Requirement>,
|
||||
overrides: Vec<Requirement>,
|
||||
excludes: Vec<PackageName>,
|
||||
build_constraints: Vec<Requirement>,
|
||||
printer: Printer,
|
||||
) -> anyhow::Result<()> {
|
||||
@@ -334,6 +335,7 @@ pub(crate) fn finalize_tool_install(
|
||||
requirements,
|
||||
constraints,
|
||||
overrides,
|
||||
excludes,
|
||||
build_constraints,
|
||||
python,
|
||||
installed_entrypoints,
|
||||
|
||||
@@ -396,6 +396,9 @@ pub(crate) async fn install(
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Resolve the excludes.
|
||||
let excludes = spec.excludes.clone();
|
||||
|
||||
// Resolve the build constraints.
|
||||
let build_constraints: Vec<Requirement> =
|
||||
operations::read_constraints(build_constraints, &client_builder)
|
||||
@@ -753,6 +756,7 @@ pub(crate) async fn install(
|
||||
requirements,
|
||||
constraints,
|
||||
overrides,
|
||||
excludes,
|
||||
build_constraints,
|
||||
printer,
|
||||
)?;
|
||||
|
||||
@@ -323,7 +323,7 @@ async fn upgrade_tool(
|
||||
Constraints::from_requirements(existing_tool_receipt.build_constraints().iter().cloned());
|
||||
|
||||
// Resolve the requirements.
|
||||
let spec = RequirementsSpecification::from_overrides(
|
||||
let spec = RequirementsSpecification::from_excludes(
|
||||
existing_tool_receipt.requirements().to_vec(),
|
||||
existing_tool_receipt
|
||||
.constraints()
|
||||
@@ -332,6 +332,7 @@ async fn upgrade_tool(
|
||||
.cloned()
|
||||
.collect(),
|
||||
existing_tool_receipt.overrides().to_vec(),
|
||||
existing_tool_receipt.excludes().to_vec(),
|
||||
);
|
||||
|
||||
// Initialize any shared state.
|
||||
@@ -446,6 +447,7 @@ async fn upgrade_tool(
|
||||
existing_tool_receipt.requirements().to_vec(),
|
||||
existing_tool_receipt.constraints().to_vec(),
|
||||
existing_tool_receipt.overrides().to_vec(),
|
||||
existing_tool_receipt.excludes().to_vec(),
|
||||
existing_tool_receipt.build_constraints().to_vec(),
|
||||
printer,
|
||||
)?;
|
||||
|
||||
@@ -1010,6 +1010,66 @@ fn test_tool_upgrade_additional_entrypoints() {
|
||||
");
|
||||
}
|
||||
|
||||
/// Upgrade a tool with an excluded dependency.
|
||||
///
|
||||
/// Compare with `tool_upgrade_respect_constraints`, which shows `pytz` being
|
||||
/// upgraded alongside `babel`. Here, `pytz` is excluded, so it should remain
|
||||
/// absent after the upgrade.
|
||||
#[test]
|
||||
fn tool_upgrade_excludes() {
|
||||
let context = uv_test::test_context!("3.12")
|
||||
.with_filtered_counts()
|
||||
.with_filtered_exe_suffix();
|
||||
let tool_dir = context.temp_dir.child("tools");
|
||||
let bin_dir = context.temp_dir.child("bin");
|
||||
|
||||
let excludes_txt = context.temp_dir.child("excludes.txt");
|
||||
excludes_txt.write_str("pytz").unwrap();
|
||||
|
||||
// Install `babel` from Test PyPI, to get an outdated version.
|
||||
// `pytz` is excluded, so it won't be installed despite being a dependency.
|
||||
uv_snapshot!(context.filters(), context.tool_install()
|
||||
.arg("babel<2.10")
|
||||
.arg("--excludes")
|
||||
.arg("excludes.txt")
|
||||
.arg("--index-url")
|
||||
.arg("https://test.pypi.org/simple/")
|
||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved [N] packages in [TIME]
|
||||
Prepared [N] packages in [TIME]
|
||||
Installed [N] packages in [TIME]
|
||||
+ babel==2.6.0
|
||||
Installed 1 executable: pybabel
|
||||
");
|
||||
|
||||
// Upgrade `babel` from PyPI. Babel should be updated (within the `<2.10`
|
||||
// constraint), but `pytz` should remain excluded.
|
||||
uv_snapshot!(context.filters(), context.tool_upgrade()
|
||||
.arg("babel")
|
||||
.arg("--index-url")
|
||||
.arg("https://pypi.org/simple/")
|
||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Updated babel v2.6.0 -> v2.9.1
|
||||
- babel==2.6.0
|
||||
+ babel==2.9.1
|
||||
Installed 1 executable: pybabel
|
||||
");
|
||||
}
|
||||
|
||||
/// When upgrading a tool from an authenticated index with invalid credentials,
|
||||
/// the command should fail with an auth error rather than silently reporting
|
||||
/// "Nothing to upgrade".
|
||||
|
||||
Reference in New Issue
Block a user