Skip to content

Commit cc8d5a9

Browse files
authored
handle an existing shebang in uv init --script (astral-sh#14141)
Closes astral-sh#14085.
1 parent c3e4b63 commit cc8d5a9

File tree

6 files changed

+88
-8
lines changed

6 files changed

+88
-8
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/uv-scripts/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ uv-pep508 = { workspace = true }
1616
uv-pypi-types = { workspace = true }
1717
uv-redacted = { workspace = true }
1818
uv-settings = { workspace = true }
19+
uv-warnings = { workspace = true }
1920
uv-workspace = { workspace = true }
2021

2122
fs-err = { workspace = true, features = ["tokio"] }
2223
indoc = { workspace = true }
2324
memchr = { workspace = true }
25+
regex = { workspace = true }
2426
serde = { workspace = true, features = ["derive"] }
2527
thiserror = { workspace = true }
2628
toml = { workspace = true }

crates/uv-scripts/src/lib.rs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use uv_pep508::PackageName;
1414
use uv_pypi_types::VerbatimParsedUrl;
1515
use uv_redacted::DisplaySafeUrl;
1616
use uv_settings::{GlobalOptions, ResolverInstallerOptions};
17+
use uv_warnings::warn_user;
1718
use uv_workspace::pyproject::Sources;
1819

1920
static FINDER: LazyLock<Finder> = LazyLock::new(|| Finder::new(b"# /// script"));
@@ -238,11 +239,25 @@ impl Pep723Script {
238239
let metadata = serialize_metadata(&default_metadata);
239240

240241
let script = if let Some(existing_contents) = existing_contents {
242+
let (mut shebang, contents) = extract_shebang(&existing_contents)?;
243+
if !shebang.is_empty() {
244+
shebang.push_str("\n#\n");
245+
// If the shebang doesn't contain `uv`, it's probably something like
246+
// `#! /usr/bin/env python`, which isn't going to respect the inline metadata.
247+
// Issue a warning for users who might not know that.
248+
// TODO: There are a lot of mistakes we could consider detecting here, like
249+
// `uv run` without `--script` when the file doesn't end in `.py`.
250+
if !regex::Regex::new(r"\buv\b").unwrap().is_match(&shebang) {
251+
warn_user!(
252+
"If you execute {} directly, it might ignore its inline metadata.\nConsider replacing its shebang with: {}",
253+
file.to_string_lossy().cyan(),
254+
"#!/usr/bin/env -S uv run --script".cyan(),
255+
);
256+
}
257+
}
241258
indoc::formatdoc! {r"
242-
{metadata}
243-
{content}
244-
",
245-
content = String::from_utf8(existing_contents).map_err(|err| Pep723Error::Utf8(err.utf8_error()))?}
259+
{shebang}{metadata}
260+
{contents}" }
246261
} else {
247262
indoc::formatdoc! {r#"
248263
{metadata}

crates/uv-warnings/src/lib.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ pub fn disable() {
2424
/// Warn a user, if warnings are enabled.
2525
#[macro_export]
2626
macro_rules! warn_user {
27-
($($arg:tt)*) => {
27+
($($arg:tt)*) => {{
2828
use $crate::anstream::eprintln;
2929
use $crate::owo_colors::OwoColorize;
3030

@@ -33,7 +33,7 @@ macro_rules! warn_user {
3333
let formatted = message.bold();
3434
eprintln!("{}{} {formatted}", "warning".yellow().bold(), ":".bold());
3535
}
36-
};
36+
}};
3737
}
3838

3939
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
4242
/// message.
4343
#[macro_export]
4444
macro_rules! warn_user_once {
45-
($($arg:tt)*) => {
45+
($($arg:tt)*) => {{
4646
use $crate::anstream::eprintln;
4747
use $crate::owo_colors::OwoColorize;
4848

@@ -54,5 +54,5 @@ macro_rules! warn_user_once {
5454
}
5555
}
5656
}
57-
};
57+
}};
5858
}

crates/uv/tests/it/init.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -929,6 +929,65 @@ fn init_script_file_conflicts() -> Result<()> {
929929
Ok(())
930930
}
931931

932+
// Init script should not trash an existing shebang.
933+
#[test]
934+
fn init_script_shebang() -> Result<()> {
935+
let context = TestContext::new("3.12");
936+
937+
let script_path = context.temp_dir.child("script.py");
938+
939+
let contents = "#! /usr/bin/env python3\nprint(\"Hello, world!\")";
940+
fs_err::write(&script_path, contents)?;
941+
uv_snapshot!(context.filters(), context.init().arg("--script").arg("script.py"), @r"
942+
success: true
943+
exit_code: 0
944+
----- stdout -----
945+
946+
----- stderr -----
947+
warning: If you execute script.py directly, it might ignore its inline metadata.
948+
Consider replacing its shebang with: #!/usr/bin/env -S uv run --script
949+
Initialized script at `script.py`
950+
");
951+
let resulting_script = fs_err::read_to_string(&script_path)?;
952+
assert_snapshot!(resulting_script, @r#"
953+
#! /usr/bin/env python3
954+
#
955+
# /// script
956+
# requires-python = ">=3.12"
957+
# dependencies = []
958+
# ///
959+
960+
print("Hello, world!")
961+
"#
962+
);
963+
964+
// If the shebang already contains `uv`, the result is the same, but we suppress the warning.
965+
let contents = "#!/usr/bin/env -S uv run --script\nprint(\"Hello, world!\")";
966+
fs_err::write(&script_path, contents)?;
967+
uv_snapshot!(context.filters(), context.init().arg("--script").arg("script.py"), @r"
968+
success: true
969+
exit_code: 0
970+
----- stdout -----
971+
972+
----- stderr -----
973+
Initialized script at `script.py`
974+
");
975+
let resulting_script = fs_err::read_to_string(&script_path)?;
976+
assert_snapshot!(resulting_script, @r#"
977+
#!/usr/bin/env -S uv run --script
978+
#
979+
# /// script
980+
# requires-python = ">=3.12"
981+
# dependencies = []
982+
# ///
983+
984+
print("Hello, world!")
985+
"#
986+
);
987+
988+
Ok(())
989+
}
990+
932991
/// Run `uv init --lib` with an existing py.typed file
933992
#[test]
934993
fn init_py_typed_exists() -> Result<()> {

docs/guides/scripts.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,10 +241,12 @@ Declaration of dependencies is also supported in this context, for example:
241241

242242
```python title="example"
243243
#!/usr/bin/env -S uv run --script
244+
#
244245
# /// script
245246
# requires-python = ">=3.12"
246247
# dependencies = ["httpx"]
247248
# ///
249+
248250
import httpx
249251

250252
print(httpx.get("https://example.com"))

0 commit comments

Comments
 (0)