Respect existing PEP 723 script settings in uv add (#11716)

## Summary

As in other commands, we need to pick up the inline settings in a PEP
723 script in `uv add`. Right now, we ignore them entirely.
This commit is contained in:
Charlie Marsh 2025-02-22 14:02:31 -10:00 committed by GitHub
parent 49d790a1f4
commit c124bd250b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 229 additions and 23 deletions

View file

@ -3,7 +3,7 @@ use anyhow::Context;
use owo_colors::OwoColorize;
use std::borrow::Cow;
use std::io::stdout;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::time::Duration;
use std::{fmt::Display, fmt::Write, process::ExitCode};
@ -53,6 +53,7 @@ use uv_fs::{Simplified, CWD};
use uv_installer::compile_tree;
use uv_normalize::PackageName;
use uv_python::PythonEnvironment;
use uv_scripts::Pep723Script;
pub(crate) use venv::venv;
pub(crate) use version::version;
@ -290,3 +291,12 @@ pub(super) fn capitalize(s: &str) -> String {
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
}
}
/// A Python file that may or may not include an existing PEP 723 script tag.
#[derive(Debug)]
pub(crate) enum ScriptPath {
/// The Python file already includes a PEP 723 script tag.
Script(Pep723Script),
/// The Python file does not include a PEP 723 script tag.
Path(PathBuf),
}

View file

@ -1,7 +1,7 @@
use std::collections::hash_map::Entry;
use std::fmt::Write;
use std::io;
use std::path::{Path, PathBuf};
use std::path::Path;
use std::str::FromStr;
use std::sync::Arc;
@ -51,7 +51,7 @@ use crate::commands::project::{
ProjectInterpreter, ScriptInterpreter, UniversalState,
};
use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter};
use crate::commands::{diagnostics, project, ExitStatus};
use crate::commands::{diagnostics, project, ExitStatus, ScriptPath};
use crate::printer::Printer;
use crate::settings::{ResolverInstallerSettings, ResolverInstallerSettingsRef};
@ -76,7 +76,7 @@ pub(crate) async fn add(
python: Option<String>,
install_mirrors: PythonInstallMirrors,
settings: ResolverInstallerSettings,
script: Option<PathBuf>,
script: Option<ScriptPath>,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
installer_metadata: bool,
@ -136,23 +136,24 @@ pub(crate) async fn add(
// If we found a script, add to the existing metadata. Otherwise, create a new inline
// metadata tag.
let script = if let Some(script) = Pep723Script::read(&script).await? {
script
} else {
let requires_python = init_script_python_requirement(
python.as_deref(),
&install_mirrors,
project_dir,
false,
python_preference,
python_downloads,
no_config,
&client_builder,
cache,
&reporter,
)
.await?;
Pep723Script::init(&script, requires_python.specifiers()).await?
let script = match script {
ScriptPath::Script(script) => script,
ScriptPath::Path(path) => {
let requires_python = init_script_python_requirement(
python.as_deref(),
&install_mirrors,
project_dir,
false,
python_preference,
python_downloads,
no_config,
&client_builder,
cache,
&reporter,
)
.await?;
Pep723Script::init(&path, requires_python.specifiers()).await?
}
};
// Discover the interpreter.

View file

@ -33,7 +33,7 @@ use uv_static::EnvVars;
use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::{DiscoveryOptions, Workspace};
use crate::commands::{ExitStatus, RunCommand, ToolRunCommand};
use crate::commands::{ExitStatus, RunCommand, ScriptPath, ToolRunCommand};
use crate::printer::Printer;
use crate::settings::{
CacheSettings, GlobalSettings, PipCheckSettings, PipCompileSettings, PipFreezeSettings,
@ -193,6 +193,14 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
) => Pep723Metadata::parse(contents)?.map(Pep723Item::Stdin),
_ => None,
},
ProjectCommand::Add(uv_cli::AddArgs {
script: Some(script),
..
}) => match Pep723Script::read(&script).await {
Ok(Some(script)) => Some(Pep723Item::Script(script)),
Ok(None) => None,
Err(err) => return Err(err.into()),
},
ProjectCommand::Remove(uv_cli::RemoveArgs {
script: Some(script),
..
@ -1615,6 +1623,19 @@ async fn run_project(
.combine(Refresh::from(args.settings.upgrade.clone())),
);
// If the script already exists, use it; otherwise, propagate the file path and we'll
// initialize it later on.
let script = script
.map(|script| match script {
Pep723Item::Script(script) => script,
Pep723Item::Stdin(..) => unreachable!("`uv add` does not support stdin"),
Pep723Item::Remote(..) => {
unreachable!("`uv add` does not support remote files")
}
})
.map(ScriptPath::Script)
.or(args.script.map(ScriptPath::Path));
let requirements = args
.packages
.into_iter()
@ -1645,7 +1666,7 @@ async fn run_project(
args.python,
args.install_mirrors,
args.settings,
args.script,
script,
globals.python_preference,
globals.python_downloads,
globals.installer_metadata,

View file

@ -5301,6 +5301,180 @@ fn add_script_relative_path() -> Result<()> {
Ok(())
}
/// Respect inline settings when adding to a PEP 732 script.
#[test]
fn add_script_settings() -> Result<()> {
let context = TestContext::new("3.12");
let script = context.temp_dir.child("script.py");
script.write_str(indoc! {r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "requests>=2",
# "rich>=12",
# ]
#
# [tool.uv]
# resolution = "lowest-direct"
# ///
import requests
from rich.pretty import pprint
resp = requests.get("https://peps.python.org/api/peps.json")
data = resp.json()
pprint([(k, v["title"]) for k, v in data.items()][:10])
"#})?;
// Lock the script.
uv_snapshot!(context.filters(), context.lock().arg("--script").arg("script.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
"###);
// Add `anyio` to the script.
uv_snapshot!(context.filters(), context.add().arg("anyio>=3").arg("--script").arg("script.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 7 packages in [TIME]
"###);
let script_content = context.read("script.py");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
script_content, @r###"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "anyio>=3",
# "requests>=2",
# "rich>=12",
# ]
#
# [tool.uv]
# resolution = "lowest-direct"
# ///
import requests
from rich.pretty import pprint
resp = requests.get("https://peps.python.org/api/peps.json")
data = resp.json()
pprint([(k, v["title"]) for k, v in data.items()][:10])
"###
);
});
let lock = context.read("script.py.lock");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r###"
version = 1
revision = 1
requires-python = ">=3.11"
[options]
resolution-mode = "lowest-direct"
exclude-newer = "2024-03-25T00:00:00Z"
[manifest]
requirements = [
{ name = "anyio", specifier = ">=3" },
{ name = "requests", specifier = ">=2" },
{ name = "rich", specifier = ">=12" },
]
[[package]]
name = "anyio"
version = "3.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/99/0d/65165f99e5f4f3b4c43a5ed9db0fb7aa655f5a58f290727a30528a87eb45/anyio-3.0.0.tar.gz", hash = "sha256:b553598332c050af19f7d41f73a7790142f5bc3d5eb8bd82f5e515ec22019bd9", size = 116952 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/49/ebee263b69fe243bd1fd0a88bc6bb0f7732bf1794ba3273cb446351f9482/anyio-3.0.0-py3-none-any.whl", hash = "sha256:e71c3d9d72291d12056c0265d07c6bbedf92332f78573e278aeb116f24f30395", size = 72182 },
]
[[package]]
name = "commonmark"
version = "0.9.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/60/48/a60f593447e8f0894ebb7f6e6c1f25dafc5e89c5879fdc9360ae93ff83f0/commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60", size = 95764 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b1/92/dfd892312d822f36c55366118b95d914e5f16de11044a27cf10a7d71bbbf/commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9", size = 51068 },
]
[[package]]
name = "idna"
version = "3.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 },
]
[[package]]
name = "pygments"
version = "2.17.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/55/59/8bccf4157baf25e4aa5a0bb7fa3ba8600907de105ebc22b0c78cfbf6f565/pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367", size = 4827772 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/97/9c/372fef8377a6e340b1704768d20daaded98bf13282b5327beb2e2fe2c7ef/pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", size = 1179756 },
]
[[package]]
name = "requests"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8e/88/102742c48605aef8d39fa719d932c67783d789679628fa1433cb4b2c7a2a/requests-2.0.0.tar.gz", hash = "sha256:78536038f54cff6ade3be6863403146665b5a3923dd61108c98d8b64141f9d70", size = 362994 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/78/be2b4c440ea767336d8448fe671fe1d78ca499e49d77dac90f92191cca0e/requests-2.0.0-py2.py3-none-any.whl", hash = "sha256:2ef65639cb9600443f85451df487818c31f993ab288f313d29cc9db4f3cbe6ed", size = 391141 },
]
[[package]]
name = "rich"
version = "12.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "commonmark" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/88/fd/13ee53f8bdf538b64c8ed505a92d3bc1f2c0d1071173d65216af61e0e88b/rich-12.0.0.tar.gz", hash = "sha256:14bfd0507edc633e021b02c45cbf7ca22e33b513817627b8de3412f047a3e798", size = 206045 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/93/ac/463598448d578f1f9168eb17405b2faebb4e76123c986b2c4ab64d387d5e/rich-12.0.0-py3-none-any.whl", hash = "sha256:fdcd2f8d416e152bcf35c659987038d1ae5a7bd336e821ca7551858a4c7e38a9", size = 224015 },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
]
"###
);
});
Ok(())
}
/// Add to a script without an existing metadata table.
#[test]
fn add_script_without_metadata_table() -> Result<()> {