-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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, | ||
|
||
#[arg(long, overrides_with("requires"), hide = true)] | ||
pub no_requires: bool, | ||
Comment on lines
+1984
to
+1985
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"))] | ||
|
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer to structure this as
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. | ||
|
There was a problem hiding this comment.
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"
.