From 5c71b5c124a1ed98fb21502693b63069f97bec6c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 31 Oct 2025 10:14:12 -0400 Subject: [PATCH] Enable first-class dependency exclusions (#16528) ## Summary This PR adds an `exclude-dependencies` setting that allows users to omit a dependency during resolution. It's effectively a formalized version of the `flask ; python_version < '0'` hack that we've suggested to users in various issues. Closes #12616. --- crates/uv-cli/src/lib.rs | 30 +++ crates/uv-configuration/src/excludes.rs | 25 +++ crates/uv-configuration/src/lib.rs | 2 + crates/uv-requirements/src/specification.rs | 37 +++- crates/uv-resolver/src/lock/mod.rs | 35 ++++ ...r__lock__tests__hash_optional_missing.snap | 1 + ...r__lock__tests__hash_optional_present.snap | 1 + ...r__lock__tests__hash_required_present.snap | 1 + ...missing_dependency_source_unambiguous.snap | 1 + ...dependency_source_version_unambiguous.snap | 1 + ...s__missing_dependency_version_dynamic.snap | 1 + ...issing_dependency_version_unambiguous.snap | 1 + ...lock__tests__source_direct_has_subdir.snap | 1 + ..._lock__tests__source_direct_no_subdir.snap | 1 + ...solver__lock__tests__source_directory.snap | 1 + ...esolver__lock__tests__source_editable.snap | 1 + crates/uv-resolver/src/manifest.rs | 14 +- crates/uv-resolver/src/resolver/mod.rs | 5 +- crates/uv-scripts/src/lib.rs | 1 + crates/uv-settings/src/lib.rs | 5 + crates/uv-settings/src/settings.rs | 6 + crates/uv-static/src/env_vars.rs | 11 +- crates/uv-workspace/src/pyproject.rs | 31 +++ crates/uv-workspace/src/workspace.rs | 20 ++ crates/uv/src/commands/pip/compile.rs | 10 + crates/uv/src/commands/pip/install.rs | 12 +- crates/uv/src/commands/pip/operations.rs | 13 +- crates/uv/src/commands/pip/sync.rs | 4 + crates/uv/src/commands/project/add.rs | 1 + crates/uv/src/commands/project/lock.rs | 13 ++ crates/uv/src/commands/project/lock_target.rs | 17 ++ crates/uv/src/commands/project/mod.rs | 4 + crates/uv/src/commands/tool/install.rs | 2 + crates/uv/src/commands/tool/run.rs | 12 +- crates/uv/src/lib.rs | 20 ++ crates/uv/src/settings.rs | 40 ++++ crates/uv/tests/it/lock.rs | 197 ++++++++++++++++++ crates/uv/tests/it/show_settings.rs | 89 +++++++- docs/reference/cli.md | 12 +- docs/reference/environment.md | 12 +- docs/reference/settings.md | 31 +++ uv.schema.json | 10 + 42 files changed, 711 insertions(+), 21 deletions(-) create mode 100644 crates/uv-configuration/src/excludes.rs diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index d28d92019..80cd4ca77 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -1246,6 +1246,16 @@ pub struct PipCompileArgs { #[arg(long, alias = "override", env = EnvVars::UV_OVERRIDE, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub overrides: Vec>, + /// Exclude packages from resolution using the given requirements files. + /// + /// Excludes files are `requirements.txt`-like files that specify packages to exclude + /// from the resolution. When a package is excluded, it will be omitted from the + /// dependency list entirely and its own dependencies will be ignored during the resolution + /// phase. Excludes are unconditional in that requirement specifiers and markers are ignored; + /// any package listed in the provided file will be omitted from all resolved environments. + #[arg(long, alias = "exclude", env = EnvVars::UV_EXCLUDE, value_delimiter = ' ', value_parser = parse_maybe_file_path)] + pub excludes: Vec>, + /// Constrain build dependencies using the given requirements files when building source /// distributions. /// @@ -1897,6 +1907,16 @@ pub struct PipInstallArgs { #[arg(long, alias = "override", env = EnvVars::UV_OVERRIDE, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub overrides: Vec>, + /// Exclude packages from resolution using the given requirements files. + /// + /// Excludes files are `requirements.txt`-like files that specify packages to exclude + /// from the resolution. When a package is excluded, it will be omitted from the + /// dependency list entirely and its own dependencies will be ignored during the resolution + /// phase. Excludes are unconditional in that requirement specifiers and markers are ignored; + /// any package listed in the provided file will be omitted from all resolved environments. + #[arg(long, alias = "exclude", env = EnvVars::UV_EXCLUDE, value_delimiter = ' ', value_parser = parse_maybe_file_path)] + pub excludes: Vec>, + /// Constrain build dependencies using the given requirements files when building source /// distributions. /// @@ -4848,6 +4868,16 @@ pub struct ToolInstallArgs { #[arg(long, alias = "override", env = EnvVars::UV_OVERRIDE, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub overrides: Vec>, + /// Exclude packages from resolution using the given requirements files. + /// + /// Excludes files are `requirements.txt`-like files that specify packages to exclude + /// from the resolution. When a package is excluded, it will be omitted from the + /// dependency list entirely and its own dependencies will be ignored during the resolution + /// phase. Excludes are unconditional in that requirement specifiers and markers are ignored; + /// any package listed in the provided file will be omitted from all resolved environments. + #[arg(long, alias = "exclude", env = EnvVars::UV_EXCLUDE, value_delimiter = ' ', value_parser = parse_maybe_file_path)] + pub excludes: Vec>, + /// Constrain build dependencies using the given requirements files when building source /// distributions. /// diff --git a/crates/uv-configuration/src/excludes.rs b/crates/uv-configuration/src/excludes.rs new file mode 100644 index 000000000..2c09a8740 --- /dev/null +++ b/crates/uv-configuration/src/excludes.rs @@ -0,0 +1,25 @@ +use rustc_hash::FxHashSet; + +use uv_normalize::PackageName; + +/// A set of packages to exclude from resolution. +#[derive(Debug, Default, Clone)] +pub struct Excludes(FxHashSet); + +impl Excludes { + /// Return an iterator over all package names in the exclusion set. + pub fn iter(&self) -> impl Iterator { + self.0.iter() + } + + /// Check if a package is excluded. + pub fn contains(&self, name: &PackageName) -> bool { + self.0.contains(name) + } +} + +impl FromIterator for Excludes { + fn from_iter>(iter: I) -> Self { + Self(iter.into_iter().collect()) + } +} diff --git a/crates/uv-configuration/src/lib.rs b/crates/uv-configuration/src/lib.rs index 500a7aa38..931634812 100644 --- a/crates/uv-configuration/src/lib.rs +++ b/crates/uv-configuration/src/lib.rs @@ -6,6 +6,7 @@ pub use dependency_groups::*; pub use dry_run::*; pub use editable::*; pub use env_file::*; +pub use excludes::*; pub use export_format::*; pub use extras::*; pub use hash::*; @@ -30,6 +31,7 @@ mod dependency_groups; mod dry_run; mod editable; mod env_file; +mod excludes; mod export_format; mod extras; mod hash; diff --git a/crates/uv-requirements/src/specification.rs b/crates/uv-requirements/src/specification.rs index 80a3c35ea..bb8d8adbf 100644 --- a/crates/uv-requirements/src/specification.rs +++ b/crates/uv-requirements/src/specification.rs @@ -61,6 +61,8 @@ pub struct RequirementsSpecification { pub constraints: Vec, /// The overrides for the project. pub overrides: Vec, + /// The excludes for the project. + pub excludes: Vec, /// The `pylock.toml` file from which to extract the resolution. pub pylock: Option, /// The source trees from which to extract requirements. @@ -345,6 +347,7 @@ impl RequirementsSpecification { requirements: &[RequirementsSource], constraints: &[RequirementsSource], overrides: &[RequirementsSource], + excludes: &[RequirementsSource], groups: Option<&GroupsSpecification>, client_builder: &BaseClientBuilder<'_>, ) -> Result { @@ -378,6 +381,20 @@ impl RequirementsSpecification { )); } + // Disallow `pylock.toml` files as excludes. + if let Some(pylock_toml) = excludes.iter().find_map(|source| { + if let RequirementsSource::PylockToml(path) = source { + Some(path) + } else { + None + } + }) { + return Err(anyhow::anyhow!( + "Cannot use `{}` as an exclude file", + pylock_toml.user_display() + )); + } + // If we have a `pylock.toml`, don't allow additional requirements, constraints, or // overrides. if let Some(pylock_toml) = requirements.iter().find_map(|source| { @@ -582,6 +599,24 @@ impl RequirementsSpecification { spec.no_build.extend(source.no_build); } + // Collect excludes. + for source in excludes { + let source = Self::from_source(source, client_builder).await?; + for req_spec in source.requirements { + match req_spec.requirement { + UnresolvedRequirement::Named(requirement) => { + spec.excludes.push(requirement.name); + } + UnresolvedRequirement::Unnamed(requirement) => { + return Err(anyhow::anyhow!( + "Unnamed requirements are not allowed as exclusions (found: `{requirement}`)" + )); + } + } + } + spec.excludes.extend(source.excludes.into_iter()); + } + Ok(spec) } @@ -597,7 +632,7 @@ impl RequirementsSpecification { requirements: &[RequirementsSource], client_builder: &BaseClientBuilder<'_>, ) -> Result { - Self::from_sources(requirements, &[], &[], None, client_builder).await + Self::from_sources(requirements, &[], &[], &[], None, client_builder).await } /// Initialize a [`RequirementsSpecification`] from a list of [`Requirement`]. diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 5bf6c64b8..4e6a509a1 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -1163,6 +1163,23 @@ impl Lock { manifest_table.insert("overrides", value(overrides)); } + if !self.manifest.excludes.is_empty() { + let excludes = self + .manifest + .excludes + .iter() + .map(|name| { + serde::Serialize::serialize(&name, toml_edit::ser::ValueSerializer::new()) + }) + .collect::, _>>()?; + let excludes = match excludes.as_slice() { + [] => Array::new(), + [name] => Array::from_iter([name]), + excludes => each_element_on_its_line_array(excludes.iter()), + }; + manifest_table.insert("excludes", value(excludes)); + } + if !self.manifest.build_constraints.is_empty() { let build_constraints = self .manifest @@ -1447,6 +1464,7 @@ impl Lock { requirements: &[Requirement], constraints: &[Requirement], overrides: &[Requirement], + excludes: &[PackageName], build_constraints: &[Requirement], dependency_groups: &BTreeMap>, dependency_metadata: &DependencyMetadata, @@ -1563,6 +1581,15 @@ impl Lock { } } + // Validate that the lockfile was generated with the same excludes. + { + let expected: BTreeSet<_> = excludes.iter().cloned().collect(); + let actual: BTreeSet<_> = self.manifest.excludes.iter().cloned().collect(); + if expected != actual { + return Ok(SatisfiesResult::MismatchedExcludes(expected, actual)); + } + } + // Validate that the lockfile was generated with the same build constraints. { let expected: BTreeSet<_> = build_constraints @@ -2049,6 +2076,8 @@ pub enum SatisfiesResult<'lock> { MismatchedConstraints(BTreeSet, BTreeSet), /// The lockfile uses a different set of overrides. MismatchedOverrides(BTreeSet, BTreeSet), + /// The lockfile uses a different set of excludes. + MismatchedExcludes(BTreeSet, BTreeSet), /// The lockfile uses a different set of build constraints. MismatchedBuildConstraints(BTreeSet, BTreeSet), /// The lockfile uses a different set of dependency groups. @@ -2148,6 +2177,9 @@ pub struct ResolverManifest { /// The overrides provided to the resolver. #[serde(default)] overrides: BTreeSet, + /// The excludes provided to the resolver. + #[serde(default)] + excludes: BTreeSet, /// The build constraints provided to the resolver. #[serde(default)] build_constraints: BTreeSet, @@ -2164,6 +2196,7 @@ impl ResolverManifest { requirements: impl IntoIterator, constraints: impl IntoIterator, overrides: impl IntoIterator, + excludes: impl IntoIterator, build_constraints: impl IntoIterator, dependency_groups: impl IntoIterator)>, dependency_metadata: impl IntoIterator, @@ -2173,6 +2206,7 @@ impl ResolverManifest { requirements: requirements.into_iter().collect(), constraints: constraints.into_iter().collect(), overrides: overrides.into_iter().collect(), + excludes: excludes.into_iter().collect(), build_constraints: build_constraints.into_iter().collect(), dependency_groups: dependency_groups .into_iter() @@ -2201,6 +2235,7 @@ impl ResolverManifest { .into_iter() .map(|requirement| requirement.relative_to(root)) .collect::, _>>()?, + excludes: self.excludes, build_constraints: self .build_constraints .into_iter() diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap index 0a847c0c2..7c04e11ef 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap @@ -120,6 +120,7 @@ Ok( dependency_groups: {}, constraints: {}, overrides: {}, + excludes: {}, build_constraints: {}, dependency_metadata: {}, }, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap index 8c75ee866..7bddd799b 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap @@ -127,6 +127,7 @@ Ok( dependency_groups: {}, constraints: {}, overrides: {}, + excludes: {}, build_constraints: {}, dependency_metadata: {}, }, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap index 1daf5a47d..8bf591754 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap @@ -119,6 +119,7 @@ Ok( dependency_groups: {}, constraints: {}, overrides: {}, + excludes: {}, build_constraints: {}, dependency_metadata: {}, }, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap index 17c061494..05079e25c 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap @@ -198,6 +198,7 @@ Ok( dependency_groups: {}, constraints: {}, overrides: {}, + excludes: {}, build_constraints: {}, dependency_metadata: {}, }, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap index 17c061494..05079e25c 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap @@ -198,6 +198,7 @@ Ok( dependency_groups: {}, constraints: {}, overrides: {}, + excludes: {}, build_constraints: {}, dependency_metadata: {}, }, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_dynamic.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_dynamic.snap index 0870ee298..2e0c24a1d 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_dynamic.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_dynamic.snap @@ -223,6 +223,7 @@ Ok( dependency_groups: {}, constraints: {}, overrides: {}, + excludes: {}, build_constraints: {}, dependency_metadata: {}, }, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap index 17c061494..05079e25c 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap @@ -198,6 +198,7 @@ Ok( dependency_groups: {}, constraints: {}, overrides: {}, + excludes: {}, build_constraints: {}, dependency_metadata: {}, }, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap index 70e0a0f71..20b59b7d9 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap @@ -98,6 +98,7 @@ Ok( dependency_groups: {}, constraints: {}, overrides: {}, + excludes: {}, build_constraints: {}, dependency_metadata: {}, }, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap index 08873c5c4..b470f5ac2 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap @@ -94,6 +94,7 @@ Ok( dependency_groups: {}, constraints: {}, overrides: {}, + excludes: {}, build_constraints: {}, dependency_metadata: {}, }, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap index 7b15016cc..ade8411b7 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap @@ -84,6 +84,7 @@ Ok( dependency_groups: {}, constraints: {}, overrides: {}, + excludes: {}, build_constraints: {}, dependency_metadata: {}, }, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap index 5afb02d76..75794fe76 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap @@ -84,6 +84,7 @@ Ok( dependency_groups: {}, constraints: {}, overrides: {}, + excludes: {}, build_constraints: {}, dependency_metadata: {}, }, diff --git a/crates/uv-resolver/src/manifest.rs b/crates/uv-resolver/src/manifest.rs index 6533a3aab..fd39470de 100644 --- a/crates/uv-resolver/src/manifest.rs +++ b/crates/uv-resolver/src/manifest.rs @@ -3,7 +3,7 @@ use std::collections::BTreeSet; use either::Either; -use uv_configuration::{Constraints, Overrides}; +use uv_configuration::{Constraints, Excludes, Overrides}; use uv_distribution_types::Requirement; use uv_normalize::PackageName; use uv_types::RequestedRequirements; @@ -23,6 +23,9 @@ pub struct Manifest { /// The overrides for the project. pub(crate) overrides: Overrides, + /// The dependency excludes for the project. + pub(crate) excludes: Excludes, + /// The preferences for the project. /// /// These represent "preferred" versions of a given package. For example, they may be the @@ -55,6 +58,7 @@ impl Manifest { requirements: Vec, constraints: Constraints, overrides: Overrides, + excludes: Excludes, preferences: Preferences, project: Option, workspace_members: BTreeSet, @@ -65,6 +69,7 @@ impl Manifest { requirements, constraints, overrides, + excludes, preferences, project, workspace_members, @@ -78,6 +83,7 @@ impl Manifest { requirements, constraints: Constraints::default(), overrides: Overrides::default(), + excludes: Excludes::default(), preferences: Preferences::default(), project: None, exclusions: Exclusions::default(), @@ -122,6 +128,7 @@ impl Manifest { .flat_map(move |lookahead| { self.overrides .apply(lookahead.requirements()) + .filter(|requirement| !self.excludes.contains(&requirement.name)) .filter(move |requirement| { requirement .evaluate_markers(env.marker_environment(), lookahead.extras()) @@ -130,6 +137,7 @@ impl Manifest { .chain( self.overrides .apply(&self.requirements) + .filter(|requirement| !self.excludes.contains(&requirement.name)) .filter(move |requirement| { requirement.evaluate_markers(env.marker_environment(), &[]) }), @@ -137,6 +145,7 @@ impl Manifest { .chain( self.constraints .requirements() + .filter(|requirement| !self.excludes.contains(&requirement.name)) .filter(move |requirement| { requirement.evaluate_markers(env.marker_environment(), &[]) }) @@ -148,6 +157,7 @@ impl Manifest { self.overrides .apply(&self.requirements) .chain(self.constraints.requirements().map(Cow::Borrowed)) + .filter(|requirement| !self.excludes.contains(&requirement.name)) .filter(move |requirement| { requirement.evaluate_markers(env.marker_environment(), &[]) }), @@ -166,6 +176,7 @@ impl Manifest { DependencyMode::Transitive => Either::Left( self.overrides .requirements() + .filter(|requirement| !self.excludes.contains(&requirement.name)) .filter(move |requirement| { requirement.evaluate_markers(env.marker_environment(), &[]) }) @@ -175,6 +186,7 @@ impl Manifest { DependencyMode::Direct => Either::Right( self.overrides .requirements() + .filter(|requirement| !self.excludes.contains(&requirement.name)) .filter(move |requirement| { requirement.evaluate_markers(env.marker_environment(), &[]) }) diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index dd574d576..e893a82b0 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -20,7 +20,7 @@ use tokio::sync::oneshot; use tokio_stream::wrappers::ReceiverStream; use tracing::{Level, debug, info, instrument, trace, warn}; -use uv_configuration::{Constraints, Overrides}; +use uv_configuration::{Constraints, Excludes, Overrides}; use uv_distribution::{ArchiveMetadata, DistributionDatabase}; use uv_distribution_types::{ BuiltDist, CompatibleDist, DerivationChain, Dist, DistErrorKind, DistributionMetadata, @@ -111,6 +111,7 @@ struct ResolverState { requirements: Vec, constraints: Constraints, overrides: Overrides, + excludes: Excludes, preferences: Preferences, git: GitResolver, capabilities: IndexCapabilities, @@ -240,6 +241,7 @@ impl requirements: manifest.requirements, constraints: manifest.constraints, overrides: manifest.overrides, + excludes: manifest.excludes, preferences: manifest.preferences, exclusions: manifest.exclusions, hasher: hasher.clone(), @@ -1866,6 +1868,7 @@ impl ResolverState>>, + pub exclude_dependencies: Option>, pub constraint_dependencies: Option>>, pub build_constraint_dependencies: Option>>, pub extra_build_dependencies: Option>>, diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs index c9fcc16b7..6b9b8e98f 100644 --- a/crates/uv-settings/src/lib.rs +++ b/crates/uv-settings/src/lib.rs @@ -212,6 +212,7 @@ fn validate_uv_toml(path: &Path, options: &Options) -> Result<(), Error> { pip: _, cache_keys: _, override_dependencies: _, + exclude_dependencies: _, constraint_dependencies: _, build_constraint_dependencies: _, environments, @@ -352,6 +353,7 @@ fn warn_uv_toml_masked_fields(options: &Options) { pip, cache_keys, override_dependencies, + exclude_dependencies, constraint_dependencies, build_constraint_dependencies, environments: _, @@ -525,6 +527,9 @@ fn warn_uv_toml_masked_fields(options: &Options) { if override_dependencies.is_some() { masked_fields.push("override-dependencies"); } + if exclude_dependencies.is_some() { + masked_fields.push("exclude-dependencies"); + } if constraint_dependencies.is_some() { masked_fields.push("constraint-dependencies"); } diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 6042af68d..32901020b 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -115,6 +115,9 @@ pub struct Options { #[cfg_attr(feature = "schemars", schemars(skip))] pub override_dependencies: Option>>, + #[cfg_attr(feature = "schemars", schemars(skip))] + pub exclude_dependencies: Option>, + #[cfg_attr(feature = "schemars", schemars(skip))] pub constraint_dependencies: Option>>, @@ -2110,6 +2113,7 @@ pub struct OptionsWire { // `crates/uv-workspace/src/pyproject.rs`. The documentation lives on that struct. // They're respected in both `pyproject.toml` and `uv.toml` files. override_dependencies: Option>>, + exclude_dependencies: Option>, constraint_dependencies: Option>>, build_constraint_dependencies: Option>>, environments: Option, @@ -2180,6 +2184,7 @@ impl From for Options { pip, cache_keys, override_dependencies, + exclude_dependencies, constraint_dependencies, build_constraint_dependencies, environments, @@ -2254,6 +2259,7 @@ impl From for Options { cache_keys, build_backend, override_dependencies, + exclude_dependencies, constraint_dependencies, build_constraint_dependencies, environments, diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index 6e2bb5204..60a6079ee 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -120,21 +120,26 @@ impl EnvVars { #[attr_added_in("0.1.34")] pub const UV_REQUIRE_HASHES: &'static str = "UV_REQUIRE_HASHES"; - /// Equivalent to the `--constraint` command-line argument. If set, uv will use this + /// Equivalent to the `--constraints` command-line argument. If set, uv will use this /// file as the constraints file. Uses space-separated list of files. #[attr_added_in("0.1.36")] pub const UV_CONSTRAINT: &'static str = "UV_CONSTRAINT"; - /// Equivalent to the `--build-constraint` command-line argument. If set, uv will use this file + /// Equivalent to the `--build-constraints` command-line argument. If set, uv will use this file /// as constraints for any source distribution builds. Uses space-separated list of files. #[attr_added_in("0.2.34")] pub const UV_BUILD_CONSTRAINT: &'static str = "UV_BUILD_CONSTRAINT"; - /// Equivalent to the `--override` command-line argument. If set, uv will use this file + /// Equivalent to the `--overrides` command-line argument. If set, uv will use this file /// as the overrides file. Uses space-separated list of files. #[attr_added_in("0.2.22")] pub const UV_OVERRIDE: &'static str = "UV_OVERRIDE"; + /// Equivalent to the `--excludes` command-line argument. If set, uv will use this + /// as the excludes file. Uses space-separated list of files. + #[attr_added_in("0.9.8")] + pub const UV_EXCLUDE: &'static str = "UV_EXCLUDE"; + /// Equivalent to the `--link-mode` command-line argument. If set, uv will use this as /// a link mode. #[attr_added_in("0.1.40")] diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index cd643d5fe..74525c6e0 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -489,6 +489,37 @@ pub struct ToolUv { )] pub override_dependencies: Option>>, + /// Dependencies to exclude when resolving the project's dependencies. + /// + /// Excludes are used to prevent a package from being selected during resolution, + /// regardless of whether it's requested by any other package. When a package is excluded, + /// it will be omitted from the dependency list entirely. + /// + /// Including a package as an exclusion will prevent it from being installed, even if + /// it's requested by transitive dependencies. This can be useful for removing optional + /// dependencies or working around packages with broken dependencies. + /// + /// !!! note + /// In `uv lock`, `uv sync`, and `uv run`, uv will only read `exclude-dependencies` from + /// the `pyproject.toml` at the workspace root, and will ignore any declarations in other + /// workspace members or `uv.toml` files. + #[cfg_attr( + feature = "schemars", + schemars( + with = "Option>", + description = "Package names to exclude, e.g., `werkzeug`, `numpy`." + ) + )] + #[option( + default = "[]", + value_type = "list[str]", + example = r#" + # Exclude Werkzeug from being installed, even if transitive dependencies request it. + exclude-dependencies = ["werkzeug"] + "# + )] + pub exclude_dependencies: Option>, + /// Constraints to apply when resolving the project's dependencies. /// /// Constraints are used to restrict the versions of dependencies that are selected during diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index 969a1b71e..6bd329d6e 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -661,6 +661,20 @@ impl Workspace { overrides.clone() } + /// Returns the set of dependency exclusions for the workspace. + pub fn exclude_dependencies(&self) -> Vec { + let Some(excludes) = self + .pyproject_toml + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.exclude_dependencies.as_ref()) + else { + return vec![]; + }; + excludes.clone() + } + /// Returns the set of constraints for the workspace. pub fn constraints(&self) -> Vec> { let Some(constraints) = self @@ -2059,6 +2073,7 @@ mod tests { "dependency-groups": null, "dev-dependencies": null, "override-dependencies": null, + "exclude-dependencies": null, "constraint-dependencies": null, "build-constraint-dependencies": null, "environments": null, @@ -2159,6 +2174,7 @@ mod tests { "dependency-groups": null, "dev-dependencies": null, "override-dependencies": null, + "exclude-dependencies": null, "constraint-dependencies": null, "build-constraint-dependencies": null, "environments": null, @@ -2372,6 +2388,7 @@ mod tests { "dependency-groups": null, "dev-dependencies": null, "override-dependencies": null, + "exclude-dependencies": null, "constraint-dependencies": null, "build-constraint-dependencies": null, "environments": null, @@ -2481,6 +2498,7 @@ mod tests { "dependency-groups": null, "dev-dependencies": null, "override-dependencies": null, + "exclude-dependencies": null, "constraint-dependencies": null, "build-constraint-dependencies": null, "environments": null, @@ -2603,6 +2621,7 @@ mod tests { "dependency-groups": null, "dev-dependencies": null, "override-dependencies": null, + "exclude-dependencies": null, "constraint-dependencies": null, "build-constraint-dependencies": null, "environments": null, @@ -2699,6 +2718,7 @@ mod tests { "dependency-groups": null, "dev-dependencies": null, "override-dependencies": null, + "exclude-dependencies": null, "constraint-dependencies": null, "build-constraint-dependencies": null, "environments": null, diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index aa89884ea..351f9228a 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -62,9 +62,11 @@ pub(crate) async fn pip_compile( requirements: &[RequirementsSource], constraints: &[RequirementsSource], overrides: &[RequirementsSource], + excludes: &[RequirementsSource], build_constraints: &[RequirementsSource], constraints_from_workspace: Vec, overrides_from_workspace: Vec, + excludes_from_workspace: Vec, build_constraints_from_workspace: Vec, environments: SupportedEnvironments, extras: ExtrasSpecification, @@ -202,6 +204,7 @@ pub(crate) async fn pip_compile( requirements, constraints, overrides, + excludes, pylock, source_trees, groups, @@ -216,6 +219,7 @@ pub(crate) async fn pip_compile( requirements, constraints, overrides, + excludes, Some(&groups), &client_builder, ) @@ -248,6 +252,11 @@ pub(crate) async fn pip_compile( ) .collect(); + let excludes: Vec = excludes + .into_iter() + .chain(excludes_from_workspace) + .collect(); + // Read build constraints. let build_constraints: Vec = operations::read_constraints(build_constraints, &client_builder) @@ -532,6 +541,7 @@ pub(crate) async fn pip_compile( requirements, constraints, overrides, + excludes, source_trees, project, BTreeSet::default(), diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index b513faa79..9f98fe426 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -23,7 +23,7 @@ use uv_distribution_types::{ use uv_fs::Simplified; use uv_install_wheel::LinkMode; use uv_installer::{InstallationStrategy, SatisfiesResult, SitePackages}; -use uv_normalize::{DefaultExtras, DefaultGroups}; +use uv_normalize::{DefaultExtras, DefaultGroups, PackageName}; use uv_preview::{Preview, PreviewFeatures}; use uv_pypi_types::Conflicts; use uv_python::{ @@ -54,9 +54,11 @@ pub(crate) async fn pip_install( requirements: &[RequirementsSource], constraints: &[RequirementsSource], overrides: &[RequirementsSource], + excludes: &[RequirementsSource], build_constraints: &[RequirementsSource], constraints_from_workspace: Vec, overrides_from_workspace: Vec, + excludes_from_workspace: Vec, build_constraints_from_workspace: Vec, extras: &ExtrasSpecification, groups: &GroupsSpecification, @@ -118,6 +120,7 @@ pub(crate) async fn pip_install( requirements, constraints, overrides, + excludes, pylock, source_trees, groups, @@ -132,6 +135,7 @@ pub(crate) async fn pip_install( requirements, constraints, overrides, + excludes, extras, Some(groups), &client_builder, @@ -167,6 +171,11 @@ pub(crate) async fn pip_install( ) .collect(); + let excludes: Vec = excludes + .into_iter() + .chain(excludes_from_workspace) + .collect(); + // Read build constraints. let build_constraints: Vec = operations::read_constraints(build_constraints, &client_builder) @@ -550,6 +559,7 @@ pub(crate) async fn pip_install( requirements, constraints, overrides, + excludes, source_trees, project, BTreeSet::default(), diff --git a/crates/uv/src/commands/pip/operations.rs b/crates/uv/src/commands/pip/operations.rs index 5a50fe769..522f6478d 100644 --- a/crates/uv/src/commands/pip/operations.rs +++ b/crates/uv/src/commands/pip/operations.rs @@ -13,8 +13,8 @@ use tracing::debug; use uv_cache::Cache; use uv_client::{BaseClientBuilder, RegistryClient}; use uv_configuration::{ - BuildOptions, Concurrency, Constraints, DependencyGroups, DryRun, ExtrasSpecification, - Overrides, Reinstall, Upgrade, + BuildOptions, Concurrency, Constraints, DependencyGroups, DryRun, Excludes, + ExtrasSpecification, Overrides, Reinstall, Upgrade, }; use uv_dispatch::BuildDispatch; use uv_distribution::{DistributionDatabase, SourcedDependencyGroups}; @@ -54,6 +54,7 @@ pub(crate) async fn read_requirements( requirements: &[RequirementsSource], constraints: &[RequirementsSource], overrides: &[RequirementsSource], + excludes: &[RequirementsSource], extras: &ExtrasSpecification, groups: Option<&GroupsSpecification>, client_builder: &BaseClientBuilder<'_>, @@ -80,6 +81,7 @@ pub(crate) async fn read_requirements( requirements, constraints, overrides, + excludes, groups, client_builder, ) @@ -92,7 +94,7 @@ pub(crate) async fn read_constraints( client_builder: &BaseClientBuilder<'_>, ) -> Result, Error> { Ok( - RequirementsSpecification::from_sources(&[], constraints, &[], None, client_builder) + RequirementsSpecification::from_sources(&[], constraints, &[], &[], None, client_builder) .await? .constraints, ) @@ -103,6 +105,7 @@ pub(crate) async fn resolve( requirements: Vec, constraints: Vec, overrides: Vec, + excludes: Vec, source_trees: Vec, mut project: Option, workspace_members: BTreeSet, @@ -282,7 +285,7 @@ pub(crate) async fn resolve( overrides }; - // Collect constraints and overrides. + // Collect constraints, overrides, and excludes. let constraints = Constraints::from_requirements( constraints .into_iter() @@ -290,6 +293,7 @@ pub(crate) async fn resolve( .chain(upgrade.constraints().cloned()), ); let overrides = Overrides::from_requirements(overrides); + let excludes = excludes.into_iter().collect::(); let preferences = Preferences::from_iter(preferences, &resolver_env); // Determine any lookahead requirements. @@ -318,6 +322,7 @@ pub(crate) async fn resolve( requirements, constraints, overrides, + excludes, preferences, project, workspace_members, diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index 0cc7c9d9a..8da323c6e 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -102,6 +102,7 @@ pub(crate) async fn pip_sync( // Initialize a few defaults. let overrides = &[]; + let excludes = &[]; let upgrade = Upgrade::default(); let resolution_mode = ResolutionMode::default(); let prerelease_mode = PrereleaseMode::default(); @@ -113,6 +114,7 @@ pub(crate) async fn pip_sync( requirements, constraints, overrides, + excludes, pylock, source_trees, groups, @@ -127,6 +129,7 @@ pub(crate) async fn pip_sync( requirements, constraints, overrides, + excludes, extras, Some(groups), &client_builder, @@ -461,6 +464,7 @@ pub(crate) async fn pip_sync( requirements, constraints, overrides, + excludes, source_trees, project, BTreeSet::default(), diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 5cafbf681..9374146ef 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -354,6 +354,7 @@ pub(crate) async fn add( &requirements, &constraints, &[], + &[], None, &client_builder, ) diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index de0690246..900085cf4 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -485,6 +485,7 @@ async fn do_lock( let required_members = target.required_members(); let requirements = target.requirements(); let overrides = target.overrides(); + let excludes = target.exclude_dependencies(); let constraints = target.constraints(); let build_constraints = target.build_constraints(); let dependency_groups = target.dependency_groups()?; @@ -764,6 +765,7 @@ async fn do_lock( &dependency_groups, &constraints, &overrides, + &excludes, &build_constraints, &conflicts, environments, @@ -887,6 +889,7 @@ async fn do_lock( .cloned() .map(UnresolvedRequirementSpecification::from) .collect(), + excludes.clone(), source_trees, // The root is always null in workspaces, it "depends on" the projects None, @@ -925,6 +928,7 @@ async fn do_lock( requirements, constraints, overrides, + excludes.clone(), build_constraints, dependency_groups, dependency_metadata.values().cloned(), @@ -983,6 +987,7 @@ impl ValidatedLock { dependency_groups: &BTreeMap>, constraints: &[Requirement], overrides: &[Requirement], + excludes: &[PackageName], build_constraints: &[Requirement], conflicts: &Conflicts, environments: Option<&SupportedEnvironments>, @@ -1213,6 +1218,7 @@ impl ValidatedLock { requirements, constraints, overrides, + excludes, build_constraints, dependency_groups, dependency_metadata, @@ -1305,6 +1311,13 @@ impl ValidatedLock { ); Ok(Self::Preferable(lock)) } + SatisfiesResult::MismatchedExcludes(expected, actual) => { + debug!( + "Resolving despite existing lockfile due to mismatched excludes:\n Requested: {:?}\n Existing: {:?}", + expected, actual + ); + Ok(Self::Preferable(lock)) + } SatisfiesResult::MismatchedBuildConstraints(expected, actual) => { debug!( "Resolving despite existing lockfile due to mismatched build constraints:\n Requested: {:?}\n Existing: {:?}", diff --git a/crates/uv/src/commands/project/lock_target.rs b/crates/uv/src/commands/project/lock_target.rs index 9bd1a8222..b5fdc99a9 100644 --- a/crates/uv/src/commands/project/lock_target.rs +++ b/crates/uv/src/commands/project/lock_target.rs @@ -62,6 +62,23 @@ impl<'lock> LockTarget<'lock> { } } + /// Returns the set of dependency exclusions for the [`LockTarget`]. + pub(crate) fn exclude_dependencies(self) -> Vec { + match self { + Self::Workspace(workspace) => workspace.exclude_dependencies(), + Self::Script(script) => script + .metadata + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.exclude_dependencies.as_ref()) + .into_iter() + .flatten() + .cloned() + .collect(), + } + } + /// Returns the set of constraints for the [`LockTarget`]. pub(crate) fn constraints(self) -> Vec> { match self { diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 644642eb1..b819ce407 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -1878,6 +1878,7 @@ pub(crate) async fn resolve_environment( requirements, constraints, overrides, + excludes, source_trees, .. } = spec.requirements; @@ -1997,6 +1998,7 @@ pub(crate) async fn resolve_environment( requirements, constraints, overrides, + excludes, source_trees, project, BTreeSet::default(), @@ -2234,6 +2236,7 @@ pub(crate) async fn update_environment( requirements, constraints, overrides, + excludes, source_trees, .. } = spec; @@ -2366,6 +2369,7 @@ pub(crate) async fn update_environment( requirements, constraints, overrides, + excludes, source_trees, project, BTreeSet::default(), diff --git a/crates/uv/src/commands/tool/install.rs b/crates/uv/src/commands/tool/install.rs index 0f35ad70c..f0633776f 100644 --- a/crates/uv/src/commands/tool/install.rs +++ b/crates/uv/src/commands/tool/install.rs @@ -53,6 +53,7 @@ pub(crate) async fn install( with: &[RequirementsSource], constraints: &[RequirementsSource], overrides: &[RequirementsSource], + excludes: &[RequirementsSource], build_constraints: &[RequirementsSource], entrypoints: &[PackageName], python: Option, @@ -251,6 +252,7 @@ pub(crate) async fn install( with, constraints, overrides, + excludes, None, &client_builder, ) diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index 918d014f9..8bb917a53 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -871,9 +871,15 @@ async fn get_or_create_environment( }; // Read the `--with` requirements. - let spec = - RequirementsSpecification::from_sources(with, constraints, overrides, None, client_builder) - .await?; + let spec = RequirementsSpecification::from_sources( + with, + constraints, + overrides, + &[], + None, + client_builder, + ) + .await?; // Resolve the `--from` and `--with` requirements. let requirements = { diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index ca5b52069..72d17c5ab 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -568,6 +568,11 @@ async fn run(mut cli: Cli) -> Result { .into_iter() .map(RequirementsSource::from_overrides_txt) .collect::, _>>()?; + let excludes = args + .excludes + .into_iter() + .map(RequirementsSource::from_requirements_txt) + .collect::, _>>()?; let build_constraints = args .build_constraints .into_iter() @@ -582,9 +587,11 @@ async fn run(mut cli: Cli) -> Result { &requirements, &constraints, &overrides, + &excludes, &build_constraints, args.constraints_from_workspace, args.overrides_from_workspace, + args.excludes_from_workspace, args.build_constraints_from_workspace, args.environments, args.settings.extras, @@ -751,6 +758,11 @@ async fn run(mut cli: Cli) -> Result { .into_iter() .map(RequirementsSource::from_overrides_txt) .collect::, _>>()?; + let excludes = args + .excludes + .into_iter() + .map(RequirementsSource::from_requirements_txt) + .collect::, _>>()?; let build_constraints = args .build_constraints .into_iter() @@ -814,9 +826,11 @@ async fn run(mut cli: Cli) -> Result { &requirements, &constraints, &overrides, + &excludes, &build_constraints, args.constraints_from_workspace, args.overrides_from_workspace, + args.excludes_from_workspace, args.build_constraints_from_workspace, &args.settings.extras, &groups, @@ -1376,6 +1390,11 @@ async fn run(mut cli: Cli) -> Result { .into_iter() .map(RequirementsSource::from_overrides_txt) .collect::, _>>()?; + let excludes = args + .excludes + .into_iter() + .map(RequirementsSource::from_requirements_txt) + .collect::, _>>()?; let build_constraints = args .build_constraints .into_iter() @@ -1389,6 +1408,7 @@ async fn run(mut cli: Cli) -> Result { &requirements, &constraints, &overrides, + &excludes, &build_constraints, &entrypoints, args.python, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 01a7e7c18..4f44b36eb 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -681,6 +681,7 @@ pub(crate) struct ToolInstallSettings { pub(crate) with_editable: Vec, pub(crate) constraints: Vec, pub(crate) overrides: Vec, + pub(crate) excludes: Vec, pub(crate) build_constraints: Vec, pub(crate) python: Option, pub(crate) python_platform: Option, @@ -710,6 +711,7 @@ impl ToolInstallSettings { with_executables_from, constraints, overrides, + excludes, build_constraints, installer, force, @@ -762,6 +764,10 @@ impl ToolInstallSettings { .into_iter() .filter_map(Maybe::into_option) .collect(), + excludes: excludes + .into_iter() + .filter_map(Maybe::into_option) + .collect(), build_constraints: build_constraints .into_iter() .filter_map(Maybe::into_option) @@ -2119,9 +2125,11 @@ pub(crate) struct PipCompileSettings { pub(crate) src_file: Vec, pub(crate) constraints: Vec, pub(crate) overrides: Vec, + pub(crate) excludes: Vec, pub(crate) build_constraints: Vec, pub(crate) constraints_from_workspace: Vec, pub(crate) overrides_from_workspace: Vec, + pub(crate) excludes_from_workspace: Vec, pub(crate) build_constraints_from_workspace: Vec, pub(crate) environments: SupportedEnvironments, pub(crate) refresh: Refresh, @@ -2139,6 +2147,7 @@ impl PipCompileSettings { src_file, constraints, overrides, + excludes, extra, all_extras, no_all_extras, @@ -2216,6 +2225,15 @@ impl PipCompileSettings { Vec::new() }; + let excludes_from_workspace = if let Some(configuration) = &filesystem { + configuration + .exclude_dependencies + .clone() + .unwrap_or_default() + } else { + Vec::new() + }; + let build_constraints_from_workspace = if let Some(configuration) = &filesystem { configuration .build_constraint_dependencies @@ -2251,8 +2269,13 @@ impl PipCompileSettings { .into_iter() .filter_map(Maybe::into_option) .collect(), + excludes: excludes + .into_iter() + .filter_map(Maybe::into_option) + .collect(), constraints_from_workspace, overrides_from_workspace, + excludes_from_workspace, build_constraints_from_workspace, environments, refresh: Refresh::from(refresh), @@ -2417,10 +2440,12 @@ pub(crate) struct PipInstallSettings { pub(crate) editables: Vec, pub(crate) constraints: Vec, pub(crate) overrides: Vec, + pub(crate) excludes: Vec, pub(crate) build_constraints: Vec, pub(crate) dry_run: DryRun, pub(crate) constraints_from_workspace: Vec, pub(crate) overrides_from_workspace: Vec, + pub(crate) excludes_from_workspace: Vec, pub(crate) build_constraints_from_workspace: Vec, pub(crate) modifications: Modifications, pub(crate) refresh: Refresh, @@ -2440,6 +2465,7 @@ impl PipInstallSettings { editable, constraints, overrides, + excludes, build_constraints, extra, all_extras, @@ -2503,6 +2529,15 @@ impl PipInstallSettings { Vec::new() }; + let excludes_from_workspace = if let Some(configuration) = &filesystem { + configuration + .exclude_dependencies + .clone() + .unwrap_or_default() + } else { + Vec::new() + }; + let build_constraints_from_workspace = if let Some(configuration) = &filesystem { configuration .build_constraint_dependencies @@ -2529,6 +2564,10 @@ impl PipInstallSettings { .into_iter() .filter_map(Maybe::into_option) .collect(), + excludes: excludes + .into_iter() + .filter_map(Maybe::into_option) + .collect(), build_constraints: build_constraints .into_iter() .filter_map(Maybe::into_option) @@ -2536,6 +2575,7 @@ impl PipInstallSettings { dry_run: DryRun::from_args(dry_run), constraints_from_workspace, overrides_from_workspace, + excludes_from_workspace, build_constraints_from_workspace, modifications: if flag(exact, inexact, "inexact").unwrap_or(false) { Modifications::Exact diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index b9d54bd44..5810b9e44 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -1774,6 +1774,203 @@ fn lock_project_with_override_sources() -> Result<()> { Ok(()) } +/// Lock a project with `uv.tool.exclude-dependencies`. +#[test] +fn lock_project_with_excludes() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["flask==3.0.0"] + + [tool.uv] + exclude-dependencies = ["werkzeug"] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 8 packages in [TIME] + "###); + + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 8 packages in [TIME] + "###); + + // Install the base dependencies from the lockfile. + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Prepared 6 packages in [TIME] + Installed 6 packages in [TIME] + + blinker==1.7.0 + + click==8.1.7 + + flask==3.0.0 + + itsdangerous==2.1.2 + + jinja2==3.1.3 + + markupsafe==2.1.5 + "###); + + // Check the lockfile contains the excludes. + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r#" + version = 1 + revision = 3 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + excludes = ["werkzeug"] + + [[package]] + name = "blinker" + version = "1.7.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a1/13/6df5fc090ff4e5d246baf1f45fe9e5623aa8565757dfa5bd243f6a545f9e/blinker-1.7.0.tar.gz", hash = "sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182", size = 28134, upload-time = "2023-11-01T22:06:01.588Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/2a/7f3714cbc6356a0efec525ce7a0613d581072ed6eb53eb7b9754f33db807/blinker-1.7.0-py3-none-any.whl", hash = "sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9", size = 13068, upload-time = "2023-11-01T22:06:00.162Z" }, + ] + + [[package]] + name = "click" + version = "8.1.7" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121, upload-time = "2023-08-17T17:29:11.868Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941, upload-time = "2023-08-17T17:29:10.08Z" }, + ] + + [[package]] + name = "colorama" + version = "0.4.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, + ] + + [[package]] + name = "flask" + version = "3.0.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/d8/09/c1a7354d3925a3c6c8cfdebf4245bae67d633ffda1ba415add06ffc839c5/flask-3.0.0.tar.gz", hash = "sha256:cfadcdb638b609361d29ec22360d6070a77d7463dcb3ab08d2c2f2f168845f58", size = 674171, upload-time = "2023-09-30T14:36:12.918Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl", hash = "sha256:21128f47e4e3b9d597a3e8521a329bf56909b690fcc3fa3e477725aa81367638", size = 99724, upload-time = "2023-09-30T14:36:10.961Z" }, + ] + + [[package]] + name = "itsdangerous" + version = "2.1.2" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/7f/a1/d3fb83e7a61fa0c0d3d08ad0a94ddbeff3731c05212617dff3a94e097f08/itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a", size = 56143, upload-time = "2022-03-24T15:12:15.102Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/68/5f/447e04e828f47465eeab35b5d408b7ebaaaee207f48b7136c5a7267a30ae/itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44", size = 15749, upload-time = "2022-03-24T15:12:13.2Z" }, + ] + + [[package]] + name = "jinja2" + version = "3.1.3" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "markupsafe" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/b2/5e/3a21abf3cd467d7876045335e681d276ac32492febe6d98ad89562d1a7e1/Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90", size = 268261, upload-time = "2024-01-10T23:12:21.133Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/30/6d/6de6be2d02603ab56e72997708809e8a5b0fbfee080735109b40a3564843/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa", size = 133236, upload-time = "2024-01-10T23:12:19.504Z" }, + ] + + [[package]] + name = "markupsafe" + version = "2.1.5" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384, upload-time = "2024-02-02T16:31:22.863Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215, upload-time = "2024-02-02T16:30:33.081Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069, upload-time = "2024-02-02T16:30:34.148Z" }, + { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452, upload-time = "2024-02-02T16:30:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462, upload-time = "2024-02-02T16:30:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869, upload-time = "2024-02-02T16:30:37.834Z" }, + { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906, upload-time = "2024-02-02T16:30:39.366Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296, upload-time = "2024-02-02T16:30:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038, upload-time = "2024-02-02T16:30:42.243Z" }, + { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572, upload-time = "2024-02-02T16:30:43.326Z" }, + { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127, upload-time = "2024-02-02T16:30:44.418Z" }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "flask" }, + ] + + [package.metadata] + requires-dist = [{ name = "flask", specifier = "==3.0.0" }] + "# + ); + }); + + // Modify the excludes and verify that `--locked` fails. + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["flask==3.0.0"] + + [tool.uv] + exclude-dependencies = ["jinja2"] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Resolved 8 packages in [TIME] + The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`. + "); + + Ok(()) +} + /// Lock a project with `uv.tool.constraint-dependencies`. #[test] fn lock_project_with_constraints() -> Result<()> { diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 9ee19e5c4..ec90005bf 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -98,9 +98,11 @@ fn resolve_uv_toml() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -299,9 +301,11 @@ fn resolve_uv_toml() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -501,9 +505,11 @@ fn resolve_uv_toml() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -735,9 +741,11 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -938,9 +946,11 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -1117,9 +1127,11 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -1345,9 +1357,11 @@ fn resolve_index_url() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -1581,9 +1595,11 @@ fn resolve_index_url() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -1875,9 +1891,11 @@ fn resolve_find_links() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -2100,9 +2118,11 @@ fn resolve_top_level() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -2284,9 +2304,11 @@ fn resolve_top_level() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -2518,9 +2540,11 @@ fn resolve_top_level() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -2775,9 +2799,11 @@ fn resolve_user_configuration() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -2949,9 +2975,11 @@ fn resolve_user_configuration() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -3123,9 +3151,11 @@ fn resolve_user_configuration() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -3299,9 +3329,11 @@ fn resolve_user_configuration() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -3496,6 +3528,7 @@ fn resolve_tool() -> anyhow::Result<()> { with_editable: [], constraints: [], overrides: [], + excludes: [], build_constraints: [], python: None, python_platform: None, @@ -3681,9 +3714,11 @@ fn resolve_poetry_toml() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -3889,9 +3924,11 @@ fn resolve_both() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -4136,9 +4173,11 @@ fn resolve_both_special_fields() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -4462,9 +4501,11 @@ fn resolve_config_file() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -4642,7 +4683,7 @@ fn resolve_config_file() -> anyhow::Result<()> { | 1 | [project] | ^^^^^^^ - unknown field `project`, expected one of `required-version`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `config-settings-package`, `no-build-isolation`, `no-build-isolation-package`, `extra-build-dependencies`, `extra-build-variables`, `exclude-newer`, `exclude-newer-package`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend` + unknown field `project`, expected one of `required-version`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `config-settings-package`, `no-build-isolation`, `no-build-isolation-package`, `extra-build-dependencies`, `extra-build-variables`, `exclude-newer`, `exclude-newer-package`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `pip`, `cache-keys`, `override-dependencies`, `exclude-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend` " ); @@ -4763,9 +4804,11 @@ fn resolve_skip_empty() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -4940,9 +4983,11 @@ fn resolve_skip_empty() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -5136,9 +5181,11 @@ fn allow_insecure_host() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -5324,9 +5371,11 @@ fn index_priority() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -5560,9 +5609,11 @@ fn index_priority() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -5802,9 +5853,11 @@ fn index_priority() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -6039,9 +6092,11 @@ fn index_priority() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -6283,9 +6338,11 @@ fn index_priority() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -6520,9 +6577,11 @@ fn index_priority() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -6771,10 +6830,12 @@ fn verify_hashes() -> anyhow::Result<()> { editables: [], constraints: [], overrides: [], + excludes: [], build_constraints: [], dry_run: Disabled, constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], modifications: Sufficient, refresh: None( @@ -6938,10 +6999,12 @@ fn verify_hashes() -> anyhow::Result<()> { editables: [], constraints: [], overrides: [], + excludes: [], build_constraints: [], dry_run: Disabled, constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], modifications: Sufficient, refresh: None( @@ -7103,10 +7166,12 @@ fn verify_hashes() -> anyhow::Result<()> { editables: [], constraints: [], overrides: [], + excludes: [], build_constraints: [], dry_run: Disabled, constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], modifications: Sufficient, refresh: None( @@ -7270,10 +7335,12 @@ fn verify_hashes() -> anyhow::Result<()> { editables: [], constraints: [], overrides: [], + excludes: [], build_constraints: [], dry_run: Disabled, constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], modifications: Sufficient, refresh: None( @@ -7435,10 +7502,12 @@ fn verify_hashes() -> anyhow::Result<()> { editables: [], constraints: [], overrides: [], + excludes: [], build_constraints: [], dry_run: Disabled, constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], modifications: Sufficient, refresh: None( @@ -7601,10 +7670,12 @@ fn verify_hashes() -> anyhow::Result<()> { editables: [], constraints: [], overrides: [], + excludes: [], build_constraints: [], dry_run: Disabled, constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], modifications: Sufficient, refresh: None( @@ -8488,9 +8559,11 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -8663,9 +8736,11 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -8861,9 +8936,11 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -9034,9 +9111,11 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -9201,9 +9280,11 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -9369,9 +9450,11 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -10367,9 +10450,11 @@ fn build_isolation_override() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], @@ -10537,9 +10622,11 @@ fn build_isolation_override() -> anyhow::Result<()> { ], constraints: [], overrides: [], + excludes: [], build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + excludes_from_workspace: [], build_constraints_from_workspace: [], environments: SupportedEnvironments( [], diff --git a/docs/reference/cli.md b/docs/reference/cli.md index dbea7db0b..4f1f447ce 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -2613,7 +2613,9 @@ uv tool install [OPTIONS]

May also be set with the UV_EXCLUDE_NEWER environment variable.

--exclude-newer-package exclude-newer-package

Limit candidate packages for specific packages to those that were uploaded prior to the given date.

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.

Can be provided multiple times for different packages.

-
--extra-index-url extra-index-url

(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

+
--excludes, --exclude excludes

Exclude packages from resolution using the given requirements files.

+

Excludes files are requirements.txt-like files that specify packages to exclude from the resolution. When a package is excluded, it will be omitted from the dependency list entirely and its own dependencies will be ignored during the resolution phase. Excludes are unconditional in that requirement specifiers and markers are ignored; any package listed in the provided file will be omitted from all resolved environments.

+

May also be set with the UV_EXCLUDE environment variable.

--extra-index-url extra-index-url

(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.

May also be set with the UV_EXTRA_INDEX_URL environment variable.

Locations to search for candidate distributions, in addition to those found in the registry indexes.

@@ -4054,7 +4056,9 @@ uv pip compile [OPTIONS] >

May also be set with the UV_EXCLUDE_NEWER environment variable.

--exclude-newer-package exclude-newer-package

Limit candidate packages for a specific package to those that were uploaded prior to the given date.

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.

Can be provided multiple times for different packages.

-
--extra extra

Include optional dependencies from the specified extra name; may be provided more than once.

+
--excludes, --exclude excludes

Exclude packages from resolution using the given requirements files.

+

Excludes files are requirements.txt-like files that specify packages to exclude from the resolution. When a package is excluded, it will be omitted from the dependency list entirely and its own dependencies will be ignored during the resolution phase. Excludes are unconditional in that requirement specifiers and markers are ignored; any package listed in the provided file will be omitted from all resolved environments.

+

May also be set with the UV_EXCLUDE environment variable.

--extra extra

Include optional dependencies from the specified extra name; may be provided more than once.

Only applies to pyproject.toml, setup.py, and setup.cfg sources.

--extra-index-url extra-index-url

(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

@@ -4647,7 +4651,9 @@ uv pip install [OPTIONS] |--editable May also be set with the UV_EXCLUDE_NEWER environment variable.

--exclude-newer-package exclude-newer-package

Limit candidate packages for specific packages to those that were uploaded prior to the given date.

Accepts package-date pairs in the format PACKAGE=DATE, where DATE is an RFC 3339 timestamp (e.g., 2006-12-02T02:07:43Z) or local date (e.g., 2006-12-02) in your system's configured time zone.

Can be provided multiple times for different packages.

-
--extra extra

Include optional dependencies from the specified extra name; may be provided more than once.

+
--excludes, --exclude excludes

Exclude packages from resolution using the given requirements files.

+

Excludes files are requirements.txt-like files that specify packages to exclude from the resolution. When a package is excluded, it will be omitted from the dependency list entirely and its own dependencies will be ignored during the resolution phase. Excludes are unconditional in that requirement specifiers and markers are ignored; any package listed in the provided file will be omitted from all resolved environments.

+

May also be set with the UV_EXCLUDE environment variable.

--extra extra

Include optional dependencies from the specified extra name; may be provided more than once.

Only applies to pylock.toml, pyproject.toml, setup.py, and setup.cfg sources.

--extra-index-url extra-index-url

(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

diff --git a/docs/reference/environment.md b/docs/reference/environment.md index eb0daae6e..a0ff012c8 100644 --- a/docs/reference/environment.md +++ b/docs/reference/environment.md @@ -15,7 +15,7 @@ Python can lead to unexpected behavior. ### `UV_BUILD_CONSTRAINT` added in `0.2.34` -Equivalent to the `--build-constraint` command-line argument. If set, uv will use this file +Equivalent to the `--build-constraints` command-line argument. If set, uv will use this file as constraints for any source distribution builds. Uses space-separated list of files. ### `UV_CACHE_DIR` @@ -62,7 +62,7 @@ local `uv.toml` file to use as the configuration file. ### `UV_CONSTRAINT` added in `0.1.36` -Equivalent to the `--constraint` command-line argument. If set, uv will use this +Equivalent to the `--constraints` command-line argument. If set, uv will use this file as the constraints file. Uses space-separated list of files. ### `UV_CREDENTIALS_DIR` @@ -103,6 +103,12 @@ compatibility. `.env` files from which to load environment variables when executing `uv run` commands. +### `UV_EXCLUDE` +added in `0.9.8` + +Equivalent to the `--excludes` command-line argument. If set, uv will use this +as the excludes file. Uses space-separated list of files. + ### `UV_EXCLUDE_NEWER` added in `0.2.12` @@ -405,7 +411,7 @@ Equivalent to the `--offline` command-line argument. If set, uv will disable net ### `UV_OVERRIDE` added in `0.2.22` -Equivalent to the `--override` command-line argument. If set, uv will use this file +Equivalent to the `--overrides` command-line argument. If set, uv will use this file as the overrides file. Uses space-separated list of files. ### `UV_PRERELEASE` diff --git a/docs/reference/settings.md b/docs/reference/settings.md index 9249d47b5..2073d8244 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -202,6 +202,37 @@ environments = ["sys_platform == 'darwin'"] --- +### [`exclude-dependencies`](#exclude-dependencies) {: #exclude-dependencies } + +Dependencies to exclude when resolving the project's dependencies. + +Excludes are used to prevent a package from being selected during resolution, +regardless of whether it's requested by any other package. When a package is excluded, +it will be omitted from the dependency list entirely. + +Including a package as an exclusion will prevent it from being installed, even if +it's requested by transitive dependencies. This can be useful for removing optional +dependencies or working around packages with broken dependencies. + +!!! note + In `uv lock`, `uv sync`, and `uv run`, uv will only read `exclude-dependencies` from + the `pyproject.toml` at the workspace root, and will ignore any declarations in other + workspace members or `uv.toml` files. + +**Default value**: `[]` + +**Type**: `list[str]` + +**Example usage**: + +```toml title="pyproject.toml" +[tool.uv] +# Exclude Werkzeug from being installed, even if transitive dependencies request it. +exclude-dependencies = ["werkzeug"] +``` + +--- + ### [`index`](#index) {: #index } The indexes to use when resolving dependencies. diff --git a/uv.schema.json b/uv.schema.json index a37f60819..d16e4a1aa 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -203,6 +203,16 @@ "type": "string" } }, + "exclude-dependencies": { + "description": "Package names to exclude, e.g., `werkzeug`, `numpy`.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, "exclude-newer": { "description": "Limit candidate packages to those that were uploaded prior to a given point in time.\n\nAccepts a superset of [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html) (e.g.,\n`2006-12-02T02:07:43Z`). A full timestamp is required to ensure that the resolver will\nbehave consistently across timezones.", "anyOf": [