This commit is contained in:
Theodore Ni 2025-06-29 23:58:41 +02:00 committed by GitHub
commit 704633e8bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 117 additions and 0 deletions

View file

@ -2174,6 +2174,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"))]

View file

@ -42,6 +42,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,
@ -61,6 +63,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(),
@ -159,6 +169,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
@ -179,6 +230,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)?;
@ -324,6 +399,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 {
@ -335,6 +420,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.

View file

@ -836,6 +836,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,

View file

@ -2325,6 +2325,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,
}
@ -2338,6 +2340,10 @@ impl PipListSettings {
format,
outdated,
no_outdated,
requires,
no_requires,
required_by,
no_required_by,
strict,
no_strict,
fetch,
@ -2352,6 +2358,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),