From f261c65bdd8d4d71c03fa0ce473710ab28807128 Mon Sep 17 00:00:00 2001 From: Eric Mark Martin Date: Mon, 13 Jan 2025 17:50:04 -0500 Subject: [PATCH] Implement `pip freeze --path` (#10488) ## Summary Resolves #5952 Add a `--path` option to `uv pip freeze` to be compatible with `pip freeze` ## Test Plan New snapshot tests --------- Co-authored-by: Charlie Marsh --- crates/uv-cli/src/lib.rs | 4 ++ crates/uv/src/commands/pip/freeze.rs | 63 ++++++++++++------ crates/uv/src/lib.rs | 1 + crates/uv/src/settings.rs | 3 + crates/uv/tests/it/pip_freeze.rs | 99 ++++++++++++++++++++++++++++ docs/reference/cli.md | 2 + 6 files changed, 151 insertions(+), 21 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index ec38129b1..e09587a19 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -1904,6 +1904,10 @@ pub struct PipFreezeArgs { )] pub python: Option>, + /// Restrict to the specified installation path for listing packages (can be used multiple times). + #[arg(long("path"), value_parser = parse_file_path)] + pub paths: Option>, + /// List packages in the system Python environment. /// /// Disables discovery of virtual environments. diff --git a/crates/uv/src/commands/pip/freeze.rs b/crates/uv/src/commands/pip/freeze.rs index 62c30bc34..260f7b142 100644 --- a/crates/uv/src/commands/pip/freeze.rs +++ b/crates/uv/src/commands/pip/freeze.rs @@ -1,4 +1,5 @@ use std::fmt::Write; +use std::path::PathBuf; use anyhow::Result; use itertools::Itertools; @@ -19,6 +20,7 @@ pub(crate) fn pip_freeze( strict: bool, python: Option<&str>, system: bool, + paths: Option>, cache: &Cache, printer: Printer, ) -> Result { @@ -31,49 +33,68 @@ pub(crate) fn pip_freeze( report_target_environment(&environment, cache, printer)?; - // Build the installed index. - let site_packages = SitePackages::from_environment(&environment)?; - for dist in site_packages + // Collect all the `site-packages` directories. + let site_packages = match paths { + Some(paths) => { + paths + .into_iter() + .filter_map(|path| { + environment + .clone() + .with_target(uv_python::Target::from(path)) + // Drop invalid paths as per `pip freeze`. + .ok() + }) + .map(|environment| SitePackages::from_environment(&environment)) + .collect::>>()? + } + None => vec![SitePackages::from_environment(&environment)?], + }; + + site_packages .iter() + .flat_map(uv_installer::SitePackages::iter) .filter(|dist| !(exclude_editable && dist.is_editable())) .sorted_unstable_by(|a, b| a.name().cmp(b.name()).then(a.version().cmp(b.version()))) - { - match dist { + .map(|dist| match dist { InstalledDist::Registry(dist) => { - writeln!(printer.stdout(), "{}=={}", dist.name().bold(), dist.version)?; + format!("{}=={}", dist.name().bold(), dist.version) } InstalledDist::Url(dist) => { if dist.editable { - writeln!(printer.stdout(), "-e {}", dist.url)?; + format!("-e {}", dist.url) } else { - writeln!(printer.stdout(), "{} @ {}", dist.name().bold(), dist.url)?; + format!("{} @ {}", dist.name().bold(), dist.url) } } InstalledDist::EggInfoFile(dist) => { - writeln!(printer.stdout(), "{}=={}", dist.name().bold(), dist.version)?; + format!("{}=={}", dist.name().bold(), dist.version) } InstalledDist::EggInfoDirectory(dist) => { - writeln!(printer.stdout(), "{}=={}", dist.name().bold(), dist.version)?; + format!("{}=={}", dist.name().bold(), dist.version) } InstalledDist::LegacyEditable(dist) => { - writeln!(printer.stdout(), "-e {}", dist.target.display())?; + format!("-e {}", dist.target.display()) } - } - } + }) + .dedup() + .try_for_each(|dist| writeln!(printer.stdout(), "{dist}"))?; // Validate that the environment is consistent. if strict { // Determine the markers to use for resolution. let markers = environment.interpreter().resolver_marker_environment(); - for diagnostic in site_packages.diagnostics(&markers)? { - writeln!( - printer.stderr(), - "{}{} {}", - "warning".yellow().bold(), - ":".bold(), - diagnostic.message().bold() - )?; + for entry in site_packages { + for diagnostic in entry.diagnostics(&markers)? { + writeln!( + printer.stderr(), + "{}{} {}", + "warning".yellow().bold(), + ":".bold(), + diagnostic.message().bold() + )?; + } } } diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 69d4e6f41..a446db3d5 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -627,6 +627,7 @@ async fn run(mut cli: Cli) -> Result { args.settings.strict, args.settings.python.as_deref(), args.settings.system, + args.paths, &cache, printer, ) diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 1e282657d..6b41a3671 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -1883,6 +1883,7 @@ impl PipUninstallSettings { #[derive(Debug, Clone)] pub(crate) struct PipFreezeSettings { pub(crate) exclude_editable: bool, + pub(crate) paths: Option>, pub(crate) settings: PipSettings, } @@ -1894,6 +1895,7 @@ impl PipFreezeSettings { strict, no_strict, python, + paths, system, no_system, compat_args: _, @@ -1901,6 +1903,7 @@ impl PipFreezeSettings { Self { exclude_editable, + paths, settings: PipSettings::combine( PipOptions { python: python.and_then(Maybe::into_option), diff --git a/crates/uv/tests/it/pip_freeze.rs b/crates/uv/tests/it/pip_freeze.rs index a8c618be0..1502cf624 100644 --- a/crates/uv/tests/it/pip_freeze.rs +++ b/crates/uv/tests/it/pip_freeze.rs @@ -354,3 +354,102 @@ Version: 0.22.0 Ok(()) } + +#[test] +fn freeze_path() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("MarkupSafe==2.1.3\ntomli==2.0.1")?; + + let target = context.temp_dir.child("install-path"); + + // Run `pip sync`. + context + .pip_sync() + .arg(requirements_txt.path()) + .arg("--target") + .arg(target.path()) + .assert() + .success(); + + // Run `pip freeze`. + uv_snapshot!(context.filters(), context.pip_freeze() + .arg("--path") + .arg(target.path()), @r" + success: true + exit_code: 0 + ----- stdout ----- + markupsafe==2.1.3 + tomli==2.0.1 + + ----- stderr ----- + "); + + Ok(()) +} + +#[test] +fn freeze_multiple_paths() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt1 = context.temp_dir.child("requirements1.txt"); + requirements_txt1.write_str("MarkupSafe==2.1.3\ntomli==2.0.1")?; + + let requirements_txt2 = context.temp_dir.child("requirements2.txt"); + requirements_txt2.write_str("MarkupSafe==2.1.3\nrequests==2.31.0")?; + + let target1 = context.temp_dir.child("install-path1"); + let target2 = context.temp_dir.child("install-path2"); + + // Run `pip sync`. + for (target, requirements_txt) in [ + (target1.path(), requirements_txt1), + (target2.path(), requirements_txt2), + ] { + context + .pip_sync() + .arg(requirements_txt.path()) + .arg("--target") + .arg(target) + .assert() + .success(); + } + + // Run `pip freeze`. + uv_snapshot!(context.filters(), context.pip_freeze().arg("--path").arg(target1.path()).arg("--path").arg(target2.path()), @r" + success: true + exit_code: 0 + ----- stdout ----- + markupsafe==2.1.3 + requests==2.31.0 + tomli==2.0.1 + + ----- stderr ----- + "); + + Ok(()) +} + +// We follow pip in just ignoring nonexistent paths +#[test] +fn freeze_nonexistent_path() { + let context = TestContext::new("3.12"); + + let nonexistent_dir = { + let dir = context.temp_dir.child("blahblah"); + assert!(!dir.exists()); + dir + }; + + // Run `pip freeze`. + uv_snapshot!(context.filters(), context.pip_freeze() + .arg("--path") + .arg(nonexistent_dir.path()), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + "); +} diff --git a/docs/reference/cli.md b/docs/reference/cli.md index ed6db02ac..462d9e070 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -7045,6 +7045,8 @@ uv pip freeze [OPTIONS]

When disabled, uv will only use locally cached data and locally available files.

May also be set with the UV_OFFLINE environment variable.

+
--path paths

Restrict to the specified installation path for listing packages (can be used multiple times)

+
--project project

Run the command within the given project directory.

All pyproject.toml, uv.toml, and .python-version files will be discovered by walking up the directory tree from the project root, as will the project’s virtual environment (.venv).