Add bounds in `uv add --script` (#16954)

## Summary

Closes https://github.com/astral-sh/uv/issues/15544.
This commit is contained in:
Charlie Marsh 2025-12-03 05:37:04 -08:00 committed by GitHub
parent 932d7b8fce
commit e00cc8c35f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 121 additions and 72 deletions

View File

@ -708,17 +708,14 @@ pub(crate) async fn add(
return Ok(ExitStatus::Success);
}
// If we're modifying a script, and lockfile doesn't exist, don't create it.
if let AddTarget::Script(ref script, _) = target {
if !LockTarget::from(script).lock_path().is_file() {
writeln!(
printer.stderr(),
"Updated `{}`",
script.path.user_display().cyan()
)?;
return Ok(ExitStatus::Success);
}
}
// If we're modifying a script, and lockfile doesn't exist, avoid creating it. We still need
// to perform resolution, since we want to use the resolved versions to populate lower bounds
// in the script.
let dry_run = if let AddTarget::Script(ref script, _) = target {
!LockTarget::from(script).lock_path().is_file()
} else {
false
};
// Update the `pypackage.toml` in-memory.
let target = target.update(&content)?;
@ -763,6 +760,7 @@ pub(crate) async fn add(
&defaulted_groups,
raw,
bounds,
dry_run,
constraints,
&settings,
&client_builder,
@ -1004,6 +1002,7 @@ async fn lock_and_sync(
groups: &DependencyGroupsWithDefaults,
raw: bool,
bound_kind: Option<AddBoundsKind>,
dry_run: bool,
constraints: Vec<NameRequirementSpecification>,
settings: &ResolverInstallerSettings,
client_builder: &BaseClientBuilder<'_>,
@ -1017,6 +1016,8 @@ async fn lock_and_sync(
project::lock::LockOperation::new(
if let LockCheck::Enabled(lock_check) = lock_check {
LockMode::Locked(target.interpreter(), lock_check)
} else if dry_run {
LockMode::DryRun(target.interpreter())
} else {
LockMode::Write(target.interpreter())
},
@ -1140,6 +1141,8 @@ async fn lock_and_sync(
project::lock::LockOperation::new(
if let LockCheck::Enabled(lock_check) = lock_check {
LockMode::Locked(target.interpreter(), lock_check)
} else if dry_run {
LockMode::DryRun(target.interpreter())
} else {
LockMode::Write(target.interpreter())
},

View File

@ -3830,14 +3830,14 @@ fn add_update_git_reference_script() -> Result<()> {
})?;
uv_snapshot!(context.filters(), context.add().arg("--script=script.py").arg("https://github.com/astral-test/uv-public-pypackage.git"),
@r###"
success: true
exit_code: 0
----- stdout -----
@r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Updated `script.py`
"###
----- stderr -----
Resolved 1 package in [TIME]
"
);
let script_content = context.read("script.py");
@ -3863,14 +3863,14 @@ fn add_update_git_reference_script() -> Result<()> {
});
uv_snapshot!(context.filters(), context.add().arg("--script=script.py").arg("uv-public-pypackage").arg("--branch=test-branch"),
@r###"
success: true
exit_code: 0
----- stdout -----
@r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Updated `script.py`
"###
----- stderr -----
Resolved 1 package in [TIME]
"
);
let script_content = context.read("script.py");
@ -6795,14 +6795,14 @@ fn add_script() -> Result<()> {
pprint([(k, v["title"]) for k, v in data.items()][:10])
"#})?;
uv_snapshot!(context.filters(), context.add().arg("anyio").arg("--script").arg("script.py"), @r###"
uv_snapshot!(context.filters(), context.add().arg("anyio").arg("--script").arg("script.py"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Updated `script.py`
"###);
Resolved 11 packages in [TIME]
");
let script_content = context.read("script.py");
@ -6810,11 +6810,11 @@ fn add_script() -> Result<()> {
filters => context.filters(),
}, {
assert_snapshot!(
script_content, @r###"
script_content, @r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "anyio",
# "anyio>=4.3.0",
# "requests<3",
# "rich",
# ]
@ -6826,6 +6826,52 @@ fn add_script() -> Result<()> {
resp = requests.get("https://peps.python.org/api/peps.json")
data = resp.json()
pprint([(k, v["title"]) for k, v in data.items()][:10])
"#
);
});
// Adding to a script without a lockfile shouldn't create a lockfile.
assert!(!context.temp_dir.join("script.py.lock").exists());
Ok(())
}
/// Test that `--bounds` is respected when adding to a script without a lockfile.
#[test]
fn add_script_bounds() -> Result<()> {
let context = TestContext::new("3.12");
let script = context.temp_dir.child("script.py");
script.write_str(indoc! {r#"
print("Hello, world!")
"#})?;
// Add `anyio` with `--bounds minor` to the script.
uv_snapshot!(context.filters(), context.add().arg("anyio").arg("--bounds").arg("minor").arg("--script").arg("script.py"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: The `bounds` option is in preview and may change in any future release. Pass `--preview-features add-bounds` to disable this warning.
Resolved 3 packages in [TIME]
");
let script_content = context.read("script.py");
// The script should have bounds with minor version constraint (e.g., `>=4.3.0,<4.4.0`).
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
script_content, @r###"
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "anyio>=4.3.0,<4.4.0",
# ]
# ///
print("Hello, world!")
"###
);
});
@ -6854,14 +6900,14 @@ fn add_script_relative_path() -> Result<()> {
print("Hello, world!")
"#})?;
uv_snapshot!(context.filters(), context.add().arg("./project").arg("--editable").arg("--script").arg("script.py"), @r###"
uv_snapshot!(context.filters(), context.add().arg("./project").arg("--editable").arg("--script").arg("script.py"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Updated `script.py`
"###);
Resolved 1 package in [TIME]
");
let script_content = context.read("script.py");
@ -7084,14 +7130,14 @@ fn add_script_trailing_comment_lines() -> Result<()> {
pprint([(k, v["title"]) for k, v in data.items()][:10])
"#})?;
uv_snapshot!(context.filters(), context.add().arg("anyio").arg("--script").arg("script.py"), @r###"
uv_snapshot!(context.filters(), context.add().arg("anyio").arg("--script").arg("script.py"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Updated `script.py`
"###);
Resolved 11 packages in [TIME]
");
let script_content = context.read("script.py");
@ -7099,11 +7145,11 @@ fn add_script_trailing_comment_lines() -> Result<()> {
filters => context.filters(),
}, {
assert_snapshot!(
script_content, @r##"
script_content, @r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "anyio",
# "anyio>=4.3.0",
# "requests<3",
# "rich",
# ]
@ -7117,7 +7163,7 @@ fn add_script_trailing_comment_lines() -> Result<()> {
resp = requests.get("https://peps.python.org/api/peps.json")
data = resp.json()
pprint([(k, v["title"]) for k, v in data.items()][:10])
"##
"#
);
});
@ -7142,14 +7188,14 @@ fn add_script_without_metadata_table() -> Result<()> {
pprint([(k, v["title"]) for k, v in data.items()][:10])
"#})?;
uv_snapshot!(context.filters(), context.add().args(["rich", "requests<3"]).arg("--script").arg("script.py"), @r###"
uv_snapshot!(context.filters(), context.add().args(["rich", "requests<3"]).arg("--script").arg("script.py"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Updated `script.py`
"###);
Resolved 9 packages in [TIME]
");
let script_content = context.read("script.py");
@ -7157,12 +7203,12 @@ fn add_script_without_metadata_table() -> Result<()> {
filters => context.filters(),
}, {
assert_snapshot!(
script_content, @r###"
script_content, @r#"
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "requests<3",
# "rich",
# "rich>=13.7.1",
# ]
# ///
import requests
@ -7171,7 +7217,7 @@ fn add_script_without_metadata_table() -> Result<()> {
resp = requests.get("https://peps.python.org/api/peps.json")
data = resp.json()
pprint([(k, v["title"]) for k, v in data.items()][:10])
"###
"#
);
});
Ok(())
@ -7193,14 +7239,14 @@ fn add_script_without_metadata_table_with_shebang() -> Result<()> {
pprint([(k, v["title"]) for k, v in data.items()][:10])
"#})?;
uv_snapshot!(context.filters(), context.add().args(["rich", "requests<3"]).arg("--script").arg("script.py"), @r###"
uv_snapshot!(context.filters(), context.add().args(["rich", "requests<3"]).arg("--script").arg("script.py"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Updated `script.py`
"###);
Resolved 9 packages in [TIME]
");
let script_content = context.read("script.py");
@ -7208,13 +7254,13 @@ fn add_script_without_metadata_table_with_shebang() -> Result<()> {
filters => context.filters(),
}, {
assert_snapshot!(
script_content, @r###"
script_content, @r#"
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "requests<3",
# "rich",
# "rich>=13.7.1",
# ]
# ///
import requests
@ -7223,7 +7269,7 @@ fn add_script_without_metadata_table_with_shebang() -> Result<()> {
resp = requests.get("https://peps.python.org/api/peps.json")
data = resp.json()
pprint([(k, v["title"]) for k, v in data.items()][:10])
"###
"#
);
});
Ok(())
@ -7249,14 +7295,14 @@ fn add_script_with_metadata_table_and_shebang() -> Result<()> {
pprint([(k, v["title"]) for k, v in data.items()][:10])
"#})?;
uv_snapshot!(context.filters(), context.add().args(["rich", "requests<3"]).arg("--script").arg("script.py"), @r###"
uv_snapshot!(context.filters(), context.add().args(["rich", "requests<3"]).arg("--script").arg("script.py"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Updated `script.py`
"###);
Resolved 9 packages in [TIME]
");
let script_content = context.read("script.py");
@ -7264,13 +7310,13 @@ fn add_script_with_metadata_table_and_shebang() -> Result<()> {
filters => context.filters(),
}, {
assert_snapshot!(
script_content, @r###"
script_content, @r#"
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "requests<3",
# "rich",
# "rich>=13.7.1",
# ]
# ///
import requests
@ -7279,7 +7325,7 @@ fn add_script_with_metadata_table_and_shebang() -> Result<()> {
resp = requests.get("https://peps.python.org/api/peps.json")
data = resp.json()
pprint([(k, v["title"]) for k, v in data.items()][:10])
"###
"#
);
});
Ok(())
@ -7301,14 +7347,14 @@ fn add_script_without_metadata_table_with_docstring() -> Result<()> {
pprint([(k, v["title"]) for k, v in data.items()][:10])
"#})?;
uv_snapshot!(context.filters(), context.add().args(["rich", "requests<3"]).arg("--script").arg("script.py"), @r###"
uv_snapshot!(context.filters(), context.add().args(["rich", "requests<3"]).arg("--script").arg("script.py"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Updated `script.py`
"###);
Resolved 9 packages in [TIME]
");
let script_content = context.read("script.py");
@ -7316,12 +7362,12 @@ fn add_script_without_metadata_table_with_docstring() -> Result<()> {
filters => context.filters(),
}, {
assert_snapshot!(
script_content, @r###"
script_content, @r#"
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "requests<3",
# "rich",
# "rich>=13.7.1",
# ]
# ///
"""This is a script."""
@ -7331,7 +7377,7 @@ fn add_script_without_metadata_table_with_docstring() -> Result<()> {
resp = requests.get("https://peps.python.org/api/peps.json")
data = resp.json()
pprint([(k, v["title"]) for k, v in data.items()][:10])
"###
"#
);
});
Ok(())
@ -7357,14 +7403,14 @@ fn add_extensionless_script() -> Result<()> {
pprint([(k, v["title"]) for k, v in data.items()][:10])
"#})?;
uv_snapshot!(context.filters(), context.add().args(["rich", "requests<3"]).arg("--script").arg("script"), @r###"
uv_snapshot!(context.filters(), context.add().args(["rich", "requests<3"]).arg("--script").arg("script"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Updated `script`
"###);
Resolved 9 packages in [TIME]
");
let script_content = context.read("script");
@ -7372,13 +7418,13 @@ fn add_extensionless_script() -> Result<()> {
filters => context.filters(),
}, {
assert_snapshot!(
script_content, @r###"
script_content, @r#"
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "requests<3",
# "rich",
# "rich>=13.7.1",
# ]
# ///
import requests
@ -7387,7 +7433,7 @@ fn add_extensionless_script() -> Result<()> {
resp = requests.get("https://peps.python.org/api/peps.json")
data = resp.json()
pprint([(k, v["title"]) for k, v in data.items()][:10])
"###
"#
);
});
Ok(())
@ -8317,14 +8363,14 @@ fn add_git_to_script() -> Result<()> {
.arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage")
.arg("--tag=0.0.1")
.arg("--script")
.arg("script.py"), @r###"
.arg("script.py"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Updated `script.py`
"###);
Resolved 4 packages in [TIME]
");
let script_content = context.read("script.py");