diff --git a/crates/uv-scripts/src/lib.rs b/crates/uv-scripts/src/lib.rs index b80cdc219..1ad27d924 100644 --- a/crates/uv-scripts/src/lib.rs +++ b/crates/uv-scripts/src/lib.rs @@ -379,6 +379,7 @@ pub struct ToolUv { #[serde(flatten)] pub top_level: ResolverInstallerOptions, pub override_dependencies: Option>>, + pub excluded_dependencies: Option>>, pub constraint_dependencies: Option>>, pub build_constraint_dependencies: Option>>, pub sources: Option>, diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index d80ccce2f..39090e9de 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -111,6 +111,9 @@ pub struct Options { #[cfg_attr(feature = "schemars", schemars(skip))] pub override_dependencies: Option>>, + #[cfg_attr(feature = "schemars", schemars(skip))] + pub excluded_dependencies: Option>>, + #[cfg_attr(feature = "schemars", schemars(skip))] pub constraint_dependencies: Option>>, @@ -1860,6 +1863,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>>, + excluded_dependencies: Option>>, constraint_dependencies: Option>>, build_constraint_dependencies: Option>>, environments: Option, @@ -1928,6 +1932,7 @@ impl From for Options { pip, cache_keys, override_dependencies, + excluded_dependencies, constraint_dependencies, build_constraint_dependencies, environments, @@ -1996,6 +2001,7 @@ impl From for Options { cache_keys, build_backend, override_dependencies, + excluded_dependencies, constraint_dependencies, build_constraint_dependencies, environments, diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 124a62881..5a50ad5d7 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -435,6 +435,37 @@ pub struct ToolUv { )] pub override_dependencies: Option>>, + /// Excludes to apply when resolving the project's dependencies. + /// + /// Excludes are used to prevent installation of a specific package, even if other dependencies + /// require it. + /// + /// Including a package as an exclude will _not_ trigger installation of the package on its + /// own; instead, the package must be requested elsewhere in the project's first-party or + /// transitive dependencies. + /// + /// !!! note + /// In `uv lock`, `uv sync`, and `uv run`, uv will only read `excluded-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 = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`." + ) + )] + #[option( + default = "[]", + value_type = "list[str]", + example = r#" + # Always install Werkzeug 2.3.0, regardless of whether transitive dependencies request + # a different version. + excluded-dependencies = ["numpy"] + "# + )] + pub excluded_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 1349d739c..d74a74baa 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -1870,6 +1870,7 @@ mod tests { "dependency-groups": null, "dev-dependencies": null, "override-dependencies": null, + "excluded-dependencies": null, "constraint-dependencies": null, "build-constraint-dependencies": null, "environments": null, @@ -1966,6 +1967,7 @@ mod tests { "dependency-groups": null, "dev-dependencies": null, "override-dependencies": null, + "excluded-dependencies": null, "constraint-dependencies": null, "build-constraint-dependencies": null, "environments": null, @@ -2177,6 +2179,7 @@ mod tests { "dependency-groups": null, "dev-dependencies": null, "override-dependencies": null, + "excluded-dependencies": null, "constraint-dependencies": null, "build-constraint-dependencies": null, "environments": null, @@ -2285,6 +2288,7 @@ mod tests { "dependency-groups": null, "dev-dependencies": null, "override-dependencies": null, + "excluded-dependencies": null, "constraint-dependencies": null, "build-constraint-dependencies": null, "environments": null, @@ -2406,6 +2410,7 @@ mod tests { "dependency-groups": null, "dev-dependencies": null, "override-dependencies": null, + "excluded-dependencies": null, "constraint-dependencies": null, "build-constraint-dependencies": null, "environments": null, @@ -2501,6 +2506,7 @@ mod tests { "dependency-groups": null, "dev-dependencies": null, "override-dependencies": null, + "excluded-dependencies": null, "constraint-dependencies": null, "build-constraint-dependencies": null, "environments": null, diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 5851022b8..a2ac26b7f 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -1711,6 +1711,66 @@ fn lock_project_with_overrides() -> Result<()> { Ok(()) } +/// Lock a project with `uv.tool.excluded-dependencies` +#[test] +fn lock_project_with_excluded_dependencies() -> 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 = ["pytest==7.4.0", "requests==2.31.0"] + + [tool.uv] + excluded-dependencies = ["pytest"] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 11 packages in [TIME] + "); + + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 11 packages in [TIME] + "); + + // Install the dependencies from the lockfile. + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Prepared 9 packages in [TIME] + Installed 9 packages in [TIME] + + certifi==2024.2.2 + + charset-normalizer==3.3.2 + + idna==3.6 + + iniconfig==2.0.0 + + packaging==24.0 + + pluggy==1.4.0 + + pytest==7.4.0 + + requests==2.31.0 + + urllib3==2.2.1 + "); + + Ok(()) +} /// Lock a project with `uv.tool.override-dependencies` that reference `tool.uv.sources`. #[test] diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index b99be1296..3fdb28a6e 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -3920,6 +3920,61 @@ fn override_dependency_from_pyproject() -> Result<()> { Ok(()) } +/// Check the `tool.uv.excluded-dependencies` in `pyproject.toml` is respected +/// Check that `tool.uv.excluded-dependencies` in `pyproject.toml` excludes dependencies. +#[test] +fn exclude_dependency_from_pyproject() -> Result<()> { + let context = TestContext::new("3.12"); + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#"[project] + name = "example" + version = "0.0.0" + dependencies = [ + "flask==3.0.0" + ] + + [tool.uv] + excluded-dependencies = [ + "werkzeug" + ] + "#, + )?; + + uv_snapshot!(context.filters(), context.pip_compile() + .arg("pyproject.toml") + .current_dir(&context.temp_dir) + , @r" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] pyproject.toml + blinker==1.7.0 + # via flask + click==8.1.7 + # via flask + flask==3.0.0 + # via example (pyproject.toml) + itsdangerous==2.1.2 + # via flask + jinja2==3.1.3 + # via flask + markupsafe==2.1.5 + # via + # jinja2 + # werkzeug + werkzeug==3.0.1 + # via flask + + ----- stderr ----- + Resolved 7 packages in [TIME] + " + ); + + Ok(()) +} + /// Check that `tool.uv.constraint-dependencies` in `pyproject.toml` is respected. #[test] fn constraint_dependency_from_pyproject() -> Result<()> { diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 7635bd523..2d9f967d3 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -3987,7 +3987,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`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `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`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `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`, `excluded-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend` " ); diff --git a/docs/reference/settings.md b/docs/reference/settings.md index 58948c80e..381fd398c 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -202,6 +202,37 @@ environments = ["sys_platform == 'darwin'"] --- +### [`excluded-dependencies`](#excluded-dependencies) {: #excluded-dependencies } + +Excludes to apply when resolving the project's dependencies. + +Excludes are used to prevent installation of a specific package, even if other dependencies +require it. + +Including a package as an exclude will _not_ trigger installation of the package on its +own; instead, the package must be requested elsewhere in the project's first-party or + transitive dependencies. + +!!! note + In `uv lock`, `uv sync`, and `uv run`, uv will only read `excluded-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] +# Always install Werkzeug 2.3.0, regardless of whether transitive dependencies request +# a different version. +excluded-dependencies = ["numpy"] +``` + +--- + ### [`index`](#index) {: #index } The indexes to use when resolving dependencies. diff --git a/uv.schema.json b/uv.schema.json index dbc4f1168..f2c888647 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -203,6 +203,16 @@ } ] }, + "excluded-dependencies": { + "description": "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, "extra-index-url": { "description": "Extra URLs of package indexes to use, in addition to `--index-url`.\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/)\n(the simple repository API), or a local directory laid out in the same format.\n\nAll indexes provided via this flag take priority over the index specified by\n[`index_url`](#index-url) or [`index`](#index) with `default = true`. When multiple indexes\nare provided, earlier values take priority.\n\nTo control uv's resolution strategy when multiple indexes are present, see\n[`index_strategy`](#index-strategy).\n\n(Deprecated: use `index` instead.)", "type": [