mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
uv tool install
hint the correct when the executable is available (#5019)
## Summary Resolves #5018. ## Test Plan `cargo test` <img width="704" alt="Screenshot 2024-07-12 at 22 16 53" src="https://github.com/user-attachments/assets/d2d4d85b-d6c3-4b47-8f1a-bb07112d5931"> --------- Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
parent
09ae7a93d1
commit
493a2bfe63
3 changed files with 156 additions and 8 deletions
|
@ -1,9 +1,12 @@
|
|||
use distribution_types::{InstalledDist, Name};
|
||||
use pypi_types::Requirement;
|
||||
use uv_cache::Cache;
|
||||
use uv_client::Connectivity;
|
||||
use uv_configuration::{Concurrency, PreviewMode};
|
||||
use uv_python::Interpreter;
|
||||
use uv_installer::SitePackages;
|
||||
use uv_python::{Interpreter, PythonEnvironment};
|
||||
use uv_requirements::RequirementsSpecification;
|
||||
use uv_tool::entrypoint_paths;
|
||||
|
||||
use crate::commands::{project, SharedState};
|
||||
use crate::printer::Printer;
|
||||
|
@ -46,3 +49,31 @@ pub(super) async fn resolve_requirements(
|
|||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Return all packages which contain an executable with the given name.
|
||||
pub(super) fn matching_packages(
|
||||
name: &str,
|
||||
environment: &PythonEnvironment,
|
||||
) -> anyhow::Result<Vec<InstalledDist>> {
|
||||
let site_packages = SitePackages::from_environment(environment)?;
|
||||
let entrypoints = site_packages
|
||||
.iter()
|
||||
.filter_map(|package| {
|
||||
entrypoint_paths(environment, package.name(), package.version())
|
||||
.ok()
|
||||
.and_then(|entrypoints| {
|
||||
entrypoints
|
||||
.iter()
|
||||
.any(|entrypoint| {
|
||||
entrypoint
|
||||
.0
|
||||
.strip_suffix(std::env::consts::EXE_SUFFIX)
|
||||
.is_some_and(|stripped| stripped == name)
|
||||
})
|
||||
.then(|| package.clone())
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(entrypoints)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ use std::str::FromStr;
|
|||
use anyhow::{bail, Context, Result};
|
||||
use itertools::Itertools;
|
||||
use owo_colors::OwoColorize;
|
||||
use tracing::debug;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use distribution_types::Name;
|
||||
use pypi_types::Requirement;
|
||||
|
@ -19,16 +19,20 @@ use uv_fs::Simplified;
|
|||
use uv_installer::SitePackages;
|
||||
use uv_normalize::PackageName;
|
||||
use uv_python::{
|
||||
EnvironmentPreference, PythonFetch, PythonInstallation, PythonPreference, PythonRequest,
|
||||
EnvironmentPreference, PythonEnvironment, PythonFetch, PythonInstallation, PythonPreference,
|
||||
PythonRequest,
|
||||
};
|
||||
use uv_requirements::RequirementsSpecification;
|
||||
use uv_shell::Shell;
|
||||
use uv_tool::{entrypoint_paths, find_executable_directory, InstalledTools, Tool, ToolEntrypoint};
|
||||
use uv_warnings::{warn_user, warn_user_once};
|
||||
|
||||
use crate::commands::project::{resolve_environment, sync_environment, update_environment};
|
||||
use crate::commands::reporters::PythonDownloadReporter;
|
||||
use crate::commands::tool::common::resolve_requirements;
|
||||
use crate::commands::{
|
||||
project::{resolve_environment, sync_environment, update_environment},
|
||||
tool::common::matching_packages,
|
||||
};
|
||||
use crate::commands::{ExitStatus, SharedState};
|
||||
use crate::printer::Printer;
|
||||
use crate::settings::ResolverInstallerSettings;
|
||||
|
@ -296,10 +300,17 @@ pub(crate) async fn install(
|
|||
.collect::<BTreeSet<_>>();
|
||||
|
||||
if target_entry_points.is_empty() {
|
||||
writeln!(
|
||||
printer.stdout(),
|
||||
"No executables are provided by package `{}`.",
|
||||
from.name.red()
|
||||
)?;
|
||||
|
||||
hint_executable_from_dependency(&from, &environment, printer)?;
|
||||
|
||||
// Clean up the environment we just created
|
||||
installed_tools.remove_environment(&from.name)?;
|
||||
|
||||
bail!("No executables found for `{}`", from.name);
|
||||
return Ok(ExitStatus::Failure);
|
||||
}
|
||||
|
||||
// Check if they exist, before installing
|
||||
|
@ -407,3 +418,46 @@ pub(crate) async fn install(
|
|||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
||||
/// Displays a hint if an executable matching the package name can be found in a dependency of the package.
|
||||
fn hint_executable_from_dependency(
|
||||
from: &Requirement,
|
||||
environment: &PythonEnvironment,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
match matching_packages(from.name.as_ref(), environment) {
|
||||
Ok(packages) => match packages.as_slice() {
|
||||
[] => {}
|
||||
[package] => {
|
||||
let command = format!("uv tool install {}", package.name());
|
||||
writeln!(
|
||||
printer.stdout(),
|
||||
"However, an executable with the name `{}` is available via dependency `{}`.\nDid you mean `{}`?",
|
||||
from.name.green(),
|
||||
package.name().green(),
|
||||
command.bold(),
|
||||
)?;
|
||||
}
|
||||
packages => {
|
||||
writeln!(
|
||||
printer.stdout(),
|
||||
"However, an executable with the name `{}` is available via the following dependencies::",
|
||||
from.name.green(),
|
||||
)?;
|
||||
|
||||
for package in packages {
|
||||
writeln!(printer.stdout(), "- {}", package.name().cyan())?;
|
||||
}
|
||||
writeln!(
|
||||
printer.stdout(),
|
||||
"Did you mean to install one of them instead?"
|
||||
)?;
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
warn!("Failed to determine executables for packages: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -170,6 +170,69 @@ fn tool_install() {
|
|||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_install_suggest_other_packages_with_executable() {
|
||||
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");
|
||||
let mut filters = context.filters();
|
||||
filters.push(("\\+ uvloop(.+)\n ", ""));
|
||||
|
||||
uv_snapshot!(filters, context.tool_install_without_exclude_newer()
|
||||
.arg("fastapi==0.111.0")
|
||||
.env("UV_EXCLUDE_NEWER", "2024-05-04T00:00:00Z") // TODO: Remove this once EXCLUDE_NEWER is bumped past 2024-05-04
|
||||
// (FastAPI 0.111 is only available from this date onwards)
|
||||
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
No executables are provided by package `fastapi`.
|
||||
However, an executable with the name `fastapi` is available via dependency `fastapi-cli`.
|
||||
Did you mean `uv tool install fastapi-cli`?
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv tool install` is experimental and may change without warning.
|
||||
Resolved 35 packages in [TIME]
|
||||
Prepared 35 packages in [TIME]
|
||||
Installed 35 packages in [TIME]
|
||||
+ annotated-types==0.6.0
|
||||
+ anyio==4.3.0
|
||||
+ certifi==2024.2.2
|
||||
+ click==8.1.7
|
||||
+ dnspython==2.6.1
|
||||
+ email-validator==2.1.1
|
||||
+ fastapi==0.111.0
|
||||
+ fastapi-cli==0.0.2
|
||||
+ h11==0.14.0
|
||||
+ httpcore==1.0.5
|
||||
+ httptools==0.6.1
|
||||
+ httpx==0.27.0
|
||||
+ idna==3.7
|
||||
+ jinja2==3.1.3
|
||||
+ markdown-it-py==3.0.0
|
||||
+ markupsafe==2.1.5
|
||||
+ mdurl==0.1.2
|
||||
+ orjson==3.10.3
|
||||
+ pydantic==2.7.1
|
||||
+ pydantic-core==2.18.2
|
||||
+ pygments==2.17.2
|
||||
+ python-dotenv==1.0.1
|
||||
+ python-multipart==0.0.9
|
||||
+ pyyaml==6.0.1
|
||||
+ rich==13.7.1
|
||||
+ shellingham==1.5.4
|
||||
+ sniffio==1.3.1
|
||||
+ starlette==0.37.2
|
||||
+ typer==0.12.3
|
||||
+ typing-extensions==4.11.0
|
||||
+ ujson==5.9.0
|
||||
+ uvicorn==0.29.0
|
||||
+ watchfiles==0.21.0
|
||||
+ websockets==12.0
|
||||
"###);
|
||||
}
|
||||
|
||||
/// Test installing a tool at a version
|
||||
#[test]
|
||||
fn tool_install_version() {
|
||||
|
@ -911,8 +974,9 @@ fn tool_install_no_entrypoints() {
|
|||
.env("XDG_BIN_HOME", bin_dir.as_os_str())
|
||||
.env("PATH", bin_dir.as_os_str()), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
No executables are provided by package `iniconfig`.
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv tool install` is experimental and may change without warning.
|
||||
|
@ -920,7 +984,6 @@ fn tool_install_no_entrypoints() {
|
|||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ iniconfig==2.0.0
|
||||
error: No executables found for `iniconfig`
|
||||
"###);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue