Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Print dependencies in uv pip list. #10886

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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,
Comment on lines +1981 to +1982
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the semantics you've implemented this seems like this should also include conflicts_with = "required_by".


#[arg(long, overrides_with("requires"), hide = true)]
pub no_requires: bool,
Comment on lines +1984 to +1985
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm what's the usecase for having this opt out flag too?


/// 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"))]
89 changes: 89 additions & 0 deletions crates/uv/src/commands/pip/list.rs
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to structure this as

let requires_map = requires.then(|| { ... });
let required_by_map = required_by.then(|| { ... });

As-is it's weird to have this shared variable for two conceptually independent results just because they happen to have the same type. It's also not clear to me that these need to be mutually exclusive outputs? (And if that's the case, ignore the note above about the cli conflicts_with)

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<FileType>,
#[serde(skip_serializing_if = "Option::is_none")]
editable_project_location: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
requires: Option<Vec<Require>>,
#[serde(skip_serializing_if = "Option::is_none")]
required_by: Option<Vec<RequiredBy>>,
}

/// A column in a table.
2 changes: 2 additions & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
@@ -649,6 +649,8 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
&args.exclude,
&args.format,
args.outdated,
args.requires,
args.required_by,
args.settings.prerelease,
args.settings.index_locations,
args.settings.index_strategy,
8 changes: 8 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
@@ -1960,6 +1960,8 @@ pub(crate) struct PipListSettings {
pub(crate) exclude: Vec<PackageName>,
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),