handle an existing shebang in uv init --script (#14141)
Some checks are pending
CI / lint (push) Waiting to run
CI / integration test | pypy on windows (push) Blocked by required conditions
CI / integration test | graalpy on ubuntu (push) Blocked by required conditions
CI / integration test | graalpy on windows (push) Blocked by required conditions
CI / integration test | pyodide on ubuntu (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / cargo clippy | ubuntu (push) Blocked by required conditions
CI / cargo clippy | windows (push) Blocked by required conditions
CI / cargo dev generate-all (push) Blocked by required conditions
CI / cargo shear (push) Waiting to run
CI / cargo test | ubuntu (push) Blocked by required conditions
CI / cargo test | macos (push) Blocked by required conditions
CI / cargo test | windows (push) Blocked by required conditions
CI / check windows trampoline | aarch64 (push) Blocked by required conditions
CI / check windows trampoline | i686 (push) Blocked by required conditions
CI / build binary | windows aarch64 (push) Blocked by required conditions
CI / check windows trampoline | x86_64 (push) Blocked by required conditions
CI / test windows trampoline | i686 (push) Blocked by required conditions
CI / test windows trampoline | x86_64 (push) Blocked by required conditions
CI / typos (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / check system | pyston (push) Blocked by required conditions
CI / build binary | linux libc (push) Blocked by required conditions
CI / build binary | linux musl (push) Blocked by required conditions
CI / build binary | macos aarch64 (push) Blocked by required conditions
CI / build binary | macos x86_64 (push) Blocked by required conditions
CI / build binary | windows x86_64 (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / build binary | freebsd (push) Blocked by required conditions
CI / ecosystem test | pydantic/pydantic-core (push) Blocked by required conditions
CI / ecosystem test | prefecthq/prefect (push) Blocked by required conditions
CI / ecosystem test | pallets/flask (push) Blocked by required conditions
CI / smoke test | linux (push) Blocked by required conditions
CI / check system | alpine (push) Blocked by required conditions
CI / smoke test | macos (push) Blocked by required conditions
CI / smoke test | windows x86_64 (push) Blocked by required conditions
CI / smoke test | windows aarch64 (push) Blocked by required conditions
CI / integration test | conda on ubuntu (push) Blocked by required conditions
CI / integration test | deadsnakes python3.9 on ubuntu (push) Blocked by required conditions
CI / integration test | free-threaded on windows (push) Blocked by required conditions
CI / integration test | pypy on ubuntu (push) Blocked by required conditions
CI / integration test | github actions (push) Blocked by required conditions
CI / integration test | free-threaded python on github actions (push) Blocked by required conditions
CI / integration test | determine publish changes (push) Blocked by required conditions
CI / integration test | registries (push) Blocked by required conditions
CI / integration test | uv publish (push) Blocked by required conditions
CI / integration test | uv_build (push) Blocked by required conditions
CI / check cache | ubuntu (push) Blocked by required conditions
CI / check cache | macos aarch64 (push) Blocked by required conditions
CI / check system | python on debian (push) Blocked by required conditions
CI / check system | python on fedora (push) Blocked by required conditions
CI / check system | python on ubuntu (push) Blocked by required conditions
CI / check system | python on rocky linux 8 (push) Blocked by required conditions
CI / check system | python on rocky linux 9 (push) Blocked by required conditions
CI / check system | graalpy on ubuntu (push) Blocked by required conditions
CI / check system | pypy on ubuntu (push) Blocked by required conditions
CI / check system | python on macos aarch64 (push) Blocked by required conditions
CI / check system | homebrew python on macos aarch64 (push) Blocked by required conditions
CI / check system | python on macos x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86 (push) Blocked by required conditions
CI / check system | python3.13 on windows x86-64 (push) Blocked by required conditions
CI / check system | x86-64 python3.13 on windows aarch64 (push) Blocked by required conditions
CI / check system | windows registry (push) Blocked by required conditions
CI / check system | python3.12 via chocolatey (push) Blocked by required conditions
CI / check system | python3.9 via pyenv (push) Blocked by required conditions
CI / check system | python3.13 (push) Blocked by required conditions
CI / check system | conda3.11 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.8 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.11 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.11 on windows x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on windows x86-64 (push) Blocked by required conditions
CI / check system | amazonlinux (push) Blocked by required conditions
CI / check system | embedded python3.10 on windows x86-64 (push) Blocked by required conditions
CI / benchmarks | walltime aarch64 linux (push) Blocked by required conditions
CI / benchmarks | instrumented (push) Blocked by required conditions

Closes https://github.com/astral-sh/uv/issues/14085.
This commit is contained in:
Jack O'Connor 2025-06-19 14:47:22 -07:00 committed by GitHub
parent c3e4b63806
commit cc8d5a9215
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 88 additions and 8 deletions

2
Cargo.lock generated
View file

@ -5742,6 +5742,7 @@ dependencies = [
"fs-err 3.1.1",
"indoc",
"memchr",
"regex",
"serde",
"thiserror 2.0.12",
"toml",
@ -5751,6 +5752,7 @@ dependencies = [
"uv-pypi-types",
"uv-redacted",
"uv-settings",
"uv-warnings",
"uv-workspace",
]

View file

@ -16,11 +16,13 @@ uv-pep508 = { workspace = true }
uv-pypi-types = { workspace = true }
uv-redacted = { workspace = true }
uv-settings = { workspace = true }
uv-warnings = { workspace = true }
uv-workspace = { workspace = true }
fs-err = { workspace = true, features = ["tokio"] }
indoc = { workspace = true }
memchr = { workspace = true }
regex = { workspace = true }
serde = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
toml = { workspace = true }

View file

@ -14,6 +14,7 @@ use uv_pep508::PackageName;
use uv_pypi_types::VerbatimParsedUrl;
use uv_redacted::DisplaySafeUrl;
use uv_settings::{GlobalOptions, ResolverInstallerOptions};
use uv_warnings::warn_user;
use uv_workspace::pyproject::Sources;
static FINDER: LazyLock<Finder> = LazyLock::new(|| Finder::new(b"# /// script"));
@ -238,11 +239,25 @@ impl Pep723Script {
let metadata = serialize_metadata(&default_metadata);
let script = if let Some(existing_contents) = existing_contents {
let (mut shebang, contents) = extract_shebang(&existing_contents)?;
if !shebang.is_empty() {
shebang.push_str("\n#\n");
// If the shebang doesn't contain `uv`, it's probably something like
// `#! /usr/bin/env python`, which isn't going to respect the inline metadata.
// Issue a warning for users who might not know that.
// TODO: There are a lot of mistakes we could consider detecting here, like
// `uv run` without `--script` when the file doesn't end in `.py`.
if !regex::Regex::new(r"\buv\b").unwrap().is_match(&shebang) {
warn_user!(
"If you execute {} directly, it might ignore its inline metadata.\nConsider replacing its shebang with: {}",
file.to_string_lossy().cyan(),
"#!/usr/bin/env -S uv run --script".cyan(),
);
}
}
indoc::formatdoc! {r"
{metadata}
{content}
",
content = String::from_utf8(existing_contents).map_err(|err| Pep723Error::Utf8(err.utf8_error()))?}
{shebang}{metadata}
{contents}" }
} else {
indoc::formatdoc! {r#"
{metadata}

View file

@ -24,7 +24,7 @@ pub fn disable() {
/// Warn a user, if warnings are enabled.
#[macro_export]
macro_rules! warn_user {
($($arg:tt)*) => {
($($arg:tt)*) => {{
use $crate::anstream::eprintln;
use $crate::owo_colors::OwoColorize;
@ -33,7 +33,7 @@ macro_rules! warn_user {
let formatted = message.bold();
eprintln!("{}{} {formatted}", "warning".yellow().bold(), ":".bold());
}
};
}};
}
pub static WARNINGS: LazyLock<Mutex<FxHashSet<String>>> = LazyLock::new(Mutex::default);
@ -42,7 +42,7 @@ pub static WARNINGS: LazyLock<Mutex<FxHashSet<String>>> = LazyLock::new(Mutex::d
/// message.
#[macro_export]
macro_rules! warn_user_once {
($($arg:tt)*) => {
($($arg:tt)*) => {{
use $crate::anstream::eprintln;
use $crate::owo_colors::OwoColorize;
@ -54,5 +54,5 @@ macro_rules! warn_user_once {
}
}
}
};
}};
}

View file

@ -929,6 +929,65 @@ fn init_script_file_conflicts() -> Result<()> {
Ok(())
}
// Init script should not trash an existing shebang.
#[test]
fn init_script_shebang() -> Result<()> {
let context = TestContext::new("3.12");
let script_path = context.temp_dir.child("script.py");
let contents = "#! /usr/bin/env python3\nprint(\"Hello, world!\")";
fs_err::write(&script_path, contents)?;
uv_snapshot!(context.filters(), context.init().arg("--script").arg("script.py"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: If you execute script.py directly, it might ignore its inline metadata.
Consider replacing its shebang with: #!/usr/bin/env -S uv run --script
Initialized script at `script.py`
");
let resulting_script = fs_err::read_to_string(&script_path)?;
assert_snapshot!(resulting_script, @r#"
#! /usr/bin/env python3
#
# /// script
# requires-python = ">=3.12"
# dependencies = []
# ///
print("Hello, world!")
"#
);
// If the shebang already contains `uv`, the result is the same, but we suppress the warning.
let contents = "#!/usr/bin/env -S uv run --script\nprint(\"Hello, world!\")";
fs_err::write(&script_path, contents)?;
uv_snapshot!(context.filters(), context.init().arg("--script").arg("script.py"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Initialized script at `script.py`
");
let resulting_script = fs_err::read_to_string(&script_path)?;
assert_snapshot!(resulting_script, @r#"
#!/usr/bin/env -S uv run --script
#
# /// script
# requires-python = ">=3.12"
# dependencies = []
# ///
print("Hello, world!")
"#
);
Ok(())
}
/// Run `uv init --lib` with an existing py.typed file
#[test]
fn init_py_typed_exists() -> Result<()> {

View file

@ -241,10 +241,12 @@ Declaration of dependencies is also supported in this context, for example:
```python title="example"
#!/usr/bin/env -S uv run --script
#
# /// script
# requires-python = ">=3.12"
# dependencies = ["httpx"]
# ///
import httpx
print(httpx.get("https://example.com"))