From 668b3e9f226eae8cee0f7f8afc288d48f66bd6b2 Mon Sep 17 00:00:00 2001 From: Theodore Ni <3806110+tjni@users.noreply.github.com> Date: Wed, 22 Jan 2025 22:40:05 -0800 Subject: [PATCH] Print dependencies in uv pip list. This is controlled by the --requires and --required-by flags and only works when the output format is JSON. --- crates/uv-cli/src/lib.rs | 18 ++++++ crates/uv/src/commands/pip/list.rs | 89 ++++++++++++++++++++++++++++++ crates/uv/src/lib.rs | 2 + crates/uv/src/settings.rs | 8 +++ 4 files changed, 117 insertions(+) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index c10404456fe0..95eb74139b06 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -1975,6 +1975,24 @@ pub struct PipListArgs { #[arg(long, overrides_with("outdated"), hide = true)] pub no_outdated: bool, + /// List each package's required packages. + /// + /// This is only allowed when the output format is `json`. + #[arg(long, overrides_with("no_requires"))] + pub requires: bool, + + #[arg(long, overrides_with("requires"), hide = true)] + pub no_requires: bool, + + /// List which packages require each package. + /// + /// This is only allowed when the output format is `json`. + #[arg(long, overrides_with("no_required_by"))] + pub required_by: bool, + + #[arg(long, overrides_with("required_by"), hide = true)] + pub no_required_by: bool, + /// Validate the Python environment, to detect packages with missing dependencies and other /// issues. #[arg(long, overrides_with("no_strict"))] diff --git a/crates/uv/src/commands/pip/list.rs b/crates/uv/src/commands/pip/list.rs index 6ec866404dca..60ba4dd85026 100644 --- a/crates/uv/src/commands/pip/list.rs +++ b/crates/uv/src/commands/pip/list.rs @@ -38,6 +38,8 @@ pub(crate) async fn pip_list( exclude: &[PackageName], format: &ListFormat, outdated: bool, + requires: bool, + required_by: bool, prerelease: PrereleaseMode, index_locations: IndexLocations, index_strategy: IndexStrategy, @@ -58,6 +60,14 @@ pub(crate) async fn pip_list( anyhow::bail!("`--outdated` cannot be used with `--format freeze`"); } + if requires && !matches!(format, ListFormat::Json) { + anyhow::bail!("`--requires` can only be used with `--format json`"); + } + + if required_by && !matches!(format, ListFormat::Json) { + anyhow::bail!("`--required_by` can only be used with `--format json`"); + } + // Detect the current Python interpreter. let environment = PythonEnvironment::find( &python.map(PythonRequest::parse).unwrap_or_default(), @@ -150,6 +160,47 @@ pub(crate) async fn pip_list( results }; + let requires_map = if requires || required_by { + let mut requires_map = FxHashMap::default(); + + // Determine the markers to use for resolution. + let markers = environment.interpreter().resolver_marker_environment(); + + if required_by { + // To compute which packages require a given package, we need to + // consider every installed package. + for package in site_packages.iter() { + if let Ok(metadata) = package.metadata() { + let requires = metadata + .requires_dist + .into_iter() + .filter(|req| req.evaluate_markers(&markers, &[])) + .map(|req| req.name) + .collect_vec(); + + requires_map.insert(package.name(), requires); + } + } + } else { + for package in &results { + if let Ok(metadata) = package.metadata() { + let requires = metadata + .requires_dist + .into_iter() + .filter(|req| req.evaluate_markers(&markers, &[])) + .map(|req| req.name) + .collect_vec(); + + requires_map.insert(package.name(), requires); + } + } + } + + requires_map + } else { + FxHashMap::default() + }; + match format { ListFormat::Json => { let rows = results @@ -170,6 +221,30 @@ pub(crate) async fn pip_list( editable_project_location: dist .as_editable() .map(|url| url.to_file_path().unwrap().simplified_display().to_string()), + requires: requires.then(|| { + if let Some(packages) = requires_map.get(dist.name()) { + packages + .iter() + .map(|name| Require { name: name.clone() }) + .collect_vec() + } else { + vec![] + } + }), + required_by: required_by.then(|| { + requires_map + .iter() + .filter(|(name, pkgs)| { + **name != dist.name() && pkgs.iter().any(|pkg| pkg == dist.name()) + }) + .map(|(name, _)| name) + .sorted_unstable() + .dedup() + .map(|name| RequiredBy { + name: (*name).clone(), + }) + .collect_vec() + }), }) .collect_vec(); let output = serde_json::to_string(&rows)?; @@ -315,6 +390,16 @@ impl From<&DistFilename> for FileType { } } +#[derive(Debug, Serialize)] +struct Require { + name: PackageName, +} + +#[derive(Debug, Serialize)] +struct RequiredBy { + name: PackageName, +} + /// An entry in a JSON list of installed packages. #[derive(Debug, Serialize)] struct Entry { @@ -326,6 +411,10 @@ struct Entry { latest_filetype: Option, #[serde(skip_serializing_if = "Option::is_none")] editable_project_location: Option, + #[serde(skip_serializing_if = "Option::is_none")] + requires: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + required_by: Option>, } /// A column in a table. diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 3af174ca7d29..7b07f2da4106 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -649,6 +649,8 @@ async fn run(mut cli: Cli) -> Result { &args.exclude, &args.format, args.outdated, + args.requires, + args.required_by, args.settings.prerelease, args.settings.index_locations, args.settings.index_strategy, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 1e59e0f94ab5..4f3754697dc7 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -1960,6 +1960,8 @@ pub(crate) struct PipListSettings { pub(crate) exclude: Vec, pub(crate) format: ListFormat, pub(crate) outdated: bool, + pub(crate) requires: bool, + pub(crate) required_by: bool, pub(crate) settings: PipSettings, } @@ -1973,6 +1975,10 @@ impl PipListSettings { format, outdated, no_outdated, + requires, + no_requires, + required_by, + no_required_by, strict, no_strict, fetch, @@ -1987,6 +1993,8 @@ impl PipListSettings { exclude, format, outdated: flag(outdated, no_outdated).unwrap_or(false), + requires: flag(requires, no_requires).unwrap_or(false), + required_by: flag(required_by, no_required_by).unwrap_or(false), settings: PipSettings::combine( PipOptions { python: python.and_then(Maybe::into_option),