use serde::Serialize;
use std::collections::BTreeSet;
use std::fmt::Write;
use uv_cli::PythonListFormat;
use uv_pep440::Version;
use uv_preview::Preview;
use anyhow::Result;
use itertools::Either;
use owo_colors::OwoColorize;
use rustc_hash::FxHashSet;
use uv_cache::Cache;
use uv_client::BaseClientBuilder;
use uv_fs::Simplified;
use uv_python::downloads::{
Error as PythonDownloadError, ManagedPythonDownloadList, PythonDownloadRequest,
};
use uv_python::{
DiscoveryError, EnvironmentPreference, PythonDownloads, PythonInstallation, PythonNotFound,
PythonPreference, PythonRequest, PythonSource, find_python_installations,
};
use crate::commands::ExitStatus;
use crate::printer::Printer;
use crate::settings::PythonListKinds;
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)]
enum Kind {
Download,
Managed,
System,
}
#[derive(Debug, Serialize)]
struct NamedVersionParts {
major: u64,
minor: u64,
patch: u64,
}
#[derive(Debug, Serialize)]
struct PrintData {
key: String,
version: Version,
version_parts: NamedVersionParts,
path: Option<String>,
symlink: Option<String>,
url: Option<String>,
os: String,
variant: String,
implementation: String,
arch: String,
libc: String,
}
#[expect(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
pub(crate) async fn list(
request: Option<String>,
kinds: PythonListKinds,
all_versions: bool,
all_platforms: bool,
all_arches: bool,
show_urls: bool,
output_format: PythonListFormat,
python_downloads_json_url: Option<String>,
python_install_mirror: Option<String>,
pypy_install_mirror: Option<String>,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
client_builder: &BaseClientBuilder<'_>,
cache: &Cache,
printer: Printer,
preview: Preview,
) -> Result<ExitStatus> {
let request = request.as_deref().map(PythonRequest::parse);
let base_download_request = if python_preference == PythonPreference::OnlySystem {
None
} else {
PythonDownloadRequest::from_request(request.as_ref().unwrap_or(&PythonRequest::Any))
};
let client = client_builder.build()?;
let download_list =
ManagedPythonDownloadList::new(&client, python_downloads_json_url.as_deref()).await?;
let mut output = BTreeSet::new();
if let Some(base_download_request) = base_download_request {
let download_request = match kinds {
PythonListKinds::Installed => None,
PythonListKinds::Downloads => Some(if all_platforms {
base_download_request
} else if all_arches {
base_download_request.fill_platform()?.with_any_arch()
} else {
base_download_request.fill_platform()?
}),
PythonListKinds::Default => {
if python_downloads.is_automatic() {
Some(if all_platforms {
base_download_request
} else if all_arches {
base_download_request.fill_platform()?.with_any_arch()
} else {
base_download_request.fill_platform()?
})
} else {
None
}
}
}
.map(|request| request.with_prereleases(true));
let downloads = download_request
.as_ref()
.map(|request| download_list.iter_matching(request))
.into_iter()
.flatten()
.filter(|download| !download.key().variant().is_debug());
for download in downloads {
output.insert((
download.key().clone(),
Kind::Download,
Either::Right(
download
.download_urls(
python_install_mirror.as_deref(),
pypy_install_mirror.as_deref(),
)?
.into_iter()
.next()
.ok_or(PythonDownloadError::NoPythonDownloadUrlFound)?,
),
));
}
}
let installed =
match kinds {
PythonListKinds::Installed | PythonListKinds::Default => {
let discovery_preference = if python_preference == PythonPreference::OnlyManaged {
PythonPreference::Managed
} else {
python_preference
};
Some(find_python_installations(
request.as_ref().unwrap_or(&PythonRequest::Any),
EnvironmentPreference::OnlySystem,
discovery_preference,
cache,
preview,
)
.filter(|result| {
result
.as_ref()
.err()
.is_none_or(DiscoveryError::is_critical)
})
.collect::<Result<Vec<Result<PythonInstallation, PythonNotFound>>, DiscoveryError>>()?
.into_iter()
.filter_map(Result::ok)
.filter(|installation| python_preference.allows_installation(installation)))
}
PythonListKinds::Downloads => None,
};
if let Some(installed) = installed {
for installation in installed {
let kind = if matches!(installation.source(), PythonSource::Managed) {
Kind::Managed
} else {
Kind::System
};
output.insert((
installation.key(),
kind,
Either::Left(installation.interpreter().real_executable().to_path_buf()),
));
}
}
let mut seen_minor = FxHashSet::default();
let mut seen_patch = FxHashSet::default();
let mut seen_paths = FxHashSet::default();
let mut include = Vec::new();
for (key, kind, uri) in output.iter().rev() {
if let Either::Left(path) = uri {
if !seen_paths.insert(path) {
continue;
}
}
if !matches!(kind, Kind::System) {
if let [major, minor, ..] = *key.version().release() {
if !seen_minor.insert((
all_platforms.then_some(*key.os()),
major,
minor,
key.variant(),
key.implementation(),
all_arches.then_some(*key.arch()),
*key.libc(),
)) {
if matches!(kind, Kind::Download) && !all_versions {
continue;
}
}
}
if let [major, minor, patch] = *key.version().release() {
if !seen_patch.insert((
all_platforms.then_some(*key.os()),
major,
minor,
patch,
key.variant(),
key.implementation(),
all_arches.then_some(*key.arch()),
key.libc(),
)) {
if matches!(kind, Kind::Download) {
continue;
}
}
}
}
include.push((key, uri));
}
match output_format {
PythonListFormat::Json => {
let data = include
.iter()
.map(|(key, uri)| -> Result<_> {
let mut path_or_none: Option<String> = None;
let mut symlink_or_none: Option<String> = None;
let mut url_or_none: Option<String> = None;
match uri {
Either::Left(path) => {
path_or_none = Some(path.user_display().to_string());
let is_symlink = fs_err::symlink_metadata(path)?.is_symlink();
if is_symlink {
symlink_or_none =
Some(path.read_link()?.user_display().to_string());
}
}
Either::Right(url) => {
url_or_none = Some((*url).to_string());
}
}
let version = key.version();
let release = version.release();
Ok(PrintData {
key: key.to_string(),
version: version.version().clone(),
#[expect(clippy::get_first)]
version_parts: NamedVersionParts {
major: release.get(0).copied().unwrap_or(0),
minor: release.get(1).copied().unwrap_or(0),
patch: release.get(2).copied().unwrap_or(0),
},
path: path_or_none,
symlink: symlink_or_none,
url: url_or_none,
arch: key.arch().to_string(),
implementation: key.implementation().to_string(),
os: key.os().to_string(),
variant: key.variant().to_string(),
libc: key.libc().to_string(),
})
})
.collect::<Result<Vec<_>>>()?;
writeln!(printer.stdout(), "{}", serde_json::to_string(&data)?)?;
}
PythonListFormat::Text => {
let width = include
.iter()
.fold(0usize, |acc, (key, _)| acc.max(key.to_string().len()));
for (key, uri) in include {
let key = key.to_string();
match uri {
Either::Left(path) => {
let is_symlink = fs_err::symlink_metadata(path)?.is_symlink();
if is_symlink {
writeln!(
printer.stdout(),
"{key:width$} {} -> {}",
path.user_display().cyan(),
path.read_link()?.user_display().cyan()
)?;
} else {
writeln!(
printer.stdout(),
"{key:width$} {}",
path.user_display().cyan()
)?;
}
}
Either::Right(url) => {
if show_urls {
writeln!(printer.stdout(), "{key:width$} {}", url.dimmed())?;
} else {
writeln!(
printer.stdout(),
"{key:width$} {}",
"<download available>".dimmed()
)?;
}
}
}
}
}
}
Ok(ExitStatus::Success)
}