Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/uv-scripts/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
23 changes: 19 additions & 4 deletions crates/uv-scripts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down Expand Up @@ -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}
Expand Down
8 changes: 4 additions & 4 deletions crates/uv-warnings/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub fn disable() {
/// Warn a user, if warnings are enabled.
#[macro_export]
macro_rules! warn_user {
($($arg:tt)*) => {
($($arg:tt)*) => {{
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without the double curlies here, putting two warn_user! invocations back-to-back doesn't compile. My instinct was that I wanted the "warning:" prefix on both lines above, but whether or not we stick with that, this version of these macros is probably more correct.

use $crate::anstream::eprintln;
use $crate::owo_colors::OwoColorize;

Expand All @@ -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);
Expand All @@ -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;

Expand All @@ -54,5 +54,5 @@ macro_rules! warn_user_once {
}
}
}
};
}};
}
59 changes: 59 additions & 0 deletions crates/uv/tests/it/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!\")";
Comment thread
oconnor663 marked this conversation as resolved.
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<()> {
Expand Down
2 changes: 2 additions & 0 deletions docs/guides/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
Loading