From f08322b931d436eb54943c4c2b2791854ee27f9d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 10 Aug 2024 20:39:18 -0400 Subject: [PATCH] Tweaks --- crates/uv-cli/src/lib.rs | 20 ++- crates/uv-scripts/src/lib.rs | 324 +++++++++++++++++------------------ crates/uv/src/settings.rs | 12 +- crates/uv/tests/edit.rs | 8 - docs/reference/cli.md | 8 +- 5 files changed, 189 insertions(+), 183 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 77360190fd2d..20985b582828 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2450,6 +2450,16 @@ pub struct AddArgs { #[arg(long, conflicts_with = "isolated")] pub package: Option, + /// Add the dependency to the specified Python script, rather than to a project. + /// + /// If provided, uv will add the dependency to the script's inline metadata + /// table, in adhere with PEP 723. If no such inline metadata table is present, + /// a new one will be created and added to the script. When executed via `uv run`, + /// uv will create a temporary environment for the script with all inline + /// dependencies installed. + #[arg(long)] + pub script: Option, + /// The Python interpreter to use for resolving and syncing. /// /// See `uv help python` for details on Python discovery and supported @@ -2462,10 +2472,6 @@ pub struct AddArgs { help_heading = "Python options" )] pub python: Option, - - /// Specifies the Python script where the dependency will be added. - #[arg(long)] - pub script: Option, } #[derive(Args)] @@ -2513,9 +2519,13 @@ pub struct RemoveArgs { #[arg(long, conflicts_with = "isolated")] pub package: Option, - /// Specifies the Python script where the dependency will be removed. + /// Remove the dependency from the specified Python script, rather than from a project. + /// + /// If provided, uv will remove the dependency from the script's inline metadata + /// table, in adhere with PEP 723. #[arg(long)] pub script: Option, + /// The Python interpreter to use for resolving and syncing. /// /// See `uv help python` for details on Python discovery and supported diff --git a/crates/uv-scripts/src/lib.rs b/crates/uv-scripts/src/lib.rs index 8af02dc64e08..9cdd12c84f59 100644 --- a/crates/uv-scripts/src/lib.rs +++ b/crates/uv-scripts/src/lib.rs @@ -19,11 +19,12 @@ static FINDER: LazyLock = LazyLock::new(|| Finder::new(b"# /// script")) /// A PEP 723 script, including its [`Pep723Metadata`]. #[derive(Debug)] pub struct Pep723Script { + /// The path to the Python script. pub path: PathBuf, + /// The parsed [`Pep723Metadata`] table from the script. pub metadata: Pep723Metadata, /// The content of the script after the metadata table. pub raw: String, - /// The content of the script before the metadata table. pub prelude: String, } @@ -40,18 +41,18 @@ impl Pep723Script { }; // Extract the `script` tag. - let Some((prelude, metadata, raw)) = extract_script_tag(&contents)? else { + let Some(script_tag) = ScriptTag::parse(&contents)? else { return Ok(None); }; // Parse the metadata. - let metadata = Pep723Metadata::from_str(&metadata)?; + let metadata = Pep723Metadata::from_str(&script_tag.metadata)?; Ok(Some(Self { path: file.as_ref().to_path_buf(), metadata, - raw, - prelude, + raw: script_tag.script, + prelude: script_tag.prelude, })) } @@ -75,16 +76,16 @@ impl Pep723Script { requires_python = requires_python, }; - let (raw, prelude) = extract_shebang(&contents)?; + let (prelude, raw) = extract_shebang(&contents)?; // Parse the metadata. let metadata = Pep723Metadata::from_str(&default_metadata)?; Ok(Self { path: file.as_ref().to_path_buf(), + prelude: prelude.unwrap_or_default(), metadata, raw, - prelude: prelude.unwrap_or(String::new()), }) } @@ -114,7 +115,7 @@ impl Pep723Script { #[serde(rename_all = "kebab-case")] pub struct Pep723Metadata { pub dependencies: Option>>, - pub requires_python: Option, + pub requires_python: Option, pub tool: Option, /// The raw unserialized document. #[serde(skip)] @@ -123,6 +124,7 @@ pub struct Pep723Metadata { impl FromStr for Pep723Metadata { type Err = Pep723Error; + /// Parse `Pep723Metadata` from a raw TOML string. fn from_str(raw: &str) -> Result { let metadata = toml::from_str(raw)?; @@ -159,141 +161,152 @@ pub enum Pep723Error { Toml(#[from] toml::de::Error), } -/// Given the contents of a Python file, extract the `script` metadata block with leading comment -/// hashes removed, any preceding shebang or content (prelude), and the remaining Python script code. -/// -/// The function returns a tuple where: -/// - The first element is the preceding content, which may include a shebang or other lines before the `script` metadata block. -/// - The second element is the extracted metadata as a string with comment hashes removed. -/// - The third element is the remaining Python code of the script. -/// -/// Given the following input string representing the contents of a Python script: -/// -/// ```python -/// #!/usr/bin/env python3 -/// # /// script -/// # requires-python = '>=3.11' -/// # dependencies = [ -/// # 'requests<3', -/// # 'rich', -/// # ] -/// # /// -/// -/// import requests -/// -/// print("Hello, World!") -/// ``` -/// -/// This function would return: -/// -/// ( -/// "#!/usr/bin/env python3\n", -/// "requires-python = '>=3.11'\ndependencies = [\n 'requests<3',\n 'rich',\n]", -/// "import requests\n\nprint(\"Hello, World!\")\n" -/// ) -/// -/// See: -fn extract_script_tag(contents: &[u8]) -> Result, Pep723Error> { - // Identify the opening pragma. - let Some(index) = FINDER.find(contents) else { - return Ok(None); - }; - - // The opening pragma must be the first line, or immediately preceded by a newline. - if !(index == 0 || matches!(contents[index - 1], b'\r' | b'\n')) { - return Ok(None); - } +#[derive(Debug, Clone, Eq, PartialEq)] +struct ScriptTag { + /// The content of the script before the metadata block. + prelude: String, + /// The metadata block. + metadata: String, + /// The content of the script after the metadata block. + script: String, +} - // Decode as UTF-8. - let prelude = if index != 0 { - std::str::from_utf8(&contents[..index])? - } else { - "" - } - .to_string(); - let contents = &contents[index..]; - let contents = std::str::from_utf8(contents)?; +impl ScriptTag { + /// Given the contents of a Python file, extract the `script` metadata block with leading + /// comment hashes removed, any preceding shebang or content (prelude), and the remaining Python + /// script. + /// + /// Given the following input string representing the contents of a Python script: + /// + /// ```python + /// #!/usr/bin/env python3 + /// # /// script + /// # requires-python = '>=3.11' + /// # dependencies = [ + /// # 'requests<3', + /// # 'rich', + /// # ] + /// # /// + /// + /// import requests + /// + /// print("Hello, World!") + /// ``` + /// + /// This function would return: + /// + /// - Preamble: `#!/usr/bin/env python3\n` + /// - Metadata: `requires-python = '>=3.11'\ndependencies = [\n 'requests<3',\n 'rich',\n]` + /// - Script: `import requests\n\nprint("Hello, World!")\n` + /// + /// See: + fn parse(contents: &[u8]) -> Result, Pep723Error> { + // Identify the opening pragma. + let Some(index) = FINDER.find(contents) else { + return Ok(None); + }; - let mut lines = contents.lines(); + // The opening pragma must be the first line, or immediately preceded by a newline. + if !(index == 0 || matches!(contents[index - 1], b'\r' | b'\n')) { + return Ok(None); + } - // Ensure that the first line is exactly `# /// script`. - if !lines.next().is_some_and(|line| line == "# /// script") { - return Ok(None); - } + // Decode as UTF-8. + let prelude = if index != 0 { + std::str::from_utf8(&contents[..index])? + } else { + "" + } + .to_string(); + let contents = &contents[index..]; + let contents = std::str::from_utf8(contents)?; - // > Every line between these two lines (# /// TYPE and # ///) MUST be a comment starting - // > with #. If there are characters after the # then the first character MUST be a space. The - // > embedded content is formed by taking away the first two characters of each line if the - // > second character is a space, otherwise just the first character (which means the line - // > consists of only a single #). - let mut toml = vec![]; - - let mut python_script = vec![]; - - while let Some(line) = lines.next() { - // Remove the leading `#`. - let Some(line) = line.strip_prefix('#') else { - python_script.push(line); - python_script.extend(lines); - break; - }; + let mut lines = contents.lines(); - // If the line is empty, continue. - if line.is_empty() { - toml.push(""); - continue; + // Ensure that the first line is exactly `# /// script`. + if !lines.next().is_some_and(|line| line == "# /// script") { + return Ok(None); } - // Otherwise, the line _must_ start with ` `. - let Some(line) = line.strip_prefix(' ') else { - python_script.push(line); - python_script.extend(lines); - break; + // > Every line between these two lines (# /// TYPE and # ///) MUST be a comment starting + // > with #. If there are characters after the # then the first character MUST be a space. The + // > embedded content is formed by taking away the first two characters of each line if the + // > second character is a space, otherwise just the first character (which means the line + // > consists of only a single #). + let mut toml = vec![]; + + let mut python_script = vec![]; + + while let Some(line) = lines.next() { + // Remove the leading `#`. + let Some(line) = line.strip_prefix('#') else { + python_script.push(line); + python_script.extend(lines); + break; + }; + + // If the line is empty, continue. + if line.is_empty() { + toml.push(""); + continue; + } + + // Otherwise, the line _must_ start with ` `. + let Some(line) = line.strip_prefix(' ') else { + python_script.push(line); + python_script.extend(lines); + break; + }; + + toml.push(line); + } + // Find the closing `# ///`. The precedence is such that we need to identify the _last_ such + // line. + // + // For example, given: + // ```python + // # /// script + // # + // # /// + // # + // # /// + // ``` + // + // The latter `///` is the closing pragma + let Some(index) = toml.iter().rev().position(|line| *line == "///") else { + return Ok(None); }; + let index = toml.len() - index; + + // Discard any lines after the closing `# ///`. + // + // For example, given: + // ```python + // # /// script + // # + // # /// + // # + // # + // ``` + // + // We need to discard the last two lines. + toml.truncate(index - 1); + + // Join the lines into a single string. + let metadata = toml.join("\n") + "\n"; + let script = python_script.join("\n") + "\n"; - toml.push(line); + Ok(Some(Self { + prelude, + metadata, + script, + })) } - // Find the closing `# ///`. The precedence is such that we need to identify the _last_ such - // line. - // - // For example, given: - // ```python - // # /// script - // # - // # /// - // # - // # /// - // ``` - // - // The latter `///` is the closing pragma - let Some(index) = toml.iter().rev().position(|line| *line == "///") else { - return Ok(None); - }; - let index = toml.len() - index; - - // Discard any lines after the closing `# ///`. - // - // For example, given: - // ```python - // # /// script - // # - // # /// - // # - // # - // ``` - // - // We need to discard the last two lines. - toml.truncate(index - 1); - - // Join the lines into a single string. - let toml = toml.join("\n") + "\n"; - let python_script = python_script.join("\n") + "\n"; - - Ok(Some((prelude, toml, python_script))) } -/// Extracts the shebang line from the given file contents and returns it along with the remaining content. -fn extract_shebang(contents: &[u8]) -> Result<(String, Option), Pep723Error> { +/// Extracts the shebang line from the given file contents and returns it along with the remaining +/// content. +fn extract_shebang(contents: &[u8]) -> Result<(Option, String), Pep723Error> { let contents = std::str::from_utf8(contents)?; let mut lines = contents.lines(); @@ -303,11 +316,11 @@ fn extract_shebang(contents: &[u8]) -> Result<(String, Option), Pep723Er if first_line.starts_with("#!") { let shebang = first_line.to_string(); let remaining_content: String = lines.collect::>().join("\n"); - return Ok((remaining_content, Some(shebang))); + return Ok((Some(shebang), remaining_content)); } } - Ok((contents.to_string(), None)) + Ok((None, contents.to_string())) } /// Formats the provided metadata by prefixing each line with `#` and wrapping it with script markers. @@ -333,7 +346,7 @@ fn serialize_metadata(metadata: &str) -> String { #[cfg(test)] mod tests { - use crate::serialize_metadata; + use crate::{serialize_metadata, ScriptTag}; #[test] fn missing_space() { @@ -343,10 +356,7 @@ mod tests { # /// "}; - assert_eq!( - super::extract_script_tag(contents.as_bytes()).unwrap(), - None - ); + assert_eq!(ScriptTag::parse(contents.as_bytes()).unwrap(), None); } #[test] @@ -360,10 +370,7 @@ mod tests { # ] "}; - assert_eq!( - super::extract_script_tag(contents.as_bytes()).unwrap(), - None - ); + assert_eq!(ScriptTag::parse(contents.as_bytes()).unwrap(), None); } #[test] @@ -380,10 +387,7 @@ mod tests { # "}; - assert_eq!( - super::extract_script_tag(contents.as_bytes()).unwrap(), - None - ); + assert_eq!(ScriptTag::parse(contents.as_bytes()).unwrap(), None); } #[test] @@ -421,13 +425,11 @@ mod tests { data = resp.json() "}; - let actual = super::extract_script_tag(contents.as_bytes()) - .unwrap() - .unwrap(); + let actual = ScriptTag::parse(contents.as_bytes()).unwrap().unwrap(); - assert_eq!(actual.0, String::new()); - assert_eq!(actual.1, expected_metadata); - assert_eq!(actual.2, expected_data); + assert_eq!(actual.prelude, String::new()); + assert_eq!(actual.metadata, expected_metadata); + assert_eq!(actual.script, expected_data); } #[test] @@ -466,13 +468,11 @@ mod tests { data = resp.json() "}; - let actual = super::extract_script_tag(contents.as_bytes()) - .unwrap() - .unwrap(); + let actual = ScriptTag::parse(contents.as_bytes()).unwrap().unwrap(); - assert_eq!(actual.0, "#!/usr/bin/env python3\n".to_string()); - assert_eq!(actual.1, expected_metadata); - assert_eq!(actual.2, expected_data); + assert_eq!(actual.prelude, "#!/usr/bin/env python3\n".to_string()); + assert_eq!(actual.metadata, expected_metadata); + assert_eq!(actual.script, expected_data); } #[test] fn embedded_comment() { @@ -498,10 +498,10 @@ mod tests { ''' "}; - let actual = super::extract_script_tag(contents.as_bytes()) + let actual = ScriptTag::parse(contents.as_bytes()) .unwrap() .unwrap() - .1; + .metadata; assert_eq!(actual, expected); } @@ -528,10 +528,10 @@ mod tests { ] "}; - let actual = super::extract_script_tag(contents.as_bytes()) + let actual = ScriptTag::parse(contents.as_bytes()) .unwrap() .unwrap() - .1; + .metadata; assert_eq!(actual, expected); } diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 8e2d3856cc8e..10da1ee3783f 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -703,10 +703,10 @@ pub(crate) struct AddSettings { pub(crate) tag: Option, pub(crate) branch: Option, pub(crate) package: Option, + pub(crate) script: Option, pub(crate) python: Option, pub(crate) refresh: Refresh, pub(crate) settings: ResolverInstallerSettings, - pub(crate) script: Option, } impl AddSettings { @@ -731,8 +731,8 @@ impl AddSettings { build, refresh, package, - python, script, + python, } = args; let requirements = requirements @@ -759,6 +759,7 @@ impl AddSettings { tag, branch, package, + script, python, editable: flag(editable, no_editable), extras: extra.unwrap_or_default(), @@ -767,7 +768,6 @@ impl AddSettings { resolver_installer_options(installer, build), filesystem, ), - script, } } } @@ -782,10 +782,10 @@ pub(crate) struct RemoveSettings { pub(crate) packages: Vec, pub(crate) dependency_type: DependencyType, pub(crate) package: Option, + pub(crate) script: Option, pub(crate) python: Option, pub(crate) refresh: Refresh, pub(crate) settings: ResolverInstallerSettings, - pub(crate) script: Option, } impl RemoveSettings { @@ -803,8 +803,8 @@ impl RemoveSettings { build, refresh, package, - python, script, + python, } = args; let dependency_type = if let Some(group) = optional { @@ -822,13 +822,13 @@ impl RemoveSettings { packages, dependency_type, package, + script, python, refresh: Refresh::from(refresh), settings: ResolverInstallerSettings::combine( resolver_installer_options(installer, build), filesystem, ), - script, } } } diff --git a/crates/uv/tests/edit.rs b/crates/uv/tests/edit.rs index 0e9e3d1923d0..02bd97ddaa09 100644 --- a/crates/uv/tests/edit.rs +++ b/crates/uv/tests/edit.rs @@ -2928,7 +2928,6 @@ fn add_script() -> Result<()> { # ] # /// - import requests from rich.pretty import pprint @@ -2962,7 +2961,6 @@ fn add_script() -> Result<()> { # ] # /// - import requests from rich.pretty import pprint @@ -3093,7 +3091,6 @@ fn remove_script() -> Result<()> { # ] # /// - import requests from rich.pretty import pprint @@ -3126,7 +3123,6 @@ fn remove_script() -> Result<()> { # ] # /// - import requests from rich.pretty import pprint @@ -3153,7 +3149,6 @@ fn remove_last_dep_script() -> Result<()> { # ] # /// - import requests from rich.pretty import pprint @@ -3183,7 +3178,6 @@ fn remove_last_dep_script() -> Result<()> { # dependencies = [] # /// - import requests from rich.pretty import pprint @@ -3211,7 +3205,6 @@ fn add_git_to_script() -> Result<()> { # ] # /// - import requests from rich.pretty import pprint @@ -3265,7 +3258,6 @@ fn add_git_to_script() -> Result<()> { # uv-public-pypackage = { git = "https://github.com/astral-test/uv-public-pypackage", tag = "0.0.1" } # /// - import requests from rich.pretty import pprint diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 550631310d5f..d7387221c3ec 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -711,7 +711,9 @@ uv add [OPTIONS] ...
--rev rev

Commit to use when adding a dependency from Git

-
--script script

Specifies the Python script where the dependency will be added

+
--script script

Add the dependency to the specified Python script, rather than to a project.

+ +

If provided, uv will add the dependency to the script’s inline metadata table, in adhere with PEP 723. If no such inline metadata table is present, a new one will be created and added to the script. When executed via uv run, uv will create a temporary environment for the script with all inline dependencies installed.

--tag tag

Tag to use when adding a dependency from Git

@@ -969,7 +971,9 @@ uv remove [OPTIONS] ...
  • lowest-direct: Resolve the lowest compatible version of any direct dependencies, and the highest compatible version of any transitive dependencies
  • -
    --script script

    Specifies the Python script where the dependency will be removed

    +
    --script script

    Remove the dependency from the specified Python script, rather than from a project.

    + +

    If provided, uv will remove the dependency from the script’s inline metadata table, in adhere with PEP 723.

    --upgrade, -U

    Allow package upgrades, ignoring pinned versions in any existing output file. Implies --refresh