From 9bb548d251012df7367ecc4c7f2496e13fa7eb04 Mon Sep 17 00:00:00 2001 From: Chan Kang Date: Tue, 12 Mar 2024 00:35:22 -0400 Subject: [PATCH] Implement "Requires" field in `pip show` (#2347) ## Summary Follow-up for https://github.com/astral-sh/uv/commit/395be442fc170af4f22c08ead59911078bc02f8c 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. --- crates/uv/src/commands/pip_show.rs | 24 ++++++ crates/uv/tests/pip_show.rs | 129 ++++++++++++++++++++++++++++- 2 files changed, 150 insertions(+), 3 deletions(-) diff --git a/crates/uv/src/commands/pip_show.rs b/crates/uv/src/commands/pip_show.rs index 2ed27248b..c134ff4fa 100644 --- a/crates/uv/src/commands/pip_show.rs +++ b/crates/uv/src/commands/pip_show.rs @@ -1,6 +1,8 @@ +use std::collections::BTreeSet; use std::fmt::Write; use anyhow::Result; +use itertools::Itertools; use owo_colors::OwoColorize; use tracing::debug; @@ -62,6 +64,9 @@ pub(crate) fn pip_show( // Build the installed index. 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. packages.sort_unstable(); packages.dedup(); @@ -116,6 +121,25 @@ pub(crate) fn pip_show( .expect("package path is not root") .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::>(); + 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. diff --git a/crates/uv/tests/pip_show.rs b/crates/uv/tests/pip_show.rs index 2b2c0d552..fc591e184 100644 --- a/crates/uv/tests/pip_show.rs +++ b/crates/uv/tests/pip_show.rs @@ -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] fn show_found_single_package() -> Result<()> { let context = TestContext::new("3.12"); @@ -81,11 +199,11 @@ fn show_found_single_package() -> Result<()> { ); context.assert_command("import markupsafe").success(); - let filters = [( + + let filters = vec![( r"Location:.*site-packages", "Location: [WORKSPACE_DIR]/site-packages", - )] - .to_vec(); + )]; uv_snapshot!(filters, Command::new(get_bin()) .arg("pip") @@ -101,6 +219,7 @@ fn show_found_single_package() -> Result<()> { Name: markupsafe Version: 2.1.3 Location: [WORKSPACE_DIR]/site-packages + Requires: ----- stderr ----- "### @@ -162,10 +281,12 @@ fn show_found_multiple_packages() -> Result<()> { Name: markupsafe Version: 2.1.3 Location: [WORKSPACE_DIR]/site-packages + Requires: --- Name: pip Version: 21.3.1 Location: [WORKSPACE_DIR]/site-packages + Requires: ----- stderr ----- "### @@ -227,6 +348,7 @@ fn show_found_one_out_of_two() -> Result<()> { Name: markupsafe Version: 2.1.3 Location: [WORKSPACE_DIR]/site-packages + Requires: ----- stderr ----- warning: Package(s) not found for: flask @@ -378,6 +500,7 @@ fn show_editable() -> Result<()> { Name: poetry-editable Version: 0.1.0 Location: [WORKSPACE_DIR]/site-packages + Requires: numpy ----- stderr ----- "###