diff --git a/crates/uv-distribution/src/pyproject.rs b/crates/uv-distribution/src/pyproject.rs index 258d1529be2e..fb2589285066 100644 --- a/crates/uv-distribution/src/pyproject.rs +++ b/crates/uv-distribution/src/pyproject.rs @@ -83,6 +83,14 @@ pub struct ToolUv { ) )] pub dev_dependencies: Option>>, + #[cfg_attr( + feature = "schemars", + schemars( + with = "Option>", + description = "PEP 508 style requirements, e.g. `flask==3.0.0`, or `black @ https://...`." + ) + )] + pub override_dependencies: Option>>, } #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 4cb0f62d4366..b1a506473ba4 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -4,6 +4,7 @@ use anstream::eprint; use distribution_types::{IndexLocations, UnresolvedRequirementSpecification}; use install_wheel_rs::linker::LinkMode; +use pep508_rs::RequirementOrigin; use uv_cache::Cache; use uv_client::{Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ @@ -124,7 +125,30 @@ pub(super) async fn do_lock( .map(UnresolvedRequirementSpecification::from) .collect(); let constraints = vec![]; - let overrides = vec![]; + + let mut overrides: Vec = vec![]; + for workspace_package in workspace.packages().values() { + if workspace_package.root() == workspace.root() { + if let Some(override_dependencies) = workspace_package + .pyproject_toml() + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.override_dependencies.as_ref()) + { + for override_dependency in override_dependencies { + let req = pypi_types::Requirement::from( + override_dependency + .clone() + .with_origin(RequirementOrigin::Workspace), + ); + overrides.push(UnresolvedRequirementSpecification::from(req)); + } + } + break; + } + } + let dev = vec![DEV_DEPENDENCIES.clone()]; let source_trees = vec![]; diff --git a/crates/uv/tests/lock.rs b/crates/uv/tests/lock.rs index 9abc58879eb5..5507f718da56 100644 --- a/crates/uv/tests/lock.rs +++ b/crates/uv/tests/lock.rs @@ -583,6 +583,56 @@ fn lock_project_extra() -> Result<()> { Ok(()) } +#[test] +fn lock_project_with_overrides() -> 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] + override-dependencies = ["werkzeug==2.3.8"] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning. + Resolved 9 packages in [TIME] + "###); + + // Install the base dependencies from the lockfile. + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv sync` is experimental and may change without warning. + Prepared 8 packages in [TIME] + Installed 8 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 + + project==0.1.0 (from file://[TEMP_DIR]/) + + werkzeug==2.3.8 + "###); + + Ok(()) +} /// Lock a project with a dependency that has an extra. #[test] fn lock_dependency_extra() -> Result<()> {