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 pypi_types::Requirement;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::Connectivity;
|
use uv_client::Connectivity;
|
||||||
use uv_configuration::{Concurrency, PreviewMode};
|
use uv_configuration::{Concurrency, PreviewMode};
|
||||||
use uv_python::Interpreter;
|
use uv_installer::SitePackages;
|
||||||
|
use uv_python::{Interpreter, PythonEnvironment};
|
||||||
use uv_requirements::RequirementsSpecification;
|
use uv_requirements::RequirementsSpecification;
|
||||||
|
use uv_tool::entrypoint_paths;
|
||||||
|
|
||||||
use crate::commands::{project, SharedState};
|
use crate::commands::{project, SharedState};
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
|
@ -46,3 +49,31 @@ pub(super) async fn resolve_requirements(
|
||||||
)
|
)
|
||||||
.await
|
.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 anyhow::{bail, Context, Result};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
use tracing::debug;
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
use distribution_types::Name;
|
use distribution_types::Name;
|
||||||
use pypi_types::Requirement;
|
use pypi_types::Requirement;
|
||||||
|
@ -19,16 +19,20 @@ use uv_fs::Simplified;
|
||||||
use uv_installer::SitePackages;
|
use uv_installer::SitePackages;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_python::{
|
use uv_python::{
|
||||||
EnvironmentPreference, PythonFetch, PythonInstallation, PythonPreference, PythonRequest,
|
EnvironmentPreference, PythonEnvironment, PythonFetch, PythonInstallation, PythonPreference,
|
||||||
|
PythonRequest,
|
||||||
};
|
};
|
||||||
use uv_requirements::RequirementsSpecification;
|
use uv_requirements::RequirementsSpecification;
|
||||||
use uv_shell::Shell;
|
use uv_shell::Shell;
|
||||||
use uv_tool::{entrypoint_paths, find_executable_directory, InstalledTools, Tool, ToolEntrypoint};
|
use uv_tool::{entrypoint_paths, find_executable_directory, InstalledTools, Tool, ToolEntrypoint};
|
||||||
use uv_warnings::{warn_user, warn_user_once};
|
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::reporters::PythonDownloadReporter;
|
||||||
use crate::commands::tool::common::resolve_requirements;
|
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::commands::{ExitStatus, SharedState};
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
use crate::settings::ResolverInstallerSettings;
|
use crate::settings::ResolverInstallerSettings;
|
||||||
|
@ -296,10 +300,17 @@ pub(crate) async fn install(
|
||||||
.collect::<BTreeSet<_>>();
|
.collect::<BTreeSet<_>>();
|
||||||
|
|
||||||
if target_entry_points.is_empty() {
|
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
|
// Clean up the environment we just created
|
||||||
installed_tools.remove_environment(&from.name)?;
|
installed_tools.remove_environment(&from.name)?;
|
||||||
|
return Ok(ExitStatus::Failure);
|
||||||
bail!("No executables found for `{}`", from.name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if they exist, before installing
|
// Check if they exist, before installing
|
||||||
|
@ -407,3 +418,46 @@ pub(crate) async fn install(
|
||||||
|
|
||||||
Ok(ExitStatus::Success)
|
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 installing a tool at a version
|
||||||
#[test]
|
#[test]
|
||||||
fn tool_install_version() {
|
fn tool_install_version() {
|
||||||
|
@ -911,8 +974,9 @@ fn tool_install_no_entrypoints() {
|
||||||
.env("XDG_BIN_HOME", bin_dir.as_os_str())
|
.env("XDG_BIN_HOME", bin_dir.as_os_str())
|
||||||
.env("PATH", bin_dir.as_os_str()), @r###"
|
.env("PATH", bin_dir.as_os_str()), @r###"
|
||||||
success: false
|
success: false
|
||||||
exit_code: 2
|
exit_code: 1
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
No executables are provided by package `iniconfig`.
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
warning: `uv tool install` is experimental and may change without warning.
|
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]
|
Prepared 1 package in [TIME]
|
||||||
Installed 1 package in [TIME]
|
Installed 1 package in [TIME]
|
||||||
+ iniconfig==2.0.0
|
+ iniconfig==2.0.0
|
||||||
error: No executables found for `iniconfig`
|
|
||||||
"###);
|
"###);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue