From 72426a12a1e306427f3f48d1eb3abafbd506c361 Mon Sep 17 00:00:00 2001 From: gaardhus Date: Tue, 9 Dec 2025 11:16:12 +0100 Subject: [PATCH 1/7] Implement `uv pip freeze --exclude` flag fmt --- crates/uv-cli/src/lib.rs | 4 +++ crates/uv/src/commands/pip/freeze.rs | 12 ++++++++- crates/uv/src/lib.rs | 1 + crates/uv/src/settings.rs | 3 +++ crates/uv/tests/it/pip_freeze.rs | 38 ++++++++++++++++++++++++++++ 5 files changed, 57 insertions(+), 1 deletion(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 6befc34b95950..f135870a054de 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2533,6 +2533,10 @@ pub struct PipFreezeArgs { #[arg(long)] pub exclude_editable: bool, + /// Exclude the specified package(s) from the output. + #[arg(long)] + pub r#exclude: Vec, + /// Validate the Python environment, to detect packages with missing dependencies and other /// issues. #[arg(long, overrides_with("no_strict"))] diff --git a/crates/uv/src/commands/pip/freeze.rs b/crates/uv/src/commands/pip/freeze.rs index fd8f3cb177349..37529cc1b7ef9 100644 --- a/crates/uv/src/commands/pip/freeze.rs +++ b/crates/uv/src/commands/pip/freeze.rs @@ -10,6 +10,7 @@ use uv_cache::Cache; use uv_distribution_types::{Diagnostic, InstalledDistKind, Name}; use uv_fs::Simplified; use uv_installer::SitePackages; +use uv_normalize::PackageName; use uv_preview::Preview; use uv_python::PythonPreference; use uv_python::{EnvironmentPreference, Prefix, PythonEnvironment, PythonRequest, Target}; @@ -21,6 +22,7 @@ use crate::printer::Printer; /// Enumerate the installed packages in the current environment. pub(crate) fn pip_freeze( exclude_editable: bool, + exclude: Vec, strict: bool, python: Option<&str>, system: bool, @@ -80,7 +82,15 @@ pub(crate) fn pip_freeze( site_packages .iter() .flat_map(uv_installer::SitePackages::iter) - .filter(|dist| !(exclude_editable && dist.is_editable())) + .filter(|dist| { + if exclude_editable && dist.is_editable() { + return false; + } + if !exclude.is_empty() && exclude.contains(dist.name()) { + return false; + } + true + }) .sorted_unstable_by(|a, b| a.name().cmp(b.name()).then(a.version().cmp(b.version()))) .map(|dist| match &dist.kind { InstalledDistKind::Registry(dist) => { diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 3560faa27412d..a0c4fd60b5e76 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -976,6 +976,7 @@ async fn run(mut cli: Cli) -> Result { commands::pip_freeze( args.exclude_editable, + args.exclude, args.settings.strict, args.settings.python.as_deref(), args.settings.system, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 4a3da83f478af..9a6227854a182 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -3024,6 +3024,7 @@ impl PipUninstallSettings { #[derive(Debug, Clone)] pub(crate) struct PipFreezeSettings { pub(crate) exclude_editable: bool, + pub(crate) exclude: Vec, pub(crate) paths: Option>, pub(crate) settings: PipSettings, } @@ -3037,6 +3038,7 @@ impl PipFreezeSettings { ) -> Self { let PipFreezeArgs { exclude_editable, + exclude, strict, no_strict, python, @@ -3050,6 +3052,7 @@ impl PipFreezeSettings { Self { exclude_editable, + exclude, paths, settings: PipSettings::combine( PipOptions { diff --git a/crates/uv/tests/it/pip_freeze.rs b/crates/uv/tests/it/pip_freeze.rs index ce1ef8b1f6cd2..e14e963419c40 100644 --- a/crates/uv/tests/it/pip_freeze.rs +++ b/crates/uv/tests/it/pip_freeze.rs @@ -574,3 +574,41 @@ fn freeze_prefix() -> Result<()> { Ok(()) } + +#[test] +fn freeze_exclude() -> 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")?; + + // Run `pip sync`. + context + .pip_sync() + .arg(requirements_txt.path()) + .assert() + .success(); + + // Run `pip freeze --exclude MarkupSafe`. + uv_snapshot!(context.filters(), context.pip_freeze().arg("--exclude").arg("MarkupSafe"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + tomli==2.0.1 + + ----- stderr ----- + "### + ); + + // Run `pip freeze --exclude MarkupSafe --exclude tomli`. + uv_snapshot!(context.filters(), context.pip_freeze().arg("--exclude").arg("MarkupSafe").arg("--exclude").arg("tomli"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + "### + ); + + Ok(()) +} From f9e0aa18de01b13e4b4bb220f043af51217ea0b7 Mon Sep 17 00:00:00 2001 From: gaardhus Date: Tue, 9 Dec 2025 12:22:15 +0100 Subject: [PATCH 2/7] fix(arg): pass 'exclude' by reference in 'pip freeze' --- crates/uv/src/commands/pip/freeze.rs | 2 +- crates/uv/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/uv/src/commands/pip/freeze.rs b/crates/uv/src/commands/pip/freeze.rs index 37529cc1b7ef9..b110c4103b464 100644 --- a/crates/uv/src/commands/pip/freeze.rs +++ b/crates/uv/src/commands/pip/freeze.rs @@ -22,7 +22,7 @@ use crate::printer::Printer; /// Enumerate the installed packages in the current environment. pub(crate) fn pip_freeze( exclude_editable: bool, - exclude: Vec, + exclude: &[PackageName], strict: bool, python: Option<&str>, system: bool, diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index a0c4fd60b5e76..4a37963efddbf 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -976,7 +976,7 @@ async fn run(mut cli: Cli) -> Result { commands::pip_freeze( args.exclude_editable, - args.exclude, + &args.exclude, args.settings.strict, args.settings.python.as_deref(), args.settings.system, From 165917be74463de6380a5a93db7665ffc1c719b9 Mon Sep 17 00:00:00 2001 From: gaardhus Date: Wed, 10 Dec 2025 15:20:15 +0100 Subject: [PATCH 3/7] remove unnecessary is_empty check --- crates/uv/src/commands/pip/freeze.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv/src/commands/pip/freeze.rs b/crates/uv/src/commands/pip/freeze.rs index b110c4103b464..a9aa4b6d3d0e2 100644 --- a/crates/uv/src/commands/pip/freeze.rs +++ b/crates/uv/src/commands/pip/freeze.rs @@ -86,7 +86,7 @@ pub(crate) fn pip_freeze( if exclude_editable && dist.is_editable() { return false; } - if !exclude.is_empty() && exclude.contains(dist.name()) { + if exclude.contains(dist.name()) { return false; } true From 21e9e840e10c1453308346d6ff01a96f30c22093 Mon Sep 17 00:00:00 2001 From: gaardhus Date: Wed, 10 Dec 2025 15:27:27 +0100 Subject: [PATCH 4/7] use pip_install instead of pip_sync in test case --- crates/uv/tests/it/pip_freeze.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/crates/uv/tests/it/pip_freeze.rs b/crates/uv/tests/it/pip_freeze.rs index e14e963419c40..e786e8c3ec817 100644 --- a/crates/uv/tests/it/pip_freeze.rs +++ b/crates/uv/tests/it/pip_freeze.rs @@ -582,15 +582,20 @@ fn freeze_exclude() -> Result<()> { let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt.write_str("MarkupSafe==2.1.3\ntomli==2.0.1")?; - // Run `pip sync`. + let prefix = context.temp_dir.child("prefix"); + + // Install packages to a prefix directory. context - .pip_sync() - .arg(requirements_txt.path()) + .pip_install() + .arg("-r") + .arg("requirements.txt") + .arg("--prefix") + .arg(prefix.path()) .assert() .success(); // Run `pip freeze --exclude MarkupSafe`. - uv_snapshot!(context.filters(), context.pip_freeze().arg("--exclude").arg("MarkupSafe"), @r###" + uv_snapshot!(context.filters(), context.pip_freeze().arg("--exclude").arg("MarkupSafe").arg("--prefix").arg(prefix.path()), @r###" success: true exit_code: 0 ----- stdout ----- @@ -601,7 +606,7 @@ fn freeze_exclude() -> Result<()> { ); // Run `pip freeze --exclude MarkupSafe --exclude tomli`. - uv_snapshot!(context.filters(), context.pip_freeze().arg("--exclude").arg("MarkupSafe").arg("--exclude").arg("tomli"), @r###" + uv_snapshot!(context.filters(), context.pip_freeze().arg("--exclude").arg("MarkupSafe").arg("--exclude").arg("tomli").arg("--prefix").arg(prefix.path()), @r###" success: true exit_code: 0 ----- stdout ----- From 7d2b0e55bde1ea3552e77b86f85cce6a64026f44 Mon Sep 17 00:00:00 2001 From: gaardhus Date: Sat, 13 Dec 2025 16:42:27 +0100 Subject: [PATCH 5/7] transform exclude from Vec to HashSet --- crates/uv/src/commands/pip/freeze.rs | 3 ++- crates/uv/src/settings.rs | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/uv/src/commands/pip/freeze.rs b/crates/uv/src/commands/pip/freeze.rs index a9aa4b6d3d0e2..0d5f3d7054f5d 100644 --- a/crates/uv/src/commands/pip/freeze.rs +++ b/crates/uv/src/commands/pip/freeze.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::fmt::Write; use std::path::PathBuf; @@ -22,7 +23,7 @@ use crate::printer::Printer; /// Enumerate the installed packages in the current environment. pub(crate) fn pip_freeze( exclude_editable: bool, - exclude: &[PackageName], + exclude: &HashSet, strict: bool, python: Option<&str>, system: bool, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 9a6227854a182..ab7d288dcc67b 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::env::VarError; use std::num::NonZeroUsize; use std::path::PathBuf; @@ -3024,7 +3025,7 @@ impl PipUninstallSettings { #[derive(Debug, Clone)] pub(crate) struct PipFreezeSettings { pub(crate) exclude_editable: bool, - pub(crate) exclude: Vec, + pub(crate) exclude: HashSet, pub(crate) paths: Option>, pub(crate) settings: PipSettings, } @@ -3052,7 +3053,7 @@ impl PipFreezeSettings { Self { exclude_editable, - exclude, + exclude: exclude.into_iter().collect(), paths, settings: PipSettings::combine( PipOptions { From 8d3a30f4d069d4be2b941e31568cd48c27e29182 Mon Sep 17 00:00:00 2001 From: gaardhus Date: Sat, 13 Dec 2025 16:43:35 +0100 Subject: [PATCH 6/7] install test dependencies as cli arguments rather than through requirements.txt file --- crates/uv/tests/it/pip_freeze.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/uv/tests/it/pip_freeze.rs b/crates/uv/tests/it/pip_freeze.rs index e786e8c3ec817..76007a4a96b2a 100644 --- a/crates/uv/tests/it/pip_freeze.rs +++ b/crates/uv/tests/it/pip_freeze.rs @@ -579,16 +579,13 @@ fn freeze_prefix() -> Result<()> { fn freeze_exclude() -> 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 prefix = context.temp_dir.child("prefix"); // Install packages to a prefix directory. context .pip_install() - .arg("-r") - .arg("requirements.txt") + .arg("MarkupSafe") + .arg("tomli") .arg("--prefix") .arg(prefix.path()) .assert() From b3f082f1de37908c95d7bd9f7adeaf65a410b33a Mon Sep 17 00:00:00 2001 From: gaardhus Date: Sat, 13 Dec 2025 17:28:55 +0100 Subject: [PATCH 7/7] fix: remove return type for test function --- crates/uv/tests/it/pip_freeze.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/uv/tests/it/pip_freeze.rs b/crates/uv/tests/it/pip_freeze.rs index 76007a4a96b2a..d4f4b9fc0ce76 100644 --- a/crates/uv/tests/it/pip_freeze.rs +++ b/crates/uv/tests/it/pip_freeze.rs @@ -576,7 +576,7 @@ fn freeze_prefix() -> Result<()> { } #[test] -fn freeze_exclude() -> Result<()> { +fn freeze_exclude() { let context = TestContext::new("3.12"); let prefix = context.temp_dir.child("prefix"); @@ -611,6 +611,4 @@ fn freeze_exclude() -> Result<()> { ----- stderr ----- "### ); - - Ok(()) }