mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 10:58:28 +00:00
uv pip list
(#1662)
Hi, love your work on `uv` 👋! Opening a Draft PR early to check if there are any existing rust table formatting libs that I am unaware of (either already in `uv`/`ruff`, or the rust ecosystem) before spending much time on inventing the wheel myself and cleaning it up. Any other pointers are also welcome (e.g. on the editable filtering). Editable project locations in `uv pip list` include the file scheme (`file://`), where they are omitted in `pip list`. Is this desired, or should it replicate pip? ## Summary Implementation for #1401 `--editable` flag is implemented. `--outdated` and `--uptodate` out of scope for this PR (requires latest version information, and type wheel/sdist) ## Test Plan Not yet implemented as I couldn't locate the tests for `uv pip freeze`. We can compare to `pip` in `scripts/compare_with_pip/compare_with_pip.py`?
This commit is contained in:
parent
8d721830db
commit
0f1377bb08
7 changed files with 483 additions and 0 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -4183,6 +4183,7 @@ dependencies = [
|
|||
"tracing-durations-export",
|
||||
"tracing-subscriber",
|
||||
"tracing-tree",
|
||||
"unicode-width",
|
||||
"url",
|
||||
"uv-build",
|
||||
"uv-cache",
|
||||
|
|
|
@ -132,6 +132,14 @@ impl InstalledDist {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return true if the distribution is editable.
|
||||
pub fn is_editable(&self) -> bool {
|
||||
match self {
|
||||
Self::Registry(_) => false,
|
||||
Self::Url(dist) => dist.editable,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`Url`] of the distribution, if it is editable.
|
||||
pub fn as_editable(&self) -> Option<&Url> {
|
||||
match self {
|
||||
|
|
|
@ -69,6 +69,7 @@ tracing-subscriber = { workspace = true }
|
|||
tracing-tree = { workspace = true }
|
||||
url = { workspace = true }
|
||||
which = { workspace = true }
|
||||
unicode-width = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
mimalloc = "0.1.39"
|
||||
|
|
|
@ -7,6 +7,7 @@ use distribution_types::InstalledMetadata;
|
|||
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_sync::pip_sync;
|
||||
pub(crate) use pip_uninstall::pip_uninstall;
|
||||
pub(crate) use venv::venv;
|
||||
|
@ -17,6 +18,7 @@ mod cache_dir;
|
|||
mod pip_compile;
|
||||
mod pip_freeze;
|
||||
mod pip_install;
|
||||
mod pip_list;
|
||||
mod pip_sync;
|
||||
mod pip_uninstall;
|
||||
mod reporters;
|
||||
|
|
156
crates/uv/src/commands/pip_list.rs
Normal file
156
crates/uv/src/commands/pip_list.rs
Normal file
|
@ -0,0 +1,156 @@
|
|||
use std::cmp::max;
|
||||
use std::fmt::Write;
|
||||
|
||||
use anstream::println;
|
||||
use anyhow::Result;
|
||||
use itertools::Itertools;
|
||||
use owo_colors::OwoColorize;
|
||||
use tracing::debug;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use distribution_types::Name;
|
||||
use platform_host::Platform;
|
||||
use uv_cache::Cache;
|
||||
use uv_fs::Normalized;
|
||||
use uv_installer::SitePackages;
|
||||
use uv_interpreter::Virtualenv;
|
||||
|
||||
use crate::commands::ExitStatus;
|
||||
use crate::printer::Printer;
|
||||
|
||||
/// Enumerate the installed packages in the current environment.
|
||||
pub(crate) fn pip_list(
|
||||
cache: &Cache,
|
||||
strict: bool,
|
||||
editable: bool,
|
||||
mut printer: Printer,
|
||||
) -> Result<ExitStatus> {
|
||||
// Detect the current Python interpreter.
|
||||
let platform = Platform::current()?;
|
||||
let venv = Virtualenv::from_env(platform, cache)?;
|
||||
|
||||
debug!(
|
||||
"Using Python {} environment at {}",
|
||||
venv.interpreter().python_version(),
|
||||
venv.python_executable().normalized_display().cyan()
|
||||
);
|
||||
|
||||
// Build the installed index.
|
||||
let site_packages = SitePackages::from_executable(&venv)?;
|
||||
|
||||
// Filter if `--editable` is specified; always sort by name.
|
||||
let results = site_packages
|
||||
.iter()
|
||||
.filter(|f| !editable || f.is_editable())
|
||||
.sorted_unstable_by(|a, b| a.name().cmp(b.name()).then(a.version().cmp(b.version())))
|
||||
.collect_vec();
|
||||
if results.is_empty() {
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
// The package name and version are always present.
|
||||
let mut columns = vec![
|
||||
Column {
|
||||
header: String::from("Package"),
|
||||
rows: results.iter().map(|f| f.name().to_string()).collect_vec(),
|
||||
},
|
||||
Column {
|
||||
header: String::from("Version"),
|
||||
rows: results
|
||||
.iter()
|
||||
.map(|f| f.version().to_string())
|
||||
.collect_vec(),
|
||||
},
|
||||
];
|
||||
|
||||
// Editable column is only displayed if at least one editable package is found.
|
||||
if site_packages
|
||||
.iter()
|
||||
.any(distribution_types::InstalledDist::is_editable)
|
||||
{
|
||||
columns.push(Column {
|
||||
header: String::from("Editable project location"),
|
||||
rows: results
|
||||
.iter()
|
||||
.map(|f| f.as_editable())
|
||||
.map(|e| {
|
||||
if let Some(url) = e {
|
||||
url.to_file_path()
|
||||
.unwrap()
|
||||
.into_os_string()
|
||||
.into_string()
|
||||
.unwrap()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
})
|
||||
.collect_vec(),
|
||||
});
|
||||
}
|
||||
|
||||
for elems in Multizip(columns.iter().map(Column::fmt_padded).collect_vec()) {
|
||||
println!("{0}", elems.join(" "));
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Column {
|
||||
/// The header of the column.
|
||||
header: String,
|
||||
/// The rows of the column.
|
||||
rows: Vec<String>,
|
||||
}
|
||||
|
||||
impl<'a> Column {
|
||||
/// Return the width of the column.
|
||||
fn max_width(&self) -> usize {
|
||||
max(
|
||||
self.header.width(),
|
||||
self.rows.iter().map(|f| f.width()).max().unwrap_or(0),
|
||||
)
|
||||
}
|
||||
|
||||
/// Return an iterator of the column, with the header and rows formatted to the maximum width.
|
||||
fn fmt_padded(&'a self) -> impl Iterator<Item = String> + 'a {
|
||||
let max_width = self.max_width();
|
||||
let header = vec![
|
||||
format!("{0:width$}", self.header, width = max_width),
|
||||
format!("{:-^width$}", "", width = max_width),
|
||||
];
|
||||
|
||||
header
|
||||
.into_iter()
|
||||
.chain(self.rows.iter().map(move |f| format!("{f:max_width$}")))
|
||||
}
|
||||
}
|
||||
|
||||
/// Zip an unknown number of iterators.
|
||||
/// Combination of [`itertools::multizip`] and [`itertools::izip`].
|
||||
#[derive(Debug)]
|
||||
struct Multizip<T>(Vec<T>);
|
||||
|
||||
impl<T> Iterator for Multizip<T>
|
||||
where
|
||||
T: Iterator,
|
||||
{
|
||||
type Item = Vec<T::Item>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.0.iter_mut().map(Iterator::next).collect()
|
||||
}
|
||||
}
|
|
@ -179,6 +179,8 @@ enum PipCommand {
|
|||
Uninstall(PipUninstallArgs),
|
||||
/// Enumerate the installed packages in the current environment.
|
||||
Freeze(PipFreezeArgs),
|
||||
/// Enumerate the installed packages in the current environment.
|
||||
List(PipListArgs),
|
||||
}
|
||||
|
||||
/// Clap parser for the union of date and datetime
|
||||
|
@ -685,6 +687,19 @@ struct PipFreezeArgs {
|
|||
strict: bool,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
struct PipListArgs {
|
||||
/// Validate the virtual environment, to detect packages with missing dependencies or other
|
||||
/// issues.
|
||||
#[clap(long)]
|
||||
strict: bool,
|
||||
|
||||
/// List editable projects.
|
||||
#[clap(short, long)]
|
||||
editable: bool,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
struct VenvArgs {
|
||||
|
@ -802,6 +817,12 @@ async fn run() -> Result<ExitStatus> {
|
|||
ContextValue::String("uv pip freeze".to_string()),
|
||||
);
|
||||
}
|
||||
"list" => {
|
||||
err.insert(
|
||||
ContextKind::SuggestedSubcommand,
|
||||
ContextValue::String("uv pip list".to_string()),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -1086,6 +1107,9 @@ async fn run() -> Result<ExitStatus> {
|
|||
Commands::Pip(PipNamespace {
|
||||
command: PipCommand::Freeze(args),
|
||||
}) => commands::pip_freeze(&cache, args.strict, printer),
|
||||
Commands::Pip(PipNamespace {
|
||||
command: PipCommand::List(args),
|
||||
}) => commands::pip_list(&cache, args.strict, args.editable, printer),
|
||||
Commands::Cache(CacheNamespace {
|
||||
command: CacheCommand::Clean(args),
|
||||
})
|
||||
|
|
291
crates/uv/tests/pip_list.rs
Normal file
291
crates/uv/tests/pip_list.rs
Normal file
|
@ -0,0 +1,291 @@
|
|||
use std::process::Command;
|
||||
|
||||
use anyhow::Result;
|
||||
use assert_fs::fixture::PathChild;
|
||||
use assert_fs::fixture::{FileTouch, FileWriteStr};
|
||||
use url::Url;
|
||||
|
||||
use common::uv_snapshot;
|
||||
|
||||
use crate::common::{get_bin, TestContext, EXCLUDE_NEWER, INSTA_FILTERS};
|
||||
|
||||
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 empty() {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
uv_snapshot!(Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("list")
|
||||
.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 -----
|
||||
"###
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_no_editable() -> 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();
|
||||
|
||||
uv_snapshot!(Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("list")
|
||||
.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 -----
|
||||
Package Version
|
||||
---------- -------
|
||||
markupsafe 2.1.3
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editable() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let current_dir = std::env::current_dir()?;
|
||||
let workspace_dir = regex::escape(
|
||||
Url::from_directory_path(current_dir.join("..").join("..").canonicalize()?)
|
||||
.unwrap()
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
let filters = [(workspace_dir.as_str(), "file://[WORKSPACE_DIR]/")]
|
||||
.into_iter()
|
||||
.chain(INSTA_FILTERS.to_vec())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Install the editable package.
|
||||
uv_snapshot!(filters, 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"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Built 1 editable in [TIME]
|
||||
Resolved 2 packages in [TIME]
|
||||
Downloaded 1 package in [TIME]
|
||||
Installed 2 packages in [TIME]
|
||||
+ numpy==1.26.2
|
||||
+ poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable)
|
||||
"###
|
||||
);
|
||||
|
||||
// Account for difference length workspace dir
|
||||
let prefix = if cfg!(windows) { "file:///" } else { "file://" };
|
||||
|
||||
// Origin of lengths used below:
|
||||
// - |Editable project location| = 25
|
||||
// - expected length = 57
|
||||
// - expected length - |Editable project location| = 32
|
||||
// - |`[WORKSPACE_DIR]/`| = 16
|
||||
// - |`file://`| = 7, |`file:///`| = 8 (windows)
|
||||
|
||||
let workspace_len_difference = workspace_dir.as_str().len() + 32 - 16 - prefix.len();
|
||||
let find_divider = "-".repeat(25 + workspace_len_difference);
|
||||
let replace_divider = "-".repeat(57);
|
||||
|
||||
let find_header = format!(
|
||||
"Editable project location{0}",
|
||||
" ".repeat(workspace_len_difference)
|
||||
);
|
||||
let replace_header = format!("Editable project location{0}", " ".repeat(32));
|
||||
|
||||
let find_whitespace = " ".repeat(25 + workspace_len_difference);
|
||||
let replace_whitespace = " ".repeat(57);
|
||||
|
||||
let search_workspace = workspace_dir.as_str().strip_prefix(prefix).unwrap();
|
||||
let replace_workspace = "[WORKSPACE_DIR]/";
|
||||
|
||||
let filters = INSTA_FILTERS
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(vec![
|
||||
(search_workspace, replace_workspace),
|
||||
(find_divider.as_str(), replace_divider.as_str()),
|
||||
(find_header.as_str(), replace_header.as_str()),
|
||||
(find_whitespace.as_str(), replace_whitespace.as_str()),
|
||||
])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
uv_snapshot!(filters, Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("list")
|
||||
.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 -----
|
||||
Package Version Editable project location
|
||||
--------------- ------- ---------------------------------------------------------
|
||||
numpy 1.26.2
|
||||
poetry-editable 0.1.0 [WORKSPACE_DIR]/scripts/editable-installs/poetry_editable
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editable_only() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let current_dir = std::env::current_dir()?;
|
||||
let workspace_dir = regex::escape(
|
||||
Url::from_directory_path(current_dir.join("..").join("..").canonicalize()?)
|
||||
.unwrap()
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
let filters = [(workspace_dir.as_str(), "file://[WORKSPACE_DIR]/")]
|
||||
.into_iter()
|
||||
.chain(INSTA_FILTERS.to_vec())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Install the editable package.
|
||||
uv_snapshot!(filters, 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"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Built 1 editable in [TIME]
|
||||
Resolved 2 packages in [TIME]
|
||||
Downloaded 1 package in [TIME]
|
||||
Installed 2 packages in [TIME]
|
||||
+ numpy==1.26.2
|
||||
+ poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable)
|
||||
"###
|
||||
);
|
||||
|
||||
// Account for difference length workspace dir
|
||||
let prefix = if cfg!(windows) { "file:///" } else { "file://" };
|
||||
|
||||
let workspace_len_difference = workspace_dir.as_str().len() + 32 - 16 - prefix.len();
|
||||
let find_divider = "-".repeat(25 + workspace_len_difference);
|
||||
let replace_divider = "-".repeat(57);
|
||||
|
||||
let find_header = format!(
|
||||
"Editable project location{0}",
|
||||
" ".repeat(workspace_len_difference)
|
||||
);
|
||||
let replace_header = format!("Editable project location{0}", " ".repeat(32));
|
||||
|
||||
let find_whitespace = " ".repeat(25 + workspace_len_difference);
|
||||
let replace_whitespace = " ".repeat(57);
|
||||
|
||||
let search_workspace = workspace_dir.as_str().strip_prefix(prefix).unwrap();
|
||||
let replace_workspace = "[WORKSPACE_DIR]/";
|
||||
|
||||
let filters = INSTA_FILTERS
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(vec![
|
||||
(search_workspace, replace_workspace),
|
||||
(find_divider.as_str(), replace_divider.as_str()),
|
||||
(find_header.as_str(), replace_header.as_str()),
|
||||
(find_whitespace.as_str(), replace_whitespace.as_str()),
|
||||
])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
uv_snapshot!(filters, Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("list")
|
||||
.arg("--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 -----
|
||||
Package Version Editable project location
|
||||
--------------- ------- ---------------------------------------------------------
|
||||
poetry-editable 0.1.0 [WORKSPACE_DIR]/scripts/editable-installs/poetry_editable
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue