Implement "Requires" field in pip show (#2347)

## Summary
Follow-up for
395be442fc

adds `Requires` field to pip show output.

I've aimed to make it behave exactly the same as `pip` does for now, but
there seem to be subtle issues that may require some discussion going
forward:
- Should `uv pip show` support extras? `pip` has an open issue for it,
but currently does not support https://github.com/pypa/pip/issues/4824.
- Relatedly, `Requred-by` field (not implemented in this PR) in `pip
show` currently doesn't take the extras into account transparently, i.e.
when `PySocks` has been installed as an extra for `requests[socks]`,
`pip show PySocks` doesn't have `requests` or `requests[socks]` under
`Requred-by` field. Should `uv pip show` for now just replicate `pip`'s
behavior for now for simplicity and parity or try to cover the extras
for completeness?

## Test Plan
Added a couple of tests:
1. `requests==2.31.0` has four dependencies that would be ordered
differently unless sorted. Additionally, it has two dependencies that
are optionally included for extras.
2. `pandas==2.1.3` depends on different versions of `numpy` depending on
the python version used.
This commit is contained in:
Chan Kang 2024-03-12 00:35:22 -04:00 committed by GitHub
parent e9c16e9aa2
commit 9bb548d251
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 150 additions and 3 deletions

View file

@ -1,6 +1,8 @@
use std::collections::BTreeSet;
use std::fmt::Write; use std::fmt::Write;
use anyhow::Result; use anyhow::Result;
use itertools::Itertools;
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
use tracing::debug; use tracing::debug;
@ -62,6 +64,9 @@ pub(crate) fn pip_show(
// Build the installed index. // Build the installed index.
let site_packages = SitePackages::from_executable(&venv)?; let site_packages = SitePackages::from_executable(&venv)?;
// Determine the markers to use for resolution.
let markers = venv.interpreter().markers();
// Sort and deduplicate the packages, which are keyed by name. // Sort and deduplicate the packages, which are keyed by name.
packages.sort_unstable(); packages.sort_unstable();
packages.dedup(); packages.dedup();
@ -116,6 +121,25 @@ pub(crate) fn pip_show(
.expect("package path is not root") .expect("package path is not root")
.simplified_display() .simplified_display()
)?; )?;
// If available, print the requirements.
if let Ok(metadata) = distribution.metadata() {
let requires_dist = metadata
.requires_dist
.into_iter()
.filter(|req| req.evaluate_markers(markers, &[]))
.map(|req| req.name)
.collect::<BTreeSet<_>>();
if requires_dist.is_empty() {
writeln!(printer.stdout(), "Requires:")?;
} else {
writeln!(
printer.stdout(),
"Requires: {}",
requires_dist.into_iter().join(", ")
)?;
}
}
} }
// Validate that the environment is consistent. // Validate that the environment is consistent.

View file

@ -56,6 +56,124 @@ fn show_empty() {
); );
} }
#[test]
fn show_requires_multiple() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.touch()?;
requirements_txt.write_str("requests==2.31.0")?;
uv_snapshot!(install_command(&context)
.arg("-r")
.arg("requirements.txt")
.arg("--strict"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 5 packages in [TIME]
Downloaded 5 packages in [TIME]
Installed 5 packages in [TIME]
+ certifi==2023.11.17
+ charset-normalizer==3.3.2
+ idna==3.4
+ requests==2.31.0
+ urllib3==2.1.0
"###
);
context.assert_command("import requests").success();
let filters = [(
r"Location:.*site-packages",
"Location: [WORKSPACE_DIR]/site-packages",
)]
.to_vec();
// Guards against the package names being sorted.
uv_snapshot!(filters, Command::new(get_bin())
.arg("pip")
.arg("show")
.arg("requests")
.arg("--cache-dir")
.arg(context.cache_dir.path())
.env("VIRTUAL_ENV", context.venv.as_os_str())
.current_dir(&context.temp_dir), @r###"
success: true
exit_code: 0
----- stdout -----
Name: requests
Version: 2.31.0
Location: [WORKSPACE_DIR]/site-packages
Requires: certifi, charset-normalizer, idna, urllib3
----- stderr -----
"###
);
Ok(())
}
/// Asserts that the Python version marker in the metadata is correctly evaluated.
/// `click` v8.1.7 requires `importlib-metadata`, but only when `python_version < "3.8"`.
#[test]
fn show_python_version_marker() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.touch()?;
requirements_txt.write_str("click==8.1.7")?;
uv_snapshot!(install_command(&context)
.arg("-r")
.arg("requirements.txt")
.arg("--strict"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Installed 1 package in [TIME]
+ click==8.1.7
"###
);
context.assert_command("import click").success();
let mut filters = vec![(
r"Location:.*site-packages",
"Location: [WORKSPACE_DIR]/site-packages",
)];
if cfg!(windows) {
filters.push(("Requires: colorama", "Requires:"));
}
uv_snapshot!(filters, Command::new(get_bin())
.arg("pip")
.arg("show")
.arg("click")
.arg("--cache-dir")
.arg(context.cache_dir.path())
.env("VIRTUAL_ENV", context.venv.as_os_str())
.current_dir(&context.temp_dir), @r###"
success: true
exit_code: 0
----- stdout -----
Name: click
Version: 8.1.7
Location: [WORKSPACE_DIR]/site-packages
Requires:
----- stderr -----
"###
);
Ok(())
}
#[test] #[test]
fn show_found_single_package() -> Result<()> { fn show_found_single_package() -> Result<()> {
let context = TestContext::new("3.12"); let context = TestContext::new("3.12");
@ -81,11 +199,11 @@ fn show_found_single_package() -> Result<()> {
); );
context.assert_command("import markupsafe").success(); context.assert_command("import markupsafe").success();
let filters = [(
let filters = vec![(
r"Location:.*site-packages", r"Location:.*site-packages",
"Location: [WORKSPACE_DIR]/site-packages", "Location: [WORKSPACE_DIR]/site-packages",
)] )];
.to_vec();
uv_snapshot!(filters, Command::new(get_bin()) uv_snapshot!(filters, Command::new(get_bin())
.arg("pip") .arg("pip")
@ -101,6 +219,7 @@ fn show_found_single_package() -> Result<()> {
Name: markupsafe Name: markupsafe
Version: 2.1.3 Version: 2.1.3
Location: [WORKSPACE_DIR]/site-packages Location: [WORKSPACE_DIR]/site-packages
Requires:
----- stderr ----- ----- stderr -----
"### "###
@ -162,10 +281,12 @@ fn show_found_multiple_packages() -> Result<()> {
Name: markupsafe Name: markupsafe
Version: 2.1.3 Version: 2.1.3
Location: [WORKSPACE_DIR]/site-packages Location: [WORKSPACE_DIR]/site-packages
Requires:
--- ---
Name: pip Name: pip
Version: 21.3.1 Version: 21.3.1
Location: [WORKSPACE_DIR]/site-packages Location: [WORKSPACE_DIR]/site-packages
Requires:
----- stderr ----- ----- stderr -----
"### "###
@ -227,6 +348,7 @@ fn show_found_one_out_of_two() -> Result<()> {
Name: markupsafe Name: markupsafe
Version: 2.1.3 Version: 2.1.3
Location: [WORKSPACE_DIR]/site-packages Location: [WORKSPACE_DIR]/site-packages
Requires:
----- stderr ----- ----- stderr -----
warning: Package(s) not found for: flask warning: Package(s) not found for: flask
@ -378,6 +500,7 @@ fn show_editable() -> Result<()> {
Name: poetry-editable Name: poetry-editable
Version: 0.1.0 Version: 0.1.0
Location: [WORKSPACE_DIR]/site-packages Location: [WORKSPACE_DIR]/site-packages
Requires: numpy
----- stderr ----- ----- stderr -----
"### "###