diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 99c2eb1b8..c89431847 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -460,7 +460,7 @@ pub struct ToolUv { )] pub environments: Option, - /// Conflicting extras may be declared here. + /// Conflicting extras or groups may be declared here. /// /// It's useful to declare conflicting extras when the extras have mutually /// incompatible dependencies. For example, extra `foo` might depend on @@ -481,12 +481,8 @@ pub struct ToolUv { /// fail. #[cfg_attr( feature = "schemars", - // Skipped for now while we iterate on this feature. - schemars(skip, description = "A list sets of conflicting groups or extras.") + schemars(description = "A list sets of conflicting groups or extras.") )] - /* - This is commented out temporarily while we finalize its - functionality and naming. This avoids it showing up in docs. #[option( default = r#"[]"#, value_type = "list[list[dict]]", @@ -500,9 +496,16 @@ pub struct ToolUv { { extra = "test2" }, ] ] + + # Or, to declare conflicting groups: + conflicts = [ + [ + { group = "test1" }, + { group = "test2" }, + ] + ] "# )] - */ pub conflicts: Option, } diff --git a/docs/reference/settings.md b/docs/reference/settings.md index fa947c3f0..70eff8f9c 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -1,4 +1,55 @@ ## Project metadata +### [`conflicts`](#conflicts) {: #conflicts } + +Conflicting extras or groups may be declared here. + +It's useful to declare conflicting extras when the extras have mutually +incompatible dependencies. For example, extra `foo` might depend on +`numpy==2.0.0` while extra `bar` might depend on `numpy==2.1.0`. These +extras cannot be activated at the same time. This usually isn't a +problem for pip-style workflows, but when using uv project support +with universal resolution, it will try to produce a resolution that +satisfies both extras simultaneously. + +When this happens, resolution will fail, because one cannot install +both `numpy 2.0.0` and `numpy 2.1.0` into the same environment. + +To work around this, you may specify `foo` and `bar` as conflicting +extras. When doing universal resolution in project mode, these extras +will get their own "forks" distinct from one another in order to permit +conflicting dependencies. In exchange, if one tries to install from the +lock file with both conflicting extras activated, installation will +fail. + +**Default value**: `[]` + +**Type**: `list[list[dict]]` + +**Example usage**: + +```toml title="pyproject.toml" +[tool.uv] +# Require that `package[test1]` and `package[test2]` +# requirements are resolved in different forks so that they +# cannot conflict with one another. +conflicts = [ + [ + { extra = "test1" }, + { extra = "test2" }, + ] +] + +# Or, to declare conflicting groups: +conflicts = [ + [ + { group = "test1" }, + { group = "test2" }, + ] +] +``` + +--- + ### [`constraint-dependencies`](#constraint-dependencies) {: #constraint-dependencies } Constraints to apply when resolving the project's dependencies. diff --git a/uv.schema.json b/uv.schema.json index aba29e3aa..a58e60345 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -76,6 +76,17 @@ } ] }, + "conflicts": { + "description": "A list sets of conflicting groups or extras.", + "anyOf": [ + { + "$ref": "#/definitions/SchemaConflicts" + }, + { + "type": "null" + } + ] + }, "constraint-dependencies": { "description": "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`.", "type": [ @@ -566,6 +577,35 @@ "$ref": "#/definitions/ConfigSettingValue" } }, + "ConflictPackage": { + "description": "The actual conflicting data for a package.\n\nThat is, either an extra or a group name.", + "oneOf": [ + { + "type": "object", + "required": [ + "Extra" + ], + "properties": { + "Extra": { + "$ref": "#/definitions/ExtraName" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "Group" + ], + "properties": { + "Group": { + "$ref": "#/definitions/GroupName" + } + }, + "additionalProperties": false + } + ] + }, "ExcludeNewer": { "description": "Exclude distributions uploaded after the given timestamp.\n\nAccepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same format (e.g., `2006-12-02`).", "type": "string", @@ -1316,6 +1356,43 @@ } ] }, + "SchemaConflictItem": { + "description": "Like [`ConflictItem`], but for deserialization in `pyproject.toml`.\n\nThe schema format is different from the in-memory format. Specifically, the schema format does not allow specifying the package name (or will make it optional in the future), where as the in-memory format needs the package name.", + "type": "object", + "required": [ + "conflict" + ], + "properties": { + "conflict": { + "$ref": "#/definitions/ConflictPackage" + }, + "package": { + "anyOf": [ + { + "$ref": "#/definitions/PackageName" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "SchemaConflictSet": { + "description": "Like [`ConflictSet`], but for deserialization in `pyproject.toml`.\n\nThe schema format is different from the in-memory format. Specifically, the schema format does not allow specifying the package name (or will make it optional in the future), where as the in-memory format needs the package name.", + "type": "array", + "items": { + "$ref": "#/definitions/SchemaConflictItem" + } + }, + "SchemaConflicts": { + "description": "Like [`Conflicts`], but for deserialization in `pyproject.toml`.\n\nThe schema format is different from the in-memory format. Specifically, the schema format does not allow specifying the package name (or will make it optional in the future), where as the in-memory format needs the package name.\n\nN.B. `Conflicts` is still used for (de)serialization. Specifically, in the lock file, where the package name is required.", + "type": "array", + "items": { + "$ref": "#/definitions/SchemaConflictSet" + } + }, "Source": { "description": "A `tool.uv.sources` value.", "anyOf": [