mirror of https://github.com/astral-sh/uv
Support for `--format=freeze` and `--format=json` in `uv pip list` (#1998)
Implements `pip list --format=freeze` and `pip list --format=json` Closes https://github.com/astral-sh/uv/issues/1970 ## Test Plan Extended existing `pip list` tests to match output. Need to look at escaping in the Windows test 🪟
This commit is contained in:
parent
df06069922
commit
190a161cc5
|
|
@ -93,6 +93,18 @@ pub(crate) enum VersionFormat {
|
|||
Json,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, clap::ValueEnum)]
|
||||
pub(crate) enum ListFormat {
|
||||
/// Display the list of packages in a human-readable table.
|
||||
#[default]
|
||||
Columns,
|
||||
/// Display the list of packages in a `pip freeze`-like format, with one package per line
|
||||
/// alongside its version.
|
||||
Freeze,
|
||||
/// Display the list of packages in a machine-readable JSON format.
|
||||
Json,
|
||||
}
|
||||
|
||||
/// Compile all Python source files in site-packages to bytecode, to speed up the
|
||||
/// initial run of any subsequent executions.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@ use anstream::println;
|
|||
use anyhow::Result;
|
||||
use itertools::Itertools;
|
||||
use owo_colors::OwoColorize;
|
||||
use serde::Serialize;
|
||||
use tracing::debug;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use distribution_types::Name;
|
||||
use distribution_types::{InstalledDist, Name};
|
||||
use platform_host::Platform;
|
||||
use uv_cache::Cache;
|
||||
use uv_fs::Simplified;
|
||||
|
|
@ -19,6 +20,8 @@ use uv_normalize::PackageName;
|
|||
use crate::commands::ExitStatus;
|
||||
use crate::printer::Printer;
|
||||
|
||||
use super::ListFormat;
|
||||
|
||||
/// Enumerate the installed packages in the current environment.
|
||||
#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
|
||||
pub(crate) fn pip_list(
|
||||
|
|
@ -26,6 +29,7 @@ pub(crate) fn pip_list(
|
|||
editable: bool,
|
||||
exclude_editable: bool,
|
||||
exclude: &[PackageName],
|
||||
format: &ListFormat,
|
||||
python: Option<&str>,
|
||||
system: bool,
|
||||
cache: &Cache,
|
||||
|
|
@ -67,45 +71,63 @@ pub(crate) fn pip_list(
|
|||
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(),
|
||||
},
|
||||
];
|
||||
match format {
|
||||
ListFormat::Columns => {
|
||||
// The package name and version are always present.
|
||||
let mut columns = vec![
|
||||
Column {
|
||||
header: String::from("Package"),
|
||||
rows: results
|
||||
.iter()
|
||||
.copied()
|
||||
.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 results.iter().any(|f| f.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(),
|
||||
});
|
||||
}
|
||||
// Editable column is only displayed if at least one editable package is found.
|
||||
if results.iter().copied().any(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(" "));
|
||||
for elems in Multizip(columns.iter().map(Column::fmt_padded).collect_vec()) {
|
||||
println!("{}", elems.join(" "));
|
||||
}
|
||||
}
|
||||
ListFormat::Json => {
|
||||
let rows = results.iter().copied().map(Row::from).collect_vec();
|
||||
let output = serde_json::to_string(&rows)?;
|
||||
println!("{output}");
|
||||
}
|
||||
ListFormat::Freeze => {
|
||||
for dist in &results {
|
||||
println!("{}=={}", dist.name().bold(), dist.version());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that the environment is consistent.
|
||||
|
|
@ -124,6 +146,26 @@ pub(crate) fn pip_list(
|
|||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct Row {
|
||||
name: String,
|
||||
version: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
editable_project_location: Option<String>,
|
||||
}
|
||||
|
||||
impl From<&InstalledDist> for Row {
|
||||
fn from(dist: &InstalledDist) -> Self {
|
||||
Self {
|
||||
name: dist.name().to_string(),
|
||||
version: dist.version().to_string(),
|
||||
editable_project_location: dist
|
||||
.as_editable()
|
||||
.map(|url| url.to_file_path().unwrap().simplified_display().to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Column {
|
||||
/// The header of the column.
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ use uv_traits::{
|
|||
ConfigSettingEntry, ConfigSettings, NoBuild, PackageNameSpecifier, SetupPyStrategy,
|
||||
};
|
||||
|
||||
use crate::commands::{extra_name_with_clap_error, ExitStatus, Upgrade, VersionFormat};
|
||||
use crate::commands::{extra_name_with_clap_error, ExitStatus, ListFormat, Upgrade, VersionFormat};
|
||||
use crate::compat::CompatArgs;
|
||||
use crate::requirements::RequirementsSource;
|
||||
|
||||
|
|
@ -916,6 +916,10 @@ struct PipListArgs {
|
|||
#[clap(long)]
|
||||
r#exclude: Vec<PackageName>,
|
||||
|
||||
/// Select the output format between: `columns` (default), `freeze`, or `json`.
|
||||
#[clap(long, value_enum, default_value_t = ListFormat::default())]
|
||||
format: ListFormat,
|
||||
|
||||
/// The Python interpreter for which packages should be listed.
|
||||
///
|
||||
/// By default, `uv` lists packages in the currently activated virtual environment, or a virtual
|
||||
|
|
@ -1423,6 +1427,7 @@ async fn run() -> Result<ExitStatus> {
|
|||
args.editable,
|
||||
args.exclude_editable,
|
||||
&args.exclude,
|
||||
&args.format,
|
||||
args.python.as_deref(),
|
||||
args.system,
|
||||
&cache,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ fn command(context: &TestContext) -> Command {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
fn list_empty() {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
uv_snapshot!(Command::new(get_bin())
|
||||
|
|
@ -47,7 +47,7 @@ fn empty() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn single_no_editable() -> Result<()> {
|
||||
fn list_single_no_editable() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||||
|
|
@ -94,7 +94,7 @@ fn single_no_editable() -> Result<()> {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn editable() -> Result<()> {
|
||||
fn list_editable() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let current_dir = std::env::current_dir()?;
|
||||
|
|
@ -196,7 +196,7 @@ fn editable() -> Result<()> {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn editable_only() -> Result<()> {
|
||||
fn list_editable_only() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let current_dir = std::env::current_dir()?;
|
||||
|
|
@ -327,7 +327,7 @@ fn editable_only() -> Result<()> {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn exclude() -> Result<()> {
|
||||
fn list_exclude() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let current_dir = std::env::current_dir()?;
|
||||
|
|
@ -460,3 +460,306 @@ fn exclude() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn list_format_json() -> 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)
|
||||
"###
|
||||
);
|
||||
|
||||
let workspace_dir = regex::escape(
|
||||
current_dir
|
||||
.join("..")
|
||||
.join("..")
|
||||
.canonicalize()?
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let workspace_len_difference = workspace_dir.as_str().len() + 32 - 16;
|
||||
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();
|
||||
let search_workspace_escaped = search_workspace.replace('/', "\\\\");
|
||||
let replace_workspace = "[WORKSPACE_DIR]";
|
||||
|
||||
let filters: Vec<_> = [
|
||||
(search_workspace, replace_workspace),
|
||||
(search_workspace_escaped.as_str(), 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()),
|
||||
]
|
||||
.into_iter()
|
||||
.chain(INSTA_FILTERS.to_vec())
|
||||
.collect();
|
||||
|
||||
uv_snapshot!(filters, Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("list")
|
||||
.arg("--format=json")
|
||||
.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":"numpy","version":"1.26.2"},{"name":"poetry-editable","version":"0.1.0","editable_project_location":"[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable"}]
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
|
||||
uv_snapshot!(filters, Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("list")
|
||||
.arg("--format=json")
|
||||
.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 -----
|
||||
[{"name":"poetry-editable","version":"0.1.0","editable_project_location":"[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable"}]
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
|
||||
uv_snapshot!(filters, Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("list")
|
||||
.arg("--format=json")
|
||||
.arg("--exclude-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":"numpy","version":"1.26.2"}]
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
|
||||
uv_snapshot!(filters, Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("list")
|
||||
.arg("--format=json")
|
||||
.arg("--editable")
|
||||
.arg("--exclude-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 -----
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_format_freeze() -> 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("--format=freeze")
|
||||
.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 -----
|
||||
numpy==1.26.2
|
||||
poetry-editable==0.1.0
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
|
||||
uv_snapshot!(filters, Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("list")
|
||||
.arg("--format=freeze")
|
||||
.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 -----
|
||||
poetry-editable==0.1.0
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
|
||||
uv_snapshot!(filters, Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("list")
|
||||
.arg("--format=freeze")
|
||||
.arg("--exclude-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 -----
|
||||
numpy==1.26.2
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
|
||||
uv_snapshot!(filters, Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("list")
|
||||
.arg("--format=freeze")
|
||||
.arg("--editable")
|
||||
.arg("--exclude-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 -----
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue