mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-14 12:29:04 +00:00
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:
parent
49d790a1f4
commit
c124bd250b
4 changed files with 229 additions and 23 deletions
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<()> {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue