diff --git a/crates/uv-settings/src/combine.rs b/crates/uv-settings/src/combine.rs index 00c597655..b50de3962 100644 --- a/crates/uv-settings/src/combine.rs +++ b/crates/uv-settings/src/combine.rs @@ -20,7 +20,7 @@ use uv_resolver::{ PrereleaseMode, ResolutionMode, }; use uv_torch::TorchMode; -use uv_workspace::pyproject::ExtraBuildDependencies; +use uv_workspace::pyproject::{ExtraBuildDependencies, ToolUvSources}; use uv_workspace::pyproject_mut::AddBoundsKind; use crate::{FilesystemOptions, Options, PipOptions}; @@ -294,3 +294,23 @@ impl Combine for Option { } } } + +impl Combine for ToolUvSources { + fn combine(self, other: Self) -> Self { + // Merge sources from other into self, with self taking precedence + let mut combined = self.into_inner(); + for (package, sources) in other.into_inner() { + combined.entry(package).or_insert(sources); + } + combined.into_iter().collect() + } +} + +impl Combine for Option { + fn combine(self, other: Self) -> Self { + match (self, other) { + (Some(a), Some(b)) => Some(a.combine(b)), + (a, b) => a.or(b), + } + } +} diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs index 6b9b8e98f..17326cd5d 100644 --- a/crates/uv-settings/src/lib.rs +++ b/crates/uv-settings/src/lib.rs @@ -219,7 +219,7 @@ fn validate_uv_toml(path: &Path, options: &Options) -> Result<(), Error> { required_environments, conflicts, workspace, - sources, + sources: _, dev_dependencies, default_groups, dependency_groups, @@ -236,9 +236,6 @@ fn validate_uv_toml(path: &Path, options: &Options) -> Result<(), Error> { if workspace.is_some() { return Err(Error::PyprojectOnlyField(path.to_path_buf(), "workspace")); } - if sources.is_some() { - return Err(Error::PyprojectOnlyField(path.to_path_buf(), "sources")); - } if dev_dependencies.is_some() { return Err(Error::PyprojectOnlyField( path.to_path_buf(), diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index c25d5f0ed..f14ad54a4 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -139,8 +139,11 @@ pub struct Options { #[cfg_attr(feature = "schemars", schemars(skip))] pub workspace: Option, + // NOTE: Unlike other fields above, `sources` is allowed in both `pyproject.toml` + // and `uv.toml` files to support local development overrides without modifying + // the committed configuration. #[cfg_attr(feature = "schemars", schemars(skip))] - pub sources: Option, + pub sources: Option, #[cfg_attr(feature = "schemars", schemars(skip))] pub dev_dependencies: Option, @@ -2117,12 +2120,15 @@ pub struct OptionsWire { environments: Option, required_environments: Option, - // NOTE(charlie): These fields should be kept in-sync with `ToolUv` in + // NOTE: These fields should be kept in-sync with `ToolUv` in // `crates/uv-workspace/src/pyproject.rs`. The documentation lives on that struct. // They're only respected in `pyproject.toml` files, and should be rejected in `uv.toml` files. conflicts: Option, workspace: Option, - sources: Option, + // NOTE: Unlike other fields above, `sources` is allowed in both `pyproject.toml` + // and `uv.toml` files to support local development overrides without modifying + // the committed configuration. + sources: Option, managed: Option, r#package: Option, default_groups: Option, diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 74525c6e0..2acd1d290 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -731,6 +731,12 @@ impl<'de> serde::de::Deserialize<'de> for ToolUvSources { } } +impl FromIterator<(PackageName, Sources)> for ToolUvSources { + fn from_iter>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } +} + #[derive(Default, Debug, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(Serialize))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] diff --git a/docs/concepts/configuration-files.md b/docs/concepts/configuration-files.md index e034bd198..9fbb15e12 100644 --- a/docs/concepts/configuration-files.md +++ b/docs/concepts/configuration-files.md @@ -62,6 +62,14 @@ configuration tables, the project-level value will be used, and the user-level v ignored. If an array is present in both tables, the arrays will be concatenated, with the project-level settings appearing earlier in the merged array. +!!! tip "Local development overrides" + + The `[sources]` table can be defined in both `uv.toml` and `pyproject.toml`. When both are + present, sources from `uv.toml` override those from `pyproject.toml` on a per-package basis. + This enables local development workflows where different team members have dependencies at + different filesystem locations. See [Dependency sources](projects/dependencies.md#local-development-overrides-with-uvtoml) + for more details. + Settings provided via environment variables take precedence over persistent configuration, and settings provided via the command line take precedence over both. diff --git a/docs/concepts/projects/dependencies.md b/docs/concepts/projects/dependencies.md index a38bcc325..36c3cc4bd 100644 --- a/docs/concepts/projects/dependencies.md +++ b/docs/concepts/projects/dependencies.md @@ -216,6 +216,34 @@ dependencies = ["foo"] foo = { path = "./packages/foo" } ``` +### Local development overrides with `uv.toml` + +Sources can also be defined in a `uv.toml` file to enable local development overrides without +modifying the committed `pyproject.toml`. This is useful when: + +- Different developers have dependencies at different local paths +- You want to temporarily override a dependency without committing the change +- You want to use version control ignore patterns (e.g., `.gitignore`) to exclude local configuration + +```toml title="uv.toml" +[sources] +httpx = { path = "../httpx", editable = true } +my-package = { git = "https://github.com/me/my-package", branch = "dev" } +``` + +!!! note + + When both `uv.toml` and `pyproject.toml` define sources for the same package, the `uv.toml` + definition takes precedence. This allows local overrides without modifying the project + configuration. + +!!! tip + + Add `uv.toml` to your `.gitignore` to maintain local-only development configurations without + affecting other team members. + +### Supported source types + The following dependency sources are supported by uv: - [Index](#index): A package resolved from a specific package index.