mirror of
https://github.com/astral-sh/uv.git
synced 2025-09-30 14:01:13 +00:00
Implement uv pip show
(#2115)
## Summary Implementation for https://github.com/astral-sh/uv/issues/1594 The output will contain only the name, version and location of the packages for now but it should be extendable to include other information in the future. Quite inexperienced with Rust, so please forgive me if there are things that obviously don't make sense 😭 ## Test Plan Added a bunch of unit tests. The exit code behavior matches `pip`'s behavior: - When the package is found -> exit code 0 - When the package isn't found -> exit code 1 - When one package is found but another isn't -> exit code 0
This commit is contained in:
parent
30bc16a3c1
commit
395be442fc
4 changed files with 586 additions and 0 deletions
|
@ -12,6 +12,7 @@ pub(crate) use pip_compile::{extra_name_with_clap_error, pip_compile, Upgrade};
|
|||
pub(crate) use pip_freeze::pip_freeze;
|
||||
pub(crate) use pip_install::pip_install;
|
||||
pub(crate) use pip_list::pip_list;
|
||||
pub(crate) use pip_show::pip_show;
|
||||
pub(crate) use pip_sync::pip_sync;
|
||||
pub(crate) use pip_uninstall::pip_uninstall;
|
||||
use uv_cache::Cache;
|
||||
|
@ -29,6 +30,7 @@ mod pip_compile;
|
|||
mod pip_freeze;
|
||||
mod pip_install;
|
||||
mod pip_list;
|
||||
mod pip_show;
|
||||
mod pip_sync;
|
||||
mod pip_uninstall;
|
||||
mod reporters;
|
||||
|
|
143
crates/uv/src/commands/pip_show.rs
Normal file
143
crates/uv/src/commands/pip_show.rs
Normal file
|
@ -0,0 +1,143 @@
|
|||
use std::fmt::Write;
|
||||
|
||||
use anyhow::Result;
|
||||
use owo_colors::OwoColorize;
|
||||
use tracing::debug;
|
||||
|
||||
use anstream::{eprintln, println};
|
||||
use distribution_types::Name;
|
||||
use platform_host::Platform;
|
||||
use uv_cache::Cache;
|
||||
use uv_fs::Simplified;
|
||||
use uv_installer::SitePackages;
|
||||
use uv_interpreter::PythonEnvironment;
|
||||
use uv_normalize::PackageName;
|
||||
|
||||
use crate::commands::ExitStatus;
|
||||
use crate::printer::Printer;
|
||||
|
||||
/// Show information about one or more installed packages.
|
||||
pub(crate) fn pip_show(
|
||||
mut packages: Vec<PackageName>,
|
||||
strict: bool,
|
||||
python: Option<&str>,
|
||||
system: bool,
|
||||
quiet: bool,
|
||||
cache: &Cache,
|
||||
mut printer: Printer,
|
||||
) -> Result<ExitStatus> {
|
||||
if packages.is_empty() {
|
||||
#[allow(clippy::print_stderr)]
|
||||
{
|
||||
eprintln!(
|
||||
"{}{} Please provide a package name or names.",
|
||||
"warning".yellow().bold(),
|
||||
":".bold(),
|
||||
);
|
||||
}
|
||||
return Ok(ExitStatus::Failure);
|
||||
}
|
||||
|
||||
// Detect the current Python interpreter.
|
||||
let platform = Platform::current()?;
|
||||
let venv = if let Some(python) = python {
|
||||
PythonEnvironment::from_requested_python(python, &platform, cache)?
|
||||
} else if system {
|
||||
PythonEnvironment::from_default_python(&platform, cache)?
|
||||
} else {
|
||||
match PythonEnvironment::from_virtualenv(platform.clone(), cache) {
|
||||
Ok(venv) => venv,
|
||||
Err(uv_interpreter::Error::VenvNotFound) => {
|
||||
PythonEnvironment::from_default_python(&platform, cache)?
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
};
|
||||
|
||||
debug!(
|
||||
"Using Python {} environment at {}",
|
||||
venv.interpreter().python_version(),
|
||||
venv.python_executable().simplified_display().cyan()
|
||||
);
|
||||
|
||||
// Build the installed index.
|
||||
let site_packages = SitePackages::from_executable(&venv)?;
|
||||
|
||||
// Sort and deduplicate the packages, which are keyed by name.
|
||||
packages.sort_unstable();
|
||||
packages.dedup();
|
||||
|
||||
// Map to the local distributions.
|
||||
let distributions = {
|
||||
let mut distributions = Vec::with_capacity(packages.len());
|
||||
|
||||
// Identify all packages that are installed.
|
||||
for package in &packages {
|
||||
let installed = site_packages.get_packages(package);
|
||||
if installed.is_empty() {
|
||||
writeln!(
|
||||
printer,
|
||||
"{}{} Package(s) not found for: {}",
|
||||
"warning".yellow().bold(),
|
||||
":".bold(),
|
||||
package.as_ref().bold()
|
||||
)?;
|
||||
} else {
|
||||
distributions.extend(installed);
|
||||
}
|
||||
}
|
||||
|
||||
distributions
|
||||
};
|
||||
|
||||
// Like `pip`, if no packages were found, return a failure.
|
||||
if distributions.is_empty() {
|
||||
return Ok(ExitStatus::Failure);
|
||||
}
|
||||
|
||||
if !quiet {
|
||||
// Print the information for each package.
|
||||
let mut first = true;
|
||||
for distribution in &distributions {
|
||||
if first {
|
||||
first = false;
|
||||
} else {
|
||||
// Print a separator between packages.
|
||||
#[allow(clippy::print_stdout)]
|
||||
{
|
||||
println!("---");
|
||||
}
|
||||
}
|
||||
|
||||
// Print the name, version, and location (e.g., the `site-packages` directory).
|
||||
#[allow(clippy::print_stdout)]
|
||||
{
|
||||
println!("Name: {}", distribution.name());
|
||||
println!("Version: {}", distribution.version());
|
||||
println!(
|
||||
"Location: {}",
|
||||
distribution
|
||||
.path()
|
||||
.parent()
|
||||
.expect("package path is not root")
|
||||
.simplified_display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that the environment is consistent.
|
||||
if strict {
|
||||
for diagnostic in site_packages.diagnostics()? {
|
||||
writeln!(
|
||||
printer,
|
||||
"{}{} {}",
|
||||
"warning".yellow().bold(),
|
||||
":".bold(),
|
||||
diagnostic.message().bold()
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
|
@ -182,6 +182,8 @@ enum PipCommand {
|
|||
Freeze(PipFreezeArgs),
|
||||
/// Enumerate the installed packages in the current environment.
|
||||
List(PipListArgs),
|
||||
/// Show information about one or more installed packages.
|
||||
Show(PipShowArgs),
|
||||
}
|
||||
|
||||
/// Clap parser for the union of date and datetime
|
||||
|
@ -947,6 +949,44 @@ struct PipListArgs {
|
|||
system: bool,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
struct PipShowArgs {
|
||||
/// The package(s) to display.
|
||||
package: Vec<PackageName>,
|
||||
|
||||
/// Validate the virtual environment, to detect packages with missing dependencies or other
|
||||
/// issues.
|
||||
#[clap(long)]
|
||||
strict: bool,
|
||||
|
||||
/// The Python interpreter for which packages should be listed.
|
||||
///
|
||||
/// By default, `uv` lists packages in the currently activated virtual environment, or a virtual
|
||||
/// environment (`.venv`) located in the current working directory or any parent directory,
|
||||
/// falling back to the system Python if no virtual environment is found.
|
||||
///
|
||||
/// Supported formats:
|
||||
/// - `3.10` looks for an installed Python 3.10 using `py --list-paths` on Windows, or
|
||||
/// `python3.10` on Linux and macOS.
|
||||
/// - `python3.10` or `python.exe` looks for a binary with the given name in `PATH`.
|
||||
/// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path.
|
||||
#[clap(long, short, verbatim_doc_comment, conflicts_with = "system")]
|
||||
python: Option<String>,
|
||||
|
||||
/// List packages for the system Python.
|
||||
///
|
||||
/// By default, `uv` lists packages in the currently activated virtual environment, or a virtual
|
||||
/// environment (`.venv`) located in the current working directory or any parent directory,
|
||||
/// falling back to the system Python if no virtual environment is found. The `--system` option
|
||||
/// instructs `uv` to use the first Python found in the system `PATH`.
|
||||
///
|
||||
/// WARNING: `--system` is intended for use in continuous integration (CI) environments and
|
||||
/// should be used with caution.
|
||||
#[clap(long, conflicts_with = "python")]
|
||||
system: bool,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
struct VenvArgs {
|
||||
|
@ -1105,6 +1145,12 @@ async fn run() -> Result<ExitStatus> {
|
|||
ContextValue::String("uv pip list".to_string()),
|
||||
);
|
||||
}
|
||||
"show" => {
|
||||
err.insert(
|
||||
ContextKind::SuggestedSubcommand,
|
||||
ContextValue::String("uv pip show".to_string()),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -1433,6 +1479,17 @@ async fn run() -> Result<ExitStatus> {
|
|||
&cache,
|
||||
printer,
|
||||
),
|
||||
Commands::Pip(PipNamespace {
|
||||
command: PipCommand::Show(args),
|
||||
}) => commands::pip_show(
|
||||
args.package,
|
||||
args.strict,
|
||||
args.python.as_deref(),
|
||||
args.system,
|
||||
cli.quiet,
|
||||
&cache,
|
||||
printer,
|
||||
),
|
||||
Commands::Cache(CacheNamespace {
|
||||
command: CacheCommand::Clean(args),
|
||||
})
|
||||
|
|
384
crates/uv/tests/pip_show.rs
Normal file
384
crates/uv/tests/pip_show.rs
Normal file
|
@ -0,0 +1,384 @@
|
|||
use std::process::Command;
|
||||
|
||||
use anyhow::Result;
|
||||
use assert_cmd::prelude::*;
|
||||
use assert_fs::fixture::PathChild;
|
||||
use assert_fs::fixture::{FileTouch, FileWriteStr};
|
||||
use indoc::indoc;
|
||||
|
||||
use common::uv_snapshot;
|
||||
|
||||
use crate::common::{get_bin, TestContext, EXCLUDE_NEWER};
|
||||
|
||||
mod common;
|
||||
|
||||
/// Create a `pip install` command with options shared across scenarios.
|
||||
fn command(context: &TestContext) -> Command {
|
||||
let mut command = Command::new(get_bin());
|
||||
command
|
||||
.arg("pip")
|
||||
.arg("install")
|
||||
.arg("--cache-dir")
|
||||
.arg(context.cache_dir.path())
|
||||
.arg("--exclude-newer")
|
||||
.arg(EXCLUDE_NEWER)
|
||||
.env("VIRTUAL_ENV", context.venv.as_os_str())
|
||||
.current_dir(&context.temp_dir);
|
||||
command
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_empty() {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
uv_snapshot!(Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("show")
|
||||
.arg("--cache-dir")
|
||||
.arg(context.cache_dir.path())
|
||||
.env("VIRTUAL_ENV", context.venv.as_os_str())
|
||||
.current_dir(&context.temp_dir), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: Please provide a package name or names.
|
||||
"###
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_found_single_package() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||||
requirements_txt.touch()?;
|
||||
requirements_txt.write_str("MarkupSafe==2.1.3")?;
|
||||
|
||||
uv_snapshot!(command(&context)
|
||||
.arg("-r")
|
||||
.arg("requirements.txt")
|
||||
.arg("--strict"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
Downloaded 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ markupsafe==2.1.3
|
||||
"###
|
||||
);
|
||||
|
||||
context.assert_command("import markupsafe").success();
|
||||
let filters = [(
|
||||
r"Location:.*site-packages",
|
||||
"Location: [WORKSPACE_DIR]/site-packages",
|
||||
)]
|
||||
.to_vec();
|
||||
|
||||
uv_snapshot!(filters, Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("show")
|
||||
.arg("markupsafe")
|
||||
.arg("--cache-dir")
|
||||
.arg(context.cache_dir.path())
|
||||
.env("VIRTUAL_ENV", context.venv.as_os_str())
|
||||
.current_dir(&context.temp_dir), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Name: markupsafe
|
||||
Version: 2.1.3
|
||||
Location: [WORKSPACE_DIR]/site-packages
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_found_multiple_packages() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||||
requirements_txt.touch()?;
|
||||
requirements_txt.write_str(indoc! {r"
|
||||
MarkupSafe==2.1.3
|
||||
pip==21.3.1
|
||||
"
|
||||
})?;
|
||||
|
||||
uv_snapshot!(command(&context)
|
||||
.arg("-r")
|
||||
.arg("requirements.txt")
|
||||
.arg("--strict"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 2 packages in [TIME]
|
||||
Downloaded 2 packages in [TIME]
|
||||
Installed 2 packages in [TIME]
|
||||
+ markupsafe==2.1.3
|
||||
+ pip==21.3.1
|
||||
"###
|
||||
);
|
||||
|
||||
context.assert_command("import markupsafe").success();
|
||||
|
||||
// In addition to the standard filters, remove the temporary directory from the snapshot.
|
||||
let filters = [(
|
||||
r"Location:.*site-packages",
|
||||
"Location: [WORKSPACE_DIR]/site-packages",
|
||||
)]
|
||||
.to_vec();
|
||||
|
||||
uv_snapshot!(filters, Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("show")
|
||||
.arg("markupsafe")
|
||||
.arg("pip")
|
||||
.arg("--cache-dir")
|
||||
.arg(context.cache_dir.path())
|
||||
.env("VIRTUAL_ENV", context.venv.as_os_str())
|
||||
.current_dir(&context.temp_dir), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Name: markupsafe
|
||||
Version: 2.1.3
|
||||
Location: [WORKSPACE_DIR]/site-packages
|
||||
---
|
||||
Name: pip
|
||||
Version: 21.3.1
|
||||
Location: [WORKSPACE_DIR]/site-packages
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_found_one_out_of_two() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||||
requirements_txt.touch()?;
|
||||
requirements_txt.write_str(indoc! {r"
|
||||
MarkupSafe==2.1.3
|
||||
pip==21.3.1
|
||||
"
|
||||
})?;
|
||||
|
||||
uv_snapshot!(command(&context)
|
||||
.arg("-r")
|
||||
.arg("requirements.txt")
|
||||
.arg("--strict"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 2 packages in [TIME]
|
||||
Downloaded 2 packages in [TIME]
|
||||
Installed 2 packages in [TIME]
|
||||
+ markupsafe==2.1.3
|
||||
+ pip==21.3.1
|
||||
"###
|
||||
);
|
||||
|
||||
context.assert_command("import markupsafe").success();
|
||||
|
||||
// In addition to the standard filters, remove the temporary directory from the snapshot.
|
||||
let filters = [(
|
||||
r"Location:.*site-packages",
|
||||
"Location: [WORKSPACE_DIR]/site-packages",
|
||||
)]
|
||||
.to_vec();
|
||||
|
||||
uv_snapshot!(filters, Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("show")
|
||||
.arg("markupsafe")
|
||||
.arg("flask")
|
||||
.arg("--cache-dir")
|
||||
.arg(context.cache_dir.path())
|
||||
.env("VIRTUAL_ENV", context.venv.as_os_str())
|
||||
.current_dir(&context.temp_dir), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Name: markupsafe
|
||||
Version: 2.1.3
|
||||
Location: [WORKSPACE_DIR]/site-packages
|
||||
|
||||
----- stderr -----
|
||||
warning: Package(s) not found for: flask
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_found_one_out_of_two_quiet() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||||
requirements_txt.touch()?;
|
||||
requirements_txt.write_str(indoc! {r"
|
||||
MarkupSafe==2.1.3
|
||||
pip==21.3.1
|
||||
"
|
||||
})?;
|
||||
|
||||
uv_snapshot!(command(&context)
|
||||
.arg("-r")
|
||||
.arg("requirements.txt")
|
||||
.arg("--strict"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 2 packages in [TIME]
|
||||
Downloaded 2 packages in [TIME]
|
||||
Installed 2 packages in [TIME]
|
||||
+ markupsafe==2.1.3
|
||||
+ pip==21.3.1
|
||||
"###
|
||||
);
|
||||
|
||||
context.assert_command("import markupsafe").success();
|
||||
|
||||
// Flask isn't installed, but markupsafe is, so the command should succeed.
|
||||
uv_snapshot!(Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("show")
|
||||
.arg("markupsafe")
|
||||
.arg("flask")
|
||||
.arg("--quiet")
|
||||
.arg("--cache-dir")
|
||||
.arg(context.cache_dir.path())
|
||||
.env("VIRTUAL_ENV", context.venv.as_os_str())
|
||||
.current_dir(&context.temp_dir), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_empty_quiet() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||||
requirements_txt.touch()?;
|
||||
requirements_txt.write_str(indoc! {r"
|
||||
MarkupSafe==2.1.3
|
||||
pip==21.3.1
|
||||
"
|
||||
})?;
|
||||
|
||||
uv_snapshot!(command(&context)
|
||||
.arg("-r")
|
||||
.arg("requirements.txt")
|
||||
.arg("--strict"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 2 packages in [TIME]
|
||||
Downloaded 2 packages in [TIME]
|
||||
Installed 2 packages in [TIME]
|
||||
+ markupsafe==2.1.3
|
||||
+ pip==21.3.1
|
||||
"###
|
||||
);
|
||||
|
||||
context.assert_command("import markupsafe").success();
|
||||
|
||||
// Flask isn't installed, so the command should fail.
|
||||
uv_snapshot!(Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("show")
|
||||
.arg("flask")
|
||||
.arg("--quiet")
|
||||
.arg("--cache-dir")
|
||||
.arg(context.cache_dir.path())
|
||||
.env("VIRTUAL_ENV", context.venv.as_os_str())
|
||||
.current_dir(&context.temp_dir), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_editable() {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
// Install the editable package.
|
||||
Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("install")
|
||||
.arg("-e")
|
||||
.arg("../../scripts/editable-installs/poetry_editable")
|
||||
.arg("--strict")
|
||||
.arg("--cache-dir")
|
||||
.arg(context.cache_dir.path())
|
||||
.arg("--exclude-newer")
|
||||
.arg(EXCLUDE_NEWER)
|
||||
.env("VIRTUAL_ENV", context.venv.as_os_str())
|
||||
.env(
|
||||
"CARGO_TARGET_DIR",
|
||||
"../../../target/target_install_editable",
|
||||
)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// In addition to the standard filters, remove the temporary directory from the snapshot.
|
||||
let filters = [(
|
||||
r"Location:.*site-packages",
|
||||
"Location: [WORKSPACE_DIR]/site-packages",
|
||||
)]
|
||||
.to_vec();
|
||||
|
||||
uv_snapshot!(filters, Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("show")
|
||||
.arg("poetry-editable")
|
||||
.arg("--cache-dir")
|
||||
.arg(context.cache_dir.path())
|
||||
.env("VIRTUAL_ENV", context.venv.as_os_str())
|
||||
.current_dir(&context.temp_dir), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Name: poetry-editable
|
||||
Version: 0.1.0
|
||||
Location: [WORKSPACE_DIR]/site-packages
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue