use std::cmp::max;
use std::fmt::Write;
use anyhow::Result;
use futures::StreamExt;
use itertools::Itertools;
use owo_colors::OwoColorize;
use rustc_hash::{FxHashMap, FxHashSet};
use serde::Serialize;
use tracing::debug;
use unicode_width::UnicodeWidthStr;
use uv_cache::{Cache, Refresh};
use uv_cache_info::Timestamp;
use uv_cli::ListFormat;
use uv_client::{BaseClientBuilder, RegistryClientBuilder};
use uv_configuration::{Concurrency, IndexStrategy, KeyringProviderType};
use uv_distribution_filename::DistFilename;
use uv_distribution_types::{
DependencyMetadata, Diagnostic, IndexCapabilities, IndexLocations, InstalledDist, Name,
RequiresPython,
};
use uv_fs::Simplified;
use uv_installer::SitePackages;
use uv_normalize::PackageName;
use uv_pep440::Version;
use uv_preview::Preview;
use uv_python::PythonRequest;
use uv_python::{EnvironmentPreference, Prefix, PythonEnvironment, PythonPreference, Target};
use uv_resolver::{ExcludeNewer, PrereleaseMode};
use crate::commands::ExitStatus;
use crate::commands::pip::latest::LatestClient;
use crate::commands::pip::operations::report_target_environment;
use crate::commands::reporters::LatestVersionReporter;
use crate::printer::Printer;
pub(crate) async fn pip_list(
editable: Option<bool>,
exclude: &FxHashSet<PackageName>,
format: &ListFormat,
outdated: bool,
prerelease: PrereleaseMode,
index_locations: IndexLocations,
index_strategy: IndexStrategy,
keyring_provider: KeyringProviderType,
client_builder: &BaseClientBuilder<'_>,
concurrency: Concurrency,
strict: bool,
exclude_newer: ExcludeNewer,
dependency_metadata: &DependencyMetadata,
python: Option<&str>,
system: bool,
target: Option<Target>,
prefix: Option<Prefix>,
cache: &Cache,
printer: Printer,
preview: Preview,
) -> Result<ExitStatus> {
if outdated && matches!(format, ListFormat::Freeze) {
anyhow::bail!("`--outdated` cannot be used with `--format freeze`");
}
let environment = PythonEnvironment::find(
&python.map(PythonRequest::parse).unwrap_or_default(),
EnvironmentPreference::from_system_flag(system, false),
PythonPreference::default().with_system_flag(system),
cache,
preview,
)?;
let environment = if let Some(target) = target {
debug!(
"Using `--target` directory at {}",
target.root().user_display()
);
environment.with_target(target)?
} else if let Some(prefix) = prefix {
debug!(
"Using `--prefix` directory at {}",
prefix.root().user_display()
);
environment.with_prefix(prefix)?
} else {
environment
};
report_target_environment(&environment, cache, printer)?;
let site_packages = SitePackages::from_environment(&environment)?;
let results = site_packages
.iter()
.filter(|dist| editable.is_none() || editable == Some(dist.is_editable()))
.filter(|dist| !exclude.contains(dist.name()))
.sorted_unstable_by(|a, b| a.name().cmp(b.name()).then(a.version().cmp(b.version())))
.collect_vec();
let latest = if outdated && !results.is_empty() {
let capabilities = IndexCapabilities::default();
let client_builder = client_builder.clone().keyring(keyring_provider);
let latest_index_locations = index_locations.clone();
let client = RegistryClientBuilder::new(
client_builder,
cache.clone().with_refresh(Refresh::All(Timestamp::now())),
)
.index_locations(index_locations)
.index_strategy(index_strategy)
.markers(environment.interpreter().markers())
.platform(environment.interpreter().platform())
.build()?;
let download_concurrency = concurrency.downloads_semaphore.clone();
let interpreter = environment.interpreter();
let tags = interpreter.tags()?;
let requires_python =
RequiresPython::greater_than_equal_version(interpreter.python_full_version());
let client = LatestClient {
client: &client,
capabilities: &capabilities,
prerelease,
exclude_newer: &exclude_newer,
index_locations: &latest_index_locations,
tags: Some(tags),
requires_python: Some(&requires_python),
};
let reporter = LatestVersionReporter::from(printer).with_length(results.len() as u64);
let mut fetches = futures::stream::iter(&results)
.map(async |dist| {
let latest = client
.find_latest(dist.name(), None, &download_concurrency)
.await?;
Ok::<(&PackageName, Option<DistFilename>), uv_client::Error>((dist.name(), latest))
})
.buffer_unordered(concurrency.downloads);
let mut map = FxHashMap::default();
while let Some((package, version)) = fetches.next().await.transpose()? {
if let Some(version) = version.as_ref() {
reporter.on_fetch_version(package, version.version());
} else {
reporter.on_fetch_progress();
}
map.insert(package, version);
}
reporter.on_fetch_complete();
map
} else {
FxHashMap::default()
};
let results = if outdated {
results
.into_iter()
.filter(|dist| {
latest[dist.name()]
.as_ref()
.is_some_and(|filename| filename.version() > dist.version())
})
.collect_vec()
} else {
results
};
match format {
ListFormat::Json => {
let rows = results
.iter()
.copied()
.map(|dist| Entry {
name: dist.name().clone(),
version: dist.version().clone(),
latest_version: latest
.get(dist.name())
.and_then(|filename| filename.as_ref())
.map(DistFilename::version)
.cloned(),
latest_filetype: latest
.get(dist.name())
.and_then(|filename| filename.as_ref())
.map(FileType::from),
editable_project_location: dist
.as_editable()
.map(|url| url.to_file_path().unwrap().simplified_display().to_string()),
})
.collect_vec();
let output = serde_json::to_string(&rows)?;
writeln!(printer.stdout_important(), "{output}")?;
}
ListFormat::Columns if results.is_empty() => {}
ListFormat::Columns => {
let mut columns = vec![
Column {
header: String::from("Package"),
rows: results
.iter()
.copied()
.map(|dist| dist.name().to_string())
.collect_vec(),
},
Column {
header: String::from("Version"),
rows: results
.iter()
.map(|dist| dist.version().to_string())
.collect_vec(),
},
];
if outdated {
columns.push(Column {
header: String::from("Latest"),
rows: results
.iter()
.map(|dist| {
latest
.get(dist.name())
.and_then(|filename| filename.as_ref())
.map(DistFilename::version)
.map(ToString::to_string)
.unwrap_or_default()
})
.collect_vec(),
});
columns.push(Column {
header: String::from("Type"),
rows: results
.iter()
.map(|dist| {
latest
.get(dist.name())
.and_then(|filename| filename.as_ref())
.map(FileType::from)
.as_ref()
.map(ToString::to_string)
.unwrap_or_default()
})
.collect_vec(),
});
}
if results.iter().copied().any(InstalledDist::is_editable) {
columns.push(Column {
header: String::from("Editable project location"),
rows: results
.iter()
.map(|dist| dist.as_editable())
.map(|url| {
url.map(|url| {
url.to_file_path().unwrap().simplified_display().to_string()
})
.unwrap_or_default()
})
.collect_vec(),
});
}
for elems in MultiZip(columns.iter().map(Column::fmt).collect_vec()) {
writeln!(printer.stdout_important(), "{}", elems.join(" ").trim_end())?;
}
}
ListFormat::Freeze if results.is_empty() => {}
ListFormat::Freeze => {
for dist in &results {
writeln!(
printer.stdout_important(),
"{}=={}",
dist.name().bold(),
dist.version()
)?;
}
}
}
if strict {
let markers = environment.interpreter().resolver_marker_environment();
let tags = environment.interpreter().tags()?;
for diagnostic in site_packages.diagnostics(&markers, tags, dependency_metadata)? {
writeln!(
printer.stderr(),
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
diagnostic.message().bold()
)?;
}
}
Ok(ExitStatus::Success)
}
#[derive(Debug)]
enum FileType {
Wheel,
SourceDistribution,
}
impl std::fmt::Display for FileType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Wheel => write!(f, "wheel"),
Self::SourceDistribution => write!(f, "sdist"),
}
}
}
impl Serialize for FileType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::Wheel => serializer.serialize_str("wheel"),
Self::SourceDistribution => serializer.serialize_str("sdist"),
}
}
}
impl From<&DistFilename> for FileType {
fn from(filename: &DistFilename) -> Self {
match filename {
DistFilename::WheelFilename(_) => Self::Wheel,
DistFilename::SourceDistFilename(_) => Self::SourceDistribution,
}
}
}
#[derive(Debug, Serialize)]
struct Entry {
name: PackageName,
version: Version,
#[serde(skip_serializing_if = "Option::is_none")]
latest_version: Option<Version>,
#[serde(skip_serializing_if = "Option::is_none")]
latest_filetype: Option<FileType>,
#[serde(skip_serializing_if = "Option::is_none")]
editable_project_location: Option<String>,
}
#[derive(Debug)]
struct Column {
header: String,
rows: Vec<String>,
}
impl<'a> Column {
fn max_width(&self) -> usize {
max(
self.header.width(),
self.rows.iter().map(|f| f.width()).max().unwrap_or(0),
)
}
fn fmt(&'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$}")))
}
}
#[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()
}
}