Skip to content

Commit

Permalink
Support extras in @ requests for tools
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Feb 8, 2025
1 parent 25e7209 commit 1d215e2
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 28 deletions.
9 changes: 5 additions & 4 deletions crates/uv/src/commands/tool/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,15 @@ pub(crate) async fn install(
.unwrap()
}
// Ex) `[email protected]`
Target::Version(name, ref version) | Target::FromVersion(_, name, ref version) => {
Target::Version(name, ref extras, ref version)
| Target::FromVersion(_, name, ref extras, ref version) => {
if editable {
bail!("`--editable` is only supported for local packages");
}

Requirement {
name: PackageName::from_str(name)?,
extras: vec![],
extras: extras.clone(),
groups: vec![],
marker: MarkerTree::default(),
source: RequirementSource::Registry {
Expand All @@ -154,14 +155,14 @@ pub(crate) async fn install(
}
}
// Ex) `ruff@latest`
Target::Latest(name) | Target::FromLatest(_, name) => {
Target::Latest(name, ref extras) | Target::FromLatest(_, name, ref extras) => {
if editable {
bail!("`--editable` is only supported for local packages");
}

Requirement {
name: PackageName::from_str(name)?,
extras: vec![],
extras: extras.clone(),
groups: vec![],
marker: MarkerTree::default(),
source: RequirementSource::Registry {
Expand Down
83 changes: 65 additions & 18 deletions crates/uv/src/commands/tool/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::str::FromStr;

use tracing::debug;

use uv_normalize::PackageName;
use uv_normalize::{ExtraName, PackageName};
use uv_pep440::Version;

mod common;
Expand All @@ -19,15 +19,15 @@ pub(crate) enum Target<'a> {
/// e.g., `ruff`
Unspecified(&'a str),
/// e.g., `[email protected]`
Version(&'a str, Version),
Version(&'a str, Vec<ExtraName>, Version),
/// e.g., `ruff@latest`
Latest(&'a str),
Latest(&'a str, Vec<ExtraName>),
/// e.g., `ruff --from ruff>=0.6.0`
From(&'a str, &'a str),
/// e.g., `ruff --from [email protected]`
FromVersion(&'a str, &'a str, Version),
FromVersion(&'a str, &'a str, Vec<ExtraName>, Version),
/// e.g., `ruff --from ruff@latest`
FromLatest(&'a str, &'a str),
FromLatest(&'a str, &'a str, Vec<ExtraName>),
}

impl<'a> Target<'a> {
Expand All @@ -45,19 +45,43 @@ impl<'a> Target<'a> {
return Self::From(target, from);
}

// Split into name and extras (e.g., `flask[dotenv]`).
let (name, extras) = match name.split_once('[') {
Some((name, extras)) => {
let Some(extras) = extras.strip_suffix(']') else {
// e.g., ignore `flask[dotenv`.
debug!("Ignoring invalid extras in `--from`");
return Self::From(target, from);
};
(name, extras)
}
None => (name, ""),
};

// e.g., ignore `git+https://github.com/astral-sh/ruff.git@main`
if PackageName::from_str(name).is_err() {
debug!("Ignoring non-package name `{name}` in `--from`");
return Self::From(target, from);
}

// e.g., ignore `ruff[1.0.0]` or any other invalid extra.
let Ok(extras) = extras
.split(',')
.map(str::trim)
.map(ExtraName::from_str)
.collect::<Result<Vec<_>, _>>()
else {
debug!("Ignoring invalid extras `{extras}` in `--from`");
return Self::From(target, from);
};

match version {
// e.g., `ruff@latest`
"latest" => return Self::FromLatest(target, name),
"latest" => return Self::FromLatest(target, name, extras),
// e.g., `[email protected]`
version => {
if let Ok(version) = Version::from_str(version) {
return Self::FromVersion(target, name, version);
return Self::FromVersion(target, name, extras, version);
}
}
};
Expand All @@ -78,19 +102,42 @@ impl<'a> Target<'a> {
return Self::Unspecified(target);
}

// Split into name and extras (e.g., `flask[dotenv]`).
let (name, extras) = match name.split_once('[') {
Some((name, extras)) => {
let Some(extras) = extras.strip_suffix(']') else {
// e.g., ignore `flask[dotenv`.
return Self::Unspecified(name);
};
(name, extras)
}
None => (name, ""),
};

// e.g., ignore `git+https://github.com/astral-sh/ruff.git@main`
if PackageName::from_str(name).is_err() {
debug!("Ignoring non-package name `{name}` in command");
return Self::Unspecified(target);
}

// e.g., ignore `ruff[1.0.0]` or any other invalid extra.
let Ok(extras) = extras
.split(',')
.map(str::trim)
.map(ExtraName::from_str)
.collect::<Result<Vec<_>, _>>()
else {
debug!("Ignoring invalid extras `{extras}` in command");
return Self::Unspecified(target);
};

match version {
// e.g., `ruff@latest`
"latest" => return Self::Latest(name),
"latest" => return Self::Latest(name, extras),
// e.g., `[email protected]`
version => {
if let Ok(version) = Version::from_str(version) {
return Self::Version(name, version);
return Self::Version(name, extras, version);
}
}
};
Expand All @@ -104,10 +151,10 @@ impl<'a> Target<'a> {
pub(crate) fn executable(&self) -> &str {
match self {
Self::Unspecified(name) => name,
Self::Version(name, _) => name,
Self::Latest(name) => name,
Self::FromVersion(name, _, _) => name,
Self::FromLatest(name, _) => name,
Self::Version(name, _, _) => name,
Self::Latest(name, _) => name,
Self::FromVersion(name, _, _, _) => name,
Self::FromLatest(name, _, _) => name,
Self::From(name, _) => name,
}
}
Expand All @@ -116,17 +163,17 @@ impl<'a> Target<'a> {
pub(crate) fn is_python(&self) -> bool {
let name = match self {
Self::Unspecified(name) => name,
Self::Version(name, _) => name,
Self::Latest(name) => name,
Self::FromVersion(_, name, _) => name,
Self::FromLatest(_, name) => name,
Self::Version(name, _, _) => name,
Self::Latest(name, _) => name,
Self::FromVersion(_, name, _, _) => name,
Self::FromLatest(_, name, _) => name,
Self::From(_, name) => name,
};
name.eq_ignore_ascii_case("python") || cfg!(windows) && name.eq_ignore_ascii_case("pythonw")
}

/// Returns `true` if the target is `latest`.
fn is_latest(&self) -> bool {
matches!(self, Self::Latest(_) | Self::FromLatest(_, _))
matches!(self, Self::Latest(..) | Self::FromLatest(..))
}
}
13 changes: 7 additions & 6 deletions crates/uv/src/commands/tool/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -460,13 +460,13 @@ async fn get_or_create_environment(
let python_request = if target.is_python() {
let target_request = match target {
Target::Unspecified(_) => None,
Target::Version(_, version) | Target::FromVersion(_, _, version) => {
Target::Version(_, _, version) | Target::FromVersion(_, _, _, version) => {
Some(PythonRequest::Version(
VersionRequest::from_str(&version.to_string()).map_err(anyhow::Error::from)?,
))
}
// TODO(zanieb): Add `PythonRequest::Latest`
Target::Latest(_) | Target::FromLatest(_, _) => {
Target::Latest(_, _) | Target::FromLatest(_, _, _) => {
return Err(anyhow::anyhow!(
"Requesting the 'latest' Python version is not yet supported"
)
Expand Down Expand Up @@ -531,9 +531,10 @@ async fn get_or_create_environment(
origin: None,
},
// Ex) `[email protected]`
Target::Version(name, version) | Target::FromVersion(_, name, version) => Requirement {
Target::Version(name, extras, version)
| Target::FromVersion(_, name, extras, version) => Requirement {
name: PackageName::from_str(name)?,
extras: vec![],
extras: extras.clone(),
groups: vec![],
marker: MarkerTree::default(),
source: RequirementSource::Registry {
Expand All @@ -546,9 +547,9 @@ async fn get_or_create_environment(
origin: None,
},
// Ex) `ruff@latest`
Target::Latest(name) | Target::FromLatest(_, name) => Requirement {
Target::Latest(name, extras) | Target::FromLatest(_, name, extras) => Requirement {
name: PackageName::from_str(name)?,
extras: vec![],
extras: extras.clone(),
groups: vec![],
marker: MarkerTree::default(),
source: RequirementSource::Registry {
Expand Down
59 changes: 59 additions & 0 deletions crates/uv/tests/it/tool_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1398,6 +1398,65 @@ fn tool_run_latest() {
"###);
}

#[test]
fn tool_run_latest_extra() {
let context = TestContext::new("3.12").with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");

uv_snapshot!(context.filters(), context.tool_run()
.arg("flask[dotenv]@latest")
.arg("--version")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###"
success: true
exit_code: 0
----- stdout -----
Python 3.12.[X]
Flask 3.0.2
Werkzeug 3.0.1
----- stderr -----
Resolved 8 packages in [TIME]
Prepared 8 packages in [TIME]
Installed 8 packages in [TIME]
+ blinker==1.7.0
+ click==8.1.7
+ flask==3.0.2
+ itsdangerous==2.1.2
+ jinja2==3.1.3
+ markupsafe==2.1.5
+ python-dotenv==1.0.1
+ werkzeug==3.0.1
"###);

uv_snapshot!(context.filters(), context.tool_run()
.arg("flask[dotenv]@3.0.0")
.arg("--version")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @r###"
success: true
exit_code: 0
----- stdout -----
Python 3.12.[X]
Flask 3.0.0
Werkzeug 3.0.1
----- stderr -----
Resolved 8 packages in [TIME]
Prepared 1 package in [TIME]
Installed 8 packages in [TIME]
+ blinker==1.7.0
+ click==8.1.7
+ flask==3.0.0
+ itsdangerous==2.1.2
+ jinja2==3.1.3
+ markupsafe==2.1.5
+ python-dotenv==1.0.1
+ werkzeug==3.0.1
"###);
}

#[test]
fn tool_run_python() {
let context = TestContext::new("3.12").with_filtered_counts();
Expand Down

0 comments on commit 1d215e2

Please sign in to comment.