diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index d28d92019128e..80cd4ca7756cd 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 0000000000000..2c09a8740372d --- /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 500a7aa387692..931634812bbb5 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 80a3c35ea3c2e..bb8d8adbfd8fe 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 5bf6c64b8c286..4e6a509a1b5c3 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 0a847c0c2e427..7c04e11efbfc2 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 8c75ee86628d2..7bddd799b75ff 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 1daf5a47dc3f4..8bf591754be59 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 17c0614948b57..05079e25c1f6c 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 17c0614948b57..05079e25c1f6c 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 0870ee2983e6d..2e0c24a1de668 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 17c0614948b57..05079e25c1f6c 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 70e0a0f710319..20b59b7d91bad 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 08873c5c4de4f..b470f5ac2cf13 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 7b15016cc7f19..ade8411b70a95 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 5afb02d76707a..75794fe76d5d6 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 6533a3aab80ff..fd39470de97e9 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 dd574d576cbc8..e893a82b059b7 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 c9fcc16b714b9..6b9b8e98ff715 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 6042af68da881..32901020b4d1c 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 6e2bb5204db87..60a6079ee4ee3 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 cd643d5fefe9e..74525c6e0c9a6 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 969a1b71e17e0..6bd329d6e2447 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 aa89884ea4c2a..351f9228a9e87 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 b513faa79eed9..9f98fe4269324 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 5a50fe769c7c2..522f6478d3988 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 0cc7c9d9a3d40..8da323c6e3f97 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 5cafbf681e2ad..9374146ef2c2a 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 de06902464a6b..900085cf486ff 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 9bd1a8222be72..b5fdc99a97db6 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 644642eb1bc8c..b819ce4070709 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 0f35ad70ce0bb..f0633776fc999 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 918d014f9bcca..8bb917a53c136 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 ca5b520690e14..72d17c5abac5b 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 01a7e7c189553..4f44b36eb3014 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 b9d54bd44d0ba..5810b9e4400e4 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 9ee19e5c41847..ec90005bf3ea8 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 dbea7db0b68c5..4f1f447cefc3d 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 eb0daae6ef91a..a0ff012c856b7 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 9249d47b5e85a..2073d824428f9 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 a37f6081985d6..d16e4a1aabe14 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": [