mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 10:58:28 +00:00
Bring parity to uvx
and uv tool install
requests (#11345)
## Summary This PR refactors the whole `Target` abstraction, mostly to remove all the repeated `From*` variants and logic in favor of a higher-level struct that captures those details separately. In doing so, it also adds support to `uvx` for unnamed requirements, for parity with `uv tool install`. So, e.g., the following will show the `flask` version: ``` uvx git+https://github.com/pallets/flask --version ``` I think this makes sense conceptually since we already support arbitrary named requirements there.
This commit is contained in:
parent
136593a1bb
commit
12d34e680e
5 changed files with 360 additions and 327 deletions
|
@ -30,7 +30,7 @@ use crate::commands::project::{
|
|||
EnvironmentSpecification, PlatformState, ProjectError,
|
||||
};
|
||||
use crate::commands::tool::common::{install_executables, refine_interpreter, remove_entrypoints};
|
||||
use crate::commands::tool::Target;
|
||||
use crate::commands::tool::{Target, ToolRequest};
|
||||
use crate::commands::ExitStatus;
|
||||
use crate::commands::{diagnostics, reporters::PythonDownloadReporter};
|
||||
use crate::printer::Printer;
|
||||
|
@ -95,105 +95,40 @@ pub(crate) async fn install(
|
|||
.allow_insecure_host(allow_insecure_host.to_vec());
|
||||
|
||||
// Parse the input requirement.
|
||||
let target = Target::parse(&package, from.as_deref());
|
||||
let request = ToolRequest::parse(&package, from.as_deref());
|
||||
|
||||
// If the user passed, e.g., `ruff@latest`, refresh the cache.
|
||||
let cache = if target.is_latest() {
|
||||
let cache = if request.is_latest() {
|
||||
cache.with_refresh(Refresh::All(Timestamp::now()))
|
||||
} else {
|
||||
cache
|
||||
};
|
||||
|
||||
// Resolve the `--from` requirement.
|
||||
let from = match target {
|
||||
let from = match &request.target {
|
||||
// Ex) `ruff`
|
||||
Target::Unspecified(name) => {
|
||||
Target::Unspecified(from) => {
|
||||
let source = if editable {
|
||||
RequirementsSource::Editable(name.to_string())
|
||||
RequirementsSource::Editable((*from).to_string())
|
||||
} else {
|
||||
RequirementsSource::Package(name.to_string())
|
||||
RequirementsSource::Package((*from).to_string())
|
||||
};
|
||||
let requirements = RequirementsSpecification::from_source(&source, &client_builder)
|
||||
.await?
|
||||
.requirements;
|
||||
resolve_names(
|
||||
requirements,
|
||||
&interpreter,
|
||||
&settings,
|
||||
&state,
|
||||
connectivity,
|
||||
concurrency,
|
||||
native_tls,
|
||||
allow_insecure_host,
|
||||
&cache,
|
||||
printer,
|
||||
preview,
|
||||
)
|
||||
.await?
|
||||
.pop()
|
||||
.unwrap()
|
||||
}
|
||||
// Ex) `ruff@0.6.0`
|
||||
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: extras.clone(),
|
||||
groups: vec![],
|
||||
marker: MarkerTree::default(),
|
||||
source: RequirementSource::Registry {
|
||||
specifier: VersionSpecifiers::from(VersionSpecifier::equals_version(
|
||||
version.clone(),
|
||||
)),
|
||||
index: None,
|
||||
conflict: None,
|
||||
},
|
||||
origin: None,
|
||||
}
|
||||
}
|
||||
// Ex) `ruff@latest`
|
||||
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: extras.clone(),
|
||||
groups: vec![],
|
||||
marker: MarkerTree::default(),
|
||||
source: RequirementSource::Registry {
|
||||
specifier: VersionSpecifiers::empty(),
|
||||
index: None,
|
||||
conflict: None,
|
||||
},
|
||||
origin: None,
|
||||
}
|
||||
}
|
||||
// Ex) `ruff>=0.6.0`
|
||||
Target::From(package, from) => {
|
||||
// Parse the positional name. If the user provided more than a package name, it's an error
|
||||
// (e.g., `uv install foo==1.0 --from foo`).
|
||||
let Ok(package) = PackageName::from_str(package) else {
|
||||
bail!("Package requirement (`{from}`) provided with `--from` conflicts with install request (`{package}`)", from = from.cyan(), package = package.cyan())
|
||||
};
|
||||
|
||||
let source = if editable {
|
||||
RequirementsSource::Editable(from.to_string())
|
||||
} else {
|
||||
RequirementsSource::Package(from.to_string())
|
||||
};
|
||||
let requirements = RequirementsSpecification::from_source(&source, &client_builder)
|
||||
let requirement = RequirementsSpecification::from_source(&source, &client_builder)
|
||||
.await?
|
||||
.requirements;
|
||||
|
||||
// Parse the `--from` requirement.
|
||||
let from_requirement = resolve_names(
|
||||
requirements,
|
||||
// If the user provided an executable name, verify that it matches the `--from` requirement.
|
||||
let executable = if let Some(executable) = request.executable {
|
||||
let Ok(executable) = PackageName::from_str(executable) else {
|
||||
bail!("Package requirement (`{from}`) provided with `--from` conflicts with install request (`{executable}`)", from = from.cyan(), executable = executable.cyan())
|
||||
};
|
||||
Some(executable)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let requirement = resolve_names(
|
||||
requirement,
|
||||
&interpreter,
|
||||
&settings,
|
||||
&state,
|
||||
|
@ -209,17 +144,58 @@ pub(crate) async fn install(
|
|||
.pop()
|
||||
.unwrap();
|
||||
|
||||
// Check if the positional name conflicts with `--from`.
|
||||
if from_requirement.name != package {
|
||||
// Determine if it's an entirely different package (e.g., `uv install foo --from bar`).
|
||||
bail!(
|
||||
"Package name (`{}`) provided with `--from` does not match install request (`{}`)",
|
||||
from_requirement.name.cyan(),
|
||||
package.cyan()
|
||||
);
|
||||
// Determine if it's an entirely different package (e.g., `uv install foo --from bar`).
|
||||
if let Some(executable) = executable {
|
||||
if requirement.name != executable {
|
||||
bail!(
|
||||
"Package name (`{}`) provided with `--from` does not match install request (`{}`)",
|
||||
requirement.name.cyan(),
|
||||
executable.cyan()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
from_requirement
|
||||
requirement
|
||||
}
|
||||
// Ex) `ruff@0.6.0`
|
||||
Target::Version(.., name, ref extras, ref version) => {
|
||||
if editable {
|
||||
bail!("`--editable` is only supported for local packages");
|
||||
}
|
||||
|
||||
Requirement {
|
||||
name: name.clone(),
|
||||
extras: extras.clone(),
|
||||
groups: vec![],
|
||||
marker: MarkerTree::default(),
|
||||
source: RequirementSource::Registry {
|
||||
specifier: VersionSpecifiers::from(VersionSpecifier::equals_version(
|
||||
version.clone(),
|
||||
)),
|
||||
index: None,
|
||||
conflict: None,
|
||||
},
|
||||
origin: None,
|
||||
}
|
||||
}
|
||||
// Ex) `ruff@latest`
|
||||
Target::Latest(.., name, ref extras) => {
|
||||
if editable {
|
||||
bail!("`--editable` is only supported for local packages");
|
||||
}
|
||||
|
||||
Requirement {
|
||||
name: name.clone(),
|
||||
extras: extras.clone(),
|
||||
groups: vec![],
|
||||
marker: MarkerTree::default(),
|
||||
source: RequirementSource::Registry {
|
||||
specifier: VersionSpecifiers::empty(),
|
||||
index: None,
|
||||
conflict: None,
|
||||
},
|
||||
origin: None,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -232,7 +208,7 @@ pub(crate) async fn install(
|
|||
}
|
||||
|
||||
// If the user passed, e.g., `ruff@latest`, we need to mark it as upgradable.
|
||||
let settings = if target.is_latest() {
|
||||
let settings = if request.is_latest() {
|
||||
ResolverInstallerSettings {
|
||||
upgrade: settings
|
||||
.upgrade
|
||||
|
@ -368,7 +344,7 @@ pub(crate) async fn install(
|
|||
.as_ref()
|
||||
.filter(|_| {
|
||||
// And the user didn't request a reinstall or upgrade...
|
||||
!target.is_latest() && settings.reinstall.is_none() && settings.upgrade.is_none()
|
||||
!request.is_latest() && settings.reinstall.is_none() && settings.upgrade.is_none()
|
||||
})
|
||||
.is_some()
|
||||
{
|
||||
|
|
|
@ -14,84 +14,62 @@ pub(crate) mod uninstall;
|
|||
pub(crate) mod update_shell;
|
||||
pub(crate) mod upgrade;
|
||||
|
||||
/// A request to run or install a tool (e.g., `uvx ruff@latest`).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct ToolRequest<'a> {
|
||||
/// The executable name (e.g., `ruff`), if specified explicitly.
|
||||
pub(crate) executable: Option<&'a str>,
|
||||
/// The target to install or run (e.g., `ruff@latest` or `ruff==0.6.0`).
|
||||
pub(crate) target: Target<'a>,
|
||||
}
|
||||
|
||||
impl<'a> ToolRequest<'a> {
|
||||
/// Parse a tool request into an executable name and a target.
|
||||
pub(crate) fn parse(command: &'a str, from: Option<&'a str>) -> Self {
|
||||
if let Some(from) = from {
|
||||
let target = Target::parse(from);
|
||||
Self {
|
||||
executable: Some(command),
|
||||
target,
|
||||
}
|
||||
} else {
|
||||
let target = Target::parse(command);
|
||||
Self {
|
||||
executable: None,
|
||||
target,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the target package is Python.
|
||||
pub(crate) fn is_python(&self) -> bool {
|
||||
let name = match self.target {
|
||||
Target::Unspecified(name) => name,
|
||||
Target::Version(name, ..) => name,
|
||||
Target::Latest(name, ..) => name,
|
||||
};
|
||||
name.eq_ignore_ascii_case("python") || cfg!(windows) && name.eq_ignore_ascii_case("pythonw")
|
||||
}
|
||||
|
||||
/// Returns `true` if the target is `latest`.
|
||||
pub(crate) fn is_latest(&self) -> bool {
|
||||
matches!(self.target, Target::Latest(..))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum Target<'a> {
|
||||
/// e.g., `ruff`
|
||||
Unspecified(&'a str),
|
||||
/// e.g., `ruff[extra]@0.6.0`
|
||||
Version(&'a str, Vec<ExtraName>, Version),
|
||||
Version(&'a str, PackageName, Vec<ExtraName>, Version),
|
||||
/// e.g., `ruff[extra]@latest`
|
||||
Latest(&'a str, Vec<ExtraName>),
|
||||
/// e.g., `ruff --from ruff[extra]>=0.6.0`
|
||||
From(&'a str, &'a str),
|
||||
/// e.g., `ruff --from ruff[extra]@0.6.0`
|
||||
FromVersion(&'a str, &'a str, Vec<ExtraName>, Version),
|
||||
/// e.g., `ruff --from ruff[extra]@latest`
|
||||
FromLatest(&'a str, &'a str, Vec<ExtraName>),
|
||||
Latest(&'a str, PackageName, Vec<ExtraName>),
|
||||
}
|
||||
|
||||
impl<'a> Target<'a> {
|
||||
/// Parse a target into a command name and a requirement.
|
||||
pub(crate) fn parse(target: &'a str, from: Option<&'a str>) -> Self {
|
||||
if let Some(from) = from {
|
||||
// e.g. `--from ruff`, no special handling
|
||||
let Some((name, version)) = from.split_once('@') else {
|
||||
return Self::From(target, from);
|
||||
};
|
||||
|
||||
// e.g. `--from ruff@`, warn and treat the whole thing as the command
|
||||
if version.is_empty() {
|
||||
debug!("Ignoring empty version request in `--from`");
|
||||
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)
|
||||
.filter(|extra| !extra.is_empty())
|
||||
.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, extras),
|
||||
// e.g., `ruff@0.6.0`
|
||||
version => {
|
||||
if let Ok(version) = Version::from_str(version) {
|
||||
return Self::FromVersion(target, name, extras, version);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// e.g. `--from ruff@invalid`, warn and treat the whole thing as the command
|
||||
debug!("Ignoring invalid version request `{version}` in `--from`");
|
||||
return Self::From(target, from);
|
||||
}
|
||||
|
||||
pub(crate) fn parse(target: &'a str) -> Self {
|
||||
// e.g. `ruff`, no special handling
|
||||
let Some((name, version)) = target.split_once('@') else {
|
||||
return Self::Unspecified(target);
|
||||
|
@ -104,22 +82,22 @@ impl<'a> Target<'a> {
|
|||
}
|
||||
|
||||
// Split into name and extras (e.g., `flask[dotenv]`).
|
||||
let (name, extras) = match name.split_once('[') {
|
||||
Some((name, extras)) => {
|
||||
let (executable, extras) = match name.split_once('[') {
|
||||
Some((executable, extras)) => {
|
||||
let Some(extras) = extras.strip_suffix(']') else {
|
||||
// e.g., ignore `flask[dotenv`.
|
||||
return Self::Unspecified(name);
|
||||
return Self::Unspecified(target);
|
||||
};
|
||||
(name, extras)
|
||||
(executable, extras)
|
||||
}
|
||||
None => (name, ""),
|
||||
};
|
||||
|
||||
// e.g., ignore `git+https://github.com/astral-sh/ruff.git@main`
|
||||
if PackageName::from_str(name).is_err() {
|
||||
let Ok(name) = PackageName::from_str(executable) else {
|
||||
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
|
||||
|
@ -135,65 +113,19 @@ impl<'a> Target<'a> {
|
|||
|
||||
match version {
|
||||
// e.g., `ruff@latest`
|
||||
"latest" => return Self::Latest(name, extras),
|
||||
"latest" => Self::Latest(executable, name, extras),
|
||||
// e.g., `ruff@0.6.0`
|
||||
version => {
|
||||
if let Ok(version) = Version::from_str(version) {
|
||||
return Self::Version(name, extras, version);
|
||||
Self::Version(executable, name, extras, version)
|
||||
} else {
|
||||
// e.g. `ruff@invalid`, warn and treat the whole thing as the command
|
||||
debug!("Ignoring invalid version request `{version}` in command");
|
||||
Self::Unspecified(target)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// e.g. `ruff@invalid`, warn and treat the whole thing as the command
|
||||
debug!("Ignoring invalid version request `{version}` in command");
|
||||
Self::Unspecified(target)
|
||||
}
|
||||
|
||||
/// Returns the name of the executable.
|
||||
pub(crate) fn executable(&self) -> &str {
|
||||
match self {
|
||||
Self::Unspecified(name) => {
|
||||
// Identify the package name from the PEP 508 specifier.
|
||||
//
|
||||
// For example, given `ruff>=0.6.0`, extract `ruff`, to use as the executable name.
|
||||
let index = name
|
||||
.find(|c| !matches!(c, 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.'))
|
||||
.unwrap_or(name.len());
|
||||
&name[..index]
|
||||
}
|
||||
Self::Version(name, _, _) => name,
|
||||
Self::Latest(name, _) => name,
|
||||
Self::FromVersion(name, _, _, _) => name,
|
||||
Self::FromLatest(name, _, _) => name,
|
||||
Self::From(name, _) => name,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the target package is Python.
|
||||
pub(crate) fn is_python(&self) -> bool {
|
||||
let name = match self {
|
||||
Self::Unspecified(name) => {
|
||||
// Identify the package name from the PEP 508 specifier.
|
||||
//
|
||||
// For example, given `ruff>=0.6.0`, extract `ruff`, to use as the executable name.
|
||||
let index = name
|
||||
.find(|c| !matches!(c, 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.'))
|
||||
.unwrap_or(name.len());
|
||||
&name[..index]
|
||||
}
|
||||
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(..))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -202,41 +134,56 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn parse_target() {
|
||||
let target = Target::parse("flask", None);
|
||||
let target = Target::parse("flask");
|
||||
let expected = Target::Unspecified("flask");
|
||||
assert_eq!(target, expected);
|
||||
|
||||
let target = Target::parse("flask@3.0.0", None);
|
||||
let expected = Target::Version("flask", vec![], Version::new([3, 0, 0]));
|
||||
assert_eq!(target, expected);
|
||||
|
||||
let target = Target::parse("flask@3.0.0", None);
|
||||
let expected = Target::Version("flask", vec![], Version::new([3, 0, 0]));
|
||||
assert_eq!(target, expected);
|
||||
|
||||
let target = Target::parse("flask@latest", None);
|
||||
let expected = Target::Latest("flask", vec![]);
|
||||
assert_eq!(target, expected);
|
||||
|
||||
let target = Target::parse("flask[dotenv]@3.0.0", None);
|
||||
let target = Target::parse("flask@3.0.0");
|
||||
let expected = Target::Version(
|
||||
"flask",
|
||||
PackageName::from_str("flask").unwrap(),
|
||||
vec![],
|
||||
Version::new([3, 0, 0]),
|
||||
);
|
||||
assert_eq!(target, expected);
|
||||
|
||||
let target = Target::parse("flask@3.0.0");
|
||||
let expected = Target::Version(
|
||||
"flask",
|
||||
PackageName::from_str("flask").unwrap(),
|
||||
vec![],
|
||||
Version::new([3, 0, 0]),
|
||||
);
|
||||
assert_eq!(target, expected);
|
||||
|
||||
let target = Target::parse("flask@latest");
|
||||
let expected = Target::Latest("flask", PackageName::from_str("flask").unwrap(), vec![]);
|
||||
assert_eq!(target, expected);
|
||||
|
||||
let target = Target::parse("flask[dotenv]@3.0.0");
|
||||
let expected = Target::Version(
|
||||
"flask",
|
||||
PackageName::from_str("flask").unwrap(),
|
||||
vec![ExtraName::from_str("dotenv").unwrap()],
|
||||
Version::new([3, 0, 0]),
|
||||
);
|
||||
assert_eq!(target, expected);
|
||||
|
||||
let target = Target::parse("flask[dotenv]@latest", None);
|
||||
let expected = Target::Latest("flask", vec![ExtraName::from_str("dotenv").unwrap()]);
|
||||
let target = Target::parse("flask[dotenv]@latest");
|
||||
let expected = Target::Latest(
|
||||
"flask",
|
||||
PackageName::from_str("flask").unwrap(),
|
||||
vec![ExtraName::from_str("dotenv").unwrap()],
|
||||
);
|
||||
assert_eq!(target, expected);
|
||||
|
||||
// Missing a closing `]`.
|
||||
let target = Target::parse("flask[dotenv", None);
|
||||
let target = Target::parse("flask[dotenv");
|
||||
let expected = Target::Unspecified("flask[dotenv");
|
||||
assert_eq!(target, expected);
|
||||
|
||||
// Too many `]`.
|
||||
let target = Target::parse("flask[dotenv]]", None);
|
||||
let target = Target::parse("flask[dotenv]]");
|
||||
let expected = Target::Unspecified("flask[dotenv]]");
|
||||
assert_eq!(target, expected);
|
||||
}
|
||||
|
|
|
@ -15,8 +15,7 @@ use uv_cache_info::Timestamp;
|
|||
use uv_cli::ExternalCommand;
|
||||
use uv_client::{BaseClientBuilder, Connectivity};
|
||||
use uv_configuration::{Concurrency, PreviewMode, TrustedHost};
|
||||
use uv_distribution_types::UnresolvedRequirement;
|
||||
use uv_distribution_types::{Name, UnresolvedRequirementSpecification};
|
||||
use uv_distribution_types::{Name, UnresolvedRequirement, UnresolvedRequirementSpecification};
|
||||
use uv_installer::{SatisfiesResult, SitePackages};
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pep440::{VersionSpecifier, VersionSpecifiers};
|
||||
|
@ -42,7 +41,7 @@ use crate::commands::project::{
|
|||
use crate::commands::reporters::PythonDownloadReporter;
|
||||
use crate::commands::run::run_to_completion;
|
||||
use crate::commands::tool::common::{matching_packages, refine_interpreter};
|
||||
use crate::commands::tool::Target;
|
||||
use crate::commands::tool::{Target, ToolRequest};
|
||||
use crate::commands::ExitStatus;
|
||||
use crate::commands::{diagnostics, project::environment::CachedEnvironment};
|
||||
use crate::printer::Printer;
|
||||
|
@ -105,10 +104,10 @@ pub(crate) async fn run(
|
|||
return Err(anyhow::anyhow!("Tool command could not be parsed as UTF-8 string. Use `--from` to specify the package name."));
|
||||
};
|
||||
|
||||
let target = Target::parse(target, from.as_deref());
|
||||
let request = ToolRequest::parse(target, from.as_deref());
|
||||
|
||||
// If the user passed, e.g., `ruff@latest`, refresh the cache.
|
||||
let cache = if target.is_latest() {
|
||||
let cache = if request.is_latest() {
|
||||
cache.with_refresh(Refresh::All(Timestamp::now()))
|
||||
} else {
|
||||
cache
|
||||
|
@ -116,7 +115,7 @@ pub(crate) async fn run(
|
|||
|
||||
// Get or create a compatible environment in which to execute the tool.
|
||||
let result = get_or_create_environment(
|
||||
&target,
|
||||
&request,
|
||||
with,
|
||||
show_resolution,
|
||||
python.as_deref(),
|
||||
|
@ -154,7 +153,7 @@ pub(crate) async fn run(
|
|||
};
|
||||
|
||||
// TODO(zanieb): Determine the executable command via the package entry points
|
||||
let executable = target.executable();
|
||||
let executable = from.executable();
|
||||
|
||||
// Construct the command
|
||||
let mut process = Command::new(executable);
|
||||
|
@ -187,7 +186,9 @@ pub(crate) async fn run(
|
|||
// If the command is found in other packages, we warn the user about the correct package to use.
|
||||
match &from {
|
||||
ToolRequirement::Python => {}
|
||||
ToolRequirement::Package(from) => {
|
||||
ToolRequirement::Package {
|
||||
requirement: from, ..
|
||||
} => {
|
||||
warn_executable_not_provided_by_package(
|
||||
executable,
|
||||
&from.name,
|
||||
|
@ -230,7 +231,9 @@ fn hint_on_not_found(
|
|||
) -> anyhow::Result<Option<ExitStatus>> {
|
||||
let from = match from {
|
||||
ToolRequirement::Python => return Ok(None),
|
||||
ToolRequirement::Package(from) => from,
|
||||
ToolRequirement::Package {
|
||||
requirement: from, ..
|
||||
} => from,
|
||||
};
|
||||
match get_entrypoints(&from.name, site_packages) {
|
||||
Ok(entrypoints) => {
|
||||
|
@ -413,14 +416,26 @@ fn warn_executable_not_provided_by_package(
|
|||
#[allow(clippy::large_enum_variant)]
|
||||
pub(crate) enum ToolRequirement {
|
||||
Python,
|
||||
Package(Requirement),
|
||||
Package {
|
||||
executable: String,
|
||||
requirement: Requirement,
|
||||
},
|
||||
}
|
||||
|
||||
impl ToolRequirement {
|
||||
fn executable(&self) -> &str {
|
||||
match self {
|
||||
ToolRequirement::Python => "python",
|
||||
ToolRequirement::Package { executable, .. } => executable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ToolRequirement {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ToolRequirement::Python => write!(f, "python"),
|
||||
ToolRequirement::Package(requirement) => write!(f, "{requirement}"),
|
||||
ToolRequirement::Package { requirement, .. } => write!(f, "{requirement}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -431,7 +446,7 @@ impl std::fmt::Display for ToolRequirement {
|
|||
/// [`PythonEnvironment`]. Otherwise, gets or creates a [`CachedEnvironment`].
|
||||
#[allow(clippy::fn_params_excessive_bools)]
|
||||
async fn get_or_create_environment(
|
||||
target: &Target<'_>,
|
||||
request: &ToolRequest<'_>,
|
||||
with: &[RequirementsSource],
|
||||
show_resolution: bool,
|
||||
python: Option<&str>,
|
||||
|
@ -457,26 +472,19 @@ async fn get_or_create_environment(
|
|||
let reporter = PythonDownloadReporter::single(printer);
|
||||
|
||||
// Check if the target is `python`
|
||||
let python_request = if target.is_python() {
|
||||
let target_request = match target {
|
||||
let python_request = if request.is_python() {
|
||||
let target_request = match &request.target {
|
||||
Target::Unspecified(_) => None,
|
||||
Target::Version(_, _, version) | Target::FromVersion(_, _, _, version) => {
|
||||
Some(PythonRequest::Version(
|
||||
VersionRequest::from_str(&version.to_string()).map_err(anyhow::Error::from)?,
|
||||
))
|
||||
}
|
||||
Target::Version(_, _, _, 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(_, _, _) => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Requesting the 'latest' Python version is not yet supported"
|
||||
)
|
||||
.into())
|
||||
}
|
||||
// From the definition of `is_python`, this can only be a bare `python`
|
||||
Target::From(_, from) => {
|
||||
debug_assert_eq!(*from, "python");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(target_request) = &target_request {
|
||||
|
@ -513,65 +521,14 @@ async fn get_or_create_environment(
|
|||
// Initialize any shared state.
|
||||
let state = PlatformState::default();
|
||||
|
||||
let from = if target.is_python() {
|
||||
let from = if request.is_python() {
|
||||
ToolRequirement::Python
|
||||
} else {
|
||||
ToolRequirement::Package(match target {
|
||||
// Ex) `ruff`
|
||||
Target::Unspecified(name) => {
|
||||
let source = RequirementsSource::Package((*name).to_string());
|
||||
let requirements = RequirementsSpecification::from_source(&source, &client_builder)
|
||||
.await?
|
||||
.requirements;
|
||||
resolve_names(
|
||||
requirements,
|
||||
&interpreter,
|
||||
settings,
|
||||
&state,
|
||||
connectivity,
|
||||
concurrency,
|
||||
native_tls,
|
||||
allow_insecure_host,
|
||||
cache,
|
||||
printer,
|
||||
preview,
|
||||
)
|
||||
.await?
|
||||
.pop()
|
||||
.unwrap()
|
||||
}
|
||||
// Ex) `ruff@0.6.0`
|
||||
Target::Version(name, extras, version)
|
||||
| Target::FromVersion(_, name, extras, version) => Requirement {
|
||||
name: PackageName::from_str(name)?,
|
||||
extras: extras.clone(),
|
||||
groups: vec![],
|
||||
marker: MarkerTree::default(),
|
||||
source: RequirementSource::Registry {
|
||||
specifier: VersionSpecifiers::from(VersionSpecifier::equals_version(
|
||||
version.clone(),
|
||||
)),
|
||||
index: None,
|
||||
conflict: None,
|
||||
},
|
||||
origin: None,
|
||||
},
|
||||
// Ex) `ruff@latest`
|
||||
Target::Latest(name, extras) | Target::FromLatest(_, name, extras) => Requirement {
|
||||
name: PackageName::from_str(name)?,
|
||||
extras: extras.clone(),
|
||||
groups: vec![],
|
||||
marker: MarkerTree::default(),
|
||||
source: RequirementSource::Registry {
|
||||
specifier: VersionSpecifiers::empty(),
|
||||
index: None,
|
||||
conflict: None,
|
||||
},
|
||||
origin: None,
|
||||
},
|
||||
let (executable, requirement) = match &request.target {
|
||||
// Ex) `ruff>=0.6.0`
|
||||
Target::From(_, from) => {
|
||||
let spec = RequirementsSpecification::parse_package(from)?;
|
||||
Target::Unspecified(requirement) => {
|
||||
let spec = RequirementsSpecification::parse_package(requirement)?;
|
||||
debug!("{:?}", spec);
|
||||
if let UnresolvedRequirement::Named(requirement) = &spec.requirement {
|
||||
if requirement.name.as_str() == "python" {
|
||||
return Err(anyhow::anyhow!(
|
||||
|
@ -582,7 +539,7 @@ async fn get_or_create_environment(
|
|||
.into());
|
||||
}
|
||||
}
|
||||
resolve_names(
|
||||
let requirement = resolve_names(
|
||||
vec![spec],
|
||||
&interpreter,
|
||||
settings,
|
||||
|
@ -597,9 +554,58 @@ async fn get_or_create_environment(
|
|||
)
|
||||
.await?
|
||||
.pop()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
// Use the executable provided by the user, if possible (as in: `uvx --from package executable`).
|
||||
//
|
||||
// If no such executable was provided, rely on the package name (as in: `uvx git+https://github.com/pallets/flask`).
|
||||
let executable = request
|
||||
.executable
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_else(|| requirement.name.to_string());
|
||||
|
||||
(executable, requirement)
|
||||
}
|
||||
})
|
||||
// Ex) `ruff@0.6.0`
|
||||
Target::Version(executable, name, extras, version) => (
|
||||
(*executable).to_string(),
|
||||
Requirement {
|
||||
name: name.clone(),
|
||||
extras: extras.clone(),
|
||||
groups: vec![],
|
||||
marker: MarkerTree::default(),
|
||||
source: RequirementSource::Registry {
|
||||
specifier: VersionSpecifiers::from(VersionSpecifier::equals_version(
|
||||
version.clone(),
|
||||
)),
|
||||
index: None,
|
||||
conflict: None,
|
||||
},
|
||||
origin: None,
|
||||
},
|
||||
),
|
||||
// Ex) `ruff@latest`
|
||||
Target::Latest(executable, name, extras) => (
|
||||
(*executable).to_string(),
|
||||
Requirement {
|
||||
name: name.clone(),
|
||||
extras: extras.clone(),
|
||||
groups: vec![],
|
||||
marker: MarkerTree::default(),
|
||||
source: RequirementSource::Registry {
|
||||
specifier: VersionSpecifiers::empty(),
|
||||
index: None,
|
||||
conflict: None,
|
||||
},
|
||||
origin: None,
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
ToolRequirement::Package {
|
||||
executable,
|
||||
requirement,
|
||||
}
|
||||
};
|
||||
|
||||
// Read the `--with` requirements.
|
||||
|
@ -616,7 +622,7 @@ async fn get_or_create_environment(
|
|||
let mut requirements = Vec::with_capacity(1 + with.len());
|
||||
match &from {
|
||||
ToolRequirement::Python => {}
|
||||
ToolRequirement::Package(requirement) => requirements.push(requirement.clone()),
|
||||
ToolRequirement::Package { requirement, .. } => requirements.push(requirement.clone()),
|
||||
}
|
||||
requirements.extend(
|
||||
resolve_names(
|
||||
|
@ -638,11 +644,11 @@ async fn get_or_create_environment(
|
|||
};
|
||||
|
||||
// Check if the tool is already installed in a compatible environment.
|
||||
if !isolated && !target.is_latest() {
|
||||
if !isolated && !request.is_latest() {
|
||||
let installed_tools = InstalledTools::from_settings()?.init()?;
|
||||
let _lock = installed_tools.lock().await?;
|
||||
|
||||
if let ToolRequirement::Package(requirement) = &from {
|
||||
if let ToolRequirement::Package { requirement, .. } = &from {
|
||||
let existing_environment = installed_tools
|
||||
.get_environment(&requirement.name, cache)?
|
||||
.filter(|environment| {
|
||||
|
|
|
@ -3351,3 +3351,57 @@ fn tool_install_python() {
|
|||
error: Cannot install Python with `uv tool install`. Did you mean to use `uv python install`?
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_install_mismatched_name() {
|
||||
let context = TestContext::new("3.12")
|
||||
.with_filtered_counts()
|
||||
.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_install()
|
||||
.arg("black")
|
||||
.arg("--from")
|
||||
.arg("https://files.pythonhosted.org/packages/af/47/93213ee66ef8fae3b93b3e29206f6b251e65c97bd91d8e1c5596ef15af0a/flask-3.1.0-py3-none-any.whl")
|
||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||
.env(EnvVars::PATH, bin_dir.as_os_str()), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Package name (`flask`) provided with `--from` does not match install request (`black`)
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.tool_install()
|
||||
.arg("black")
|
||||
.arg("--from")
|
||||
.arg("flask @ https://files.pythonhosted.org/packages/af/47/93213ee66ef8fae3b93b3e29206f6b251e65c97bd91d8e1c5596ef15af0a/flask-3.1.0-py3-none-any.whl")
|
||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||
.env(EnvVars::PATH, bin_dir.as_os_str()), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Package name (`flask`) provided with `--from` does not match install request (`black`)
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.tool_install()
|
||||
.arg("flask")
|
||||
.arg("--from")
|
||||
.arg("black @ https://files.pythonhosted.org/packages/af/47/93213ee66ef8fae3b93b3e29206f6b251e65c97bd91d8e1c5596ef15af0a/flask-3.1.0-py3-none-any.whl")
|
||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||
.env(EnvVars::PATH, bin_dir.as_os_str()), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Package name (`black`) provided with `--from` does not match install request (`flask`)
|
||||
"###);
|
||||
}
|
||||
|
|
|
@ -655,6 +655,56 @@ fn tool_run_url() {
|
|||
+ markupsafe==2.1.5
|
||||
+ werkzeug==3.0.1
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.tool_run()
|
||||
.arg("--from")
|
||||
.arg("https://files.pythonhosted.org/packages/61/80/ffe1da13ad9300f87c93af113edd0638c75138c42a0994becfacac078c06/flask-3.0.3-py3-none-any.whl")
|
||||
.arg("flask")
|
||||
.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.3
|
||||
Werkzeug 3.0.1
|
||||
|
||||
----- stderr -----
|
||||
Resolved [N] packages in [TIME]
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.tool_run()
|
||||
.arg("flask @ https://files.pythonhosted.org/packages/61/80/ffe1da13ad9300f87c93af113edd0638c75138c42a0994becfacac078c06/flask-3.0.3-py3-none-any.whl")
|
||||
.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.3
|
||||
Werkzeug 3.0.1
|
||||
|
||||
----- stderr -----
|
||||
Resolved [N] packages in [TIME]
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.tool_run()
|
||||
.arg("https://files.pythonhosted.org/packages/61/80/ffe1da13ad9300f87c93af113edd0638c75138c42a0994becfacac078c06/flask-3.0.3-py3-none-any.whl")
|
||||
.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.3
|
||||
Werkzeug 3.0.1
|
||||
|
||||
----- stderr -----
|
||||
Resolved [N] packages in [TIME]
|
||||
"###);
|
||||
}
|
||||
|
||||
/// Read requirements from a `requirements.txt` file.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue