From 339eb2ac838b0ae016e0c15cc4628b2dcfabf9fd Mon Sep 17 00:00:00 2001 From: Robin Lewis Date: Fri, 28 Mar 2025 07:17:08 -0400 Subject: [PATCH 1/9] test: fixture for dep groups with conflicts --- .../dependency-group-conflicts/README.md | 0 .../dependency_group_conflicts/__init__.py | 0 .../dependency-group-conflicts/pyproject.toml | 30 ++++++++ .../dependency-group-conflicts/uv.lock | 71 +++++++++++++++++++ 4 files changed, 101 insertions(+) create mode 100644 lib/fixtures/dependency-group-conflicts/README.md create mode 100644 lib/fixtures/dependency-group-conflicts/dependency_group_conflicts/__init__.py create mode 100644 lib/fixtures/dependency-group-conflicts/pyproject.toml create mode 100644 lib/fixtures/dependency-group-conflicts/uv.lock diff --git a/lib/fixtures/dependency-group-conflicts/README.md b/lib/fixtures/dependency-group-conflicts/README.md new file mode 100644 index 0000000..e69de29 diff --git a/lib/fixtures/dependency-group-conflicts/dependency_group_conflicts/__init__.py b/lib/fixtures/dependency-group-conflicts/dependency_group_conflicts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/fixtures/dependency-group-conflicts/pyproject.toml b/lib/fixtures/dependency-group-conflicts/pyproject.toml new file mode 100644 index 0000000..8217d93 --- /dev/null +++ b/lib/fixtures/dependency-group-conflicts/pyproject.toml @@ -0,0 +1,30 @@ +[project] +name = "dependency-group-conflicts" +version = "0.1.0" +description = "Testing conflicts in dependency groups" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [] + +# Dependency groups are rendered in uv.lock as package.dev-dependencies +[dependency-groups] +group-a = ["urllib3"] +group-b = ["arpeggio"] +group-c = ["tqdm"] + +[tool.uv] +default-groups = [ "group-a" ] +conflicts = [ + [ + { group = "group-a" }, + { group = "group-b" }, + ], + [ + { group = "group-a" }, + { group = "group-c" }, + ], +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/lib/fixtures/dependency-group-conflicts/uv.lock b/lib/fixtures/dependency-group-conflicts/uv.lock new file mode 100644 index 0000000..8721237 --- /dev/null +++ b/lib/fixtures/dependency-group-conflicts/uv.lock @@ -0,0 +1,71 @@ +version = 1 +requires-python = ">=3.11" +conflicts = [[ + { package = "dependency-group-conflicts", group = "group-a" }, + { package = "dependency-group-conflicts", group = "group-b" }, +], [ + { package = "dependency-group-conflicts", group = "group-a" }, + { package = "dependency-group-conflicts", group = "group-c" }, +]] + +[[package]] +name = "arpeggio" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/c4/516bb54456f85ad1947702ea4cef543a59de66d31a9887dbc3d9df36e3e1/Arpeggio-2.0.2.tar.gz", hash = "sha256:c790b2b06e226d2dd468e4fbfb5b7f506cec66416031fde1441cf1de2a0ba700", size = 766643 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/4f/d28bf30a19d4649b40b501d531b44e73afada99044df100380fd9567e92f/Arpeggio-2.0.2-py2.py3-none-any.whl", hash = "sha256:f7c8ae4f4056a89e020c24c7202ac8df3e2bc84e416746f20b0da35bb1de0250", size = 55287 }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "dependency-group-conflicts" +version = "0.1.0" +source = { editable = "." } + +[package.dev-dependencies] +group-a = [ + { name = "urllib3" }, +] +group-b = [ + { name = "arpeggio" }, +] +group-c = [ + { name = "tqdm" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +group-a = [{ name = "urllib3" }] +group-b = [{ name = "arpeggio" }] +group-c = [{ name = "tqdm" }] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, +] + +[[package]] +name = "urllib3" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, +] From e6e30104d00a055da339e7b678755ab208ad4aaf Mon Sep 17 00:00:00 2001 From: Robin Lewis Date: Fri, 28 Mar 2025 07:15:36 -0400 Subject: [PATCH 2/9] test: ensure eval abort in conflicting groups --- dev/checks.nix | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dev/checks.nix b/dev/checks.nix index a46b3ed..5043f49 100644 --- a/dev/checks.nix +++ b/dev/checks.nix @@ -80,9 +80,17 @@ let sourcePreference: let mkCheck = mkCheck' sourcePreference; + # Returns true iff this fails to evaluate + mkFail = args: ! (builtins.tryEval (mkCheck args)).success; nameSuffix = if sourcePreference == "wheel" then "" else "-pref-${sourcePreference}"; in + assert mkFail { + root = ../lib/fixtures/dependency-group-conflicts; + spec = { + dependency-group-conflicts = [ "group-a" "group-b" ]; + }; + }; mapAttrs' (name: v: nameValuePair "${name}${nameSuffix}" v) { trivial = mkCheck { root = ../lib/fixtures/trivial; From d6a0c9b2e90bf97491ecd934ffd123be7eab91e1 Mon Sep 17 00:00:00 2001 From: Robin Lewis Date: Fri, 28 Mar 2025 01:35:44 -0400 Subject: [PATCH 3/9] test: fix use of ! in bash test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bash is a nightmare: $ ( set -e ; echo y ; true ; echo n ) y n $ ( set -e ; echo y ; false ; echo n ) y $ ( set -e ; echo y ; ! true ; echo n ) y n It gets worse: $ f () { echo y ; false ; echo n ; } $ ( set -e ; f ) y $ ( set -e ; ! f ) y n Avoid, lol. 🥲 --- dev/checks.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/checks.nix b/dev/checks.nix index 5043f49..297e1c9 100644 --- a/dev/checks.nix +++ b/dev/checks.nix @@ -196,7 +196,7 @@ let }; # Check that arpeggio _isn't_ available check = '' - ! python -c "import arpeggio" + python -c "import arpeggio" && exit 1 ''; }; From e1b487688c70b54e5a439d688832fbed9d1191bc Mon Sep 17 00:00:00 2001 From: Robin Lewis Date: Fri, 28 Mar 2025 07:17:08 -0400 Subject: [PATCH 4/9] test: tickle a bug in group conflict resolution --- dev/checks.nix | 41 +++++++++++++++++++++++++++++++++++++++++ lib/test_workspace.nix | 20 ++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/dev/checks.nix b/dev/checks.nix index 297e1c9..d43f74f 100644 --- a/dev/checks.nix +++ b/dev/checks.nix @@ -152,10 +152,51 @@ let }; dependencyGroups = mkCheck { + name = "dependency-groups"; root = ../lib/fixtures/dependency-groups; spec = { dependency-groups = [ "group-a" ]; }; + check = '' + python -c 'import urllib3' + python -c 'import arpeggio' && exit 1 + ''; + }; + + dependencyGroupNoSelect = mkCheck { + name = "dependency-groups-noselect"; + root = ../lib/fixtures/dependency-groups; + spec = { + dependency-groups = [ ]; + }; + check = '' + python -c 'import urllib3' && exit 1 + python -c 'import arpeggio' && exit 1 + ''; + }; + + dependencyGroupConflictsA = mkCheck { + name = "dependency-groups-a"; + root = ../lib/fixtures/dependency-group-conflicts; + spec = { + dependency-group-conflicts = [ "group-a" ]; + }; + check = '' + python -c 'import urllib3' + python -c 'import arpeggio' && exit 1 + ''; + }; + + dependencyGroupConflictsB = mkCheck { + name = "dependency-groups-b"; + root = ../lib/fixtures/dependency-group-conflicts; + spec = { + dependency-group-conflicts = [ "group-b" ]; + }; + check = '' + python -c 'import urllib3' && exit 1 + python -c 'import arpeggio' + ''; }; optionalDeps = mkCheck { diff --git a/lib/test_workspace.nix b/lib/test_workspace.nix index 6ef5a87..f032a52 100644 --- a/lib/test_workspace.nix +++ b/lib/test_workspace.nix @@ -120,6 +120,26 @@ in }; }; }; + + testConflictingDependencyGroups = { + expr = mkTest ./fixtures/dependency-group-conflicts; + expected = rec { + all = groups; + groups = { + dependency-group-conflicts = [ + "group-a" + "group-b" + "group-c" + ]; + }; + optionals = { + dependency-group-conflicts = [ ]; + }; + default = { + dependency-group-conflicts = [ ]; + }; + }; + }; }; # Test workspaceRoot passed as a string From ebf3f2ee82d950c6e34f3abebca43da14af90694 Mon Sep 17 00:00:00 2001 From: Robin Lewis Date: Fri, 28 Mar 2025 01:45:00 -0400 Subject: [PATCH 5/9] fix: group conflict resolution --- lib/lock1.nix | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/lib/lock1.nix b/lib/lock1.nix index 4cb995d..1bfc391 100644 --- a/lib/lock1.nix +++ b/lib/lock1.nix @@ -241,27 +241,33 @@ fix (self: { ( let # Get a list of deselected dependency conflicts to filter - deselected' = concatMap ( - conflict: - let - # Find a single conflict branch to select - resolution = partition ( - def: - let - extras' = - spec.${def.package} or (throw "Package '${spec.package}' not present in resolution specification"); - in - elem (def.extra or def.group) extras' - ) conflict; - - in - throwIf (length resolution.right == 0) + extras' = pkg: + spec.${pkg} or (throw "Package '${spec.package}' not present in resolution specification"); + conflictEntryRelevant = def: elem (def.extra or def.group) (extras' def.package); + # Every element is a uv conflict declaration parsed into two lists: + # all items which apply to this spec, and all which don’t. + # + # [ + # { right = [ ]; wrong = [ ... ]; } + # ... + # ] + conflictsRes = map (partition conflictEntryRelevant) lock.conflicts; + # Any conflict declaration in which there is not a _single_ + # declaration which is relevant to this specification is completely + # irrelevant, and we should just ignore it wholesale. All the rest + # can be merged into a single declaration. + conflictMerged = lib.mapAttrs (_: lib.unique) + (lib.zipAttrsWith (_: lib.flatten) + (builtins.filter (c: 0 < builtins.length c.right) conflictsRes)); + right = conflictMerged.right or []; + wrong = conflictMerged.wrong or []; + deselected' = + throwIf (length right == 0) "Conflict resolution selected no conflict specifier. Misspelled extra/group?" throwIf - (length resolution.right > 1) - "Conflict resolution selected more than one conflict specifier, resolution still ambigious" - resolution.wrong - ) lock.conflicts; + (length right > 1) + "Conflict resolution selected more than one conflict specifier, resolution still ambigious: ${lib.concatMapStringsSep ", " builtins.toJSON right}" + wrong; deselected = groupBy (def: def.package) deselected'; in # Return rewritten lock without conflicts From cb65f1ff9bd454238f222056eb7c263536e9a010 Mon Sep 17 00:00:00 2001 From: Robin Lewis Date: Fri, 28 Mar 2025 04:34:58 -0400 Subject: [PATCH 6/9] test: dep groups with conflicts and no selection --- dev/checks.nix | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/dev/checks.nix b/dev/checks.nix index d43f74f..b0d0441 100644 --- a/dev/checks.nix +++ b/dev/checks.nix @@ -175,6 +175,19 @@ let ''; }; + dependencyGroupNone = mkCheck { + name = "dependency-group-conflicts-noselect"; + root = ../lib/fixtures/dependency-group-conflicts; + spec = { + dependency-group-conflicts = [ ]; + }; + check = '' + python -c 'import urllib3' && exit 1 + python -c 'import arpeggio' && exit 1 + python -c 'import tqdm' && exit 1 + ''; + }; + dependencyGroupConflictsA = mkCheck { name = "dependency-groups-a"; root = ../lib/fixtures/dependency-group-conflicts; @@ -184,6 +197,7 @@ let check = '' python -c 'import urllib3' python -c 'import arpeggio' && exit 1 + python -c 'import tqdm' && exit 1 ''; }; @@ -196,6 +210,7 @@ let check = '' python -c 'import urllib3' && exit 1 python -c 'import arpeggio' + python -c 'import tqdm' && exit 1 ''; }; From 915cd35353c867f429416ac92f72e5fa2d1c6633 Mon Sep 17 00:00:00 2001 From: Robin Lewis Date: Fri, 28 Mar 2025 04:38:07 -0400 Subject: [PATCH 7/9] test: dep group with conflicts and multi groups --- dev/checks.nix | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/dev/checks.nix b/dev/checks.nix index b0d0441..14c628b 100644 --- a/dev/checks.nix +++ b/dev/checks.nix @@ -214,6 +214,19 @@ let ''; }; + dependencyGroupConflictsBC = mkCheck { + name = "dependency-groups-bc"; + root = ../lib/fixtures/dependency-group-conflicts; + spec = { + dependency-group-conflicts = [ "group-b" "group-c" ]; + }; + check = '' + python -c 'import urllib3' && exit 1 + python -c 'import arpeggio' + python -c 'import tqdm' + ''; + }; + optionalDeps = mkCheck { root = ../lib/fixtures/optional-deps; spec = { From 4ba517f3b20313979c9b9119018a13a76888276f Mon Sep 17 00:00:00 2001 From: Robin Lewis Date: Fri, 28 Mar 2025 04:35:20 -0400 Subject: [PATCH 8/9] fix: allow selecting no dep group from conflicts --- lib/lock1.nix | 105 +++++++++++++++++++++++--------------------------- 1 file changed, 48 insertions(+), 57 deletions(-) diff --git a/lib/lock1.nix b/lib/lock1.nix index 1bfc391..107c87b 100644 --- a/lib/lock1.nix +++ b/lib/lock1.nix @@ -235,63 +235,54 @@ fix (self: { lock, spec, }: - if lock.conflicts == [ ] then - lock - else - ( - let - # Get a list of deselected dependency conflicts to filter - extras' = pkg: - spec.${pkg} or (throw "Package '${spec.package}' not present in resolution specification"); - conflictEntryRelevant = def: elem (def.extra or def.group) (extras' def.package); - # Every element is a uv conflict declaration parsed into two lists: - # all items which apply to this spec, and all which don’t. - # - # [ - # { right = [ ]; wrong = [ ... ]; } - # ... - # ] - conflictsRes = map (partition conflictEntryRelevant) lock.conflicts; - # Any conflict declaration in which there is not a _single_ - # declaration which is relevant to this specification is completely - # irrelevant, and we should just ignore it wholesale. All the rest - # can be merged into a single declaration. - conflictMerged = lib.mapAttrs (_: lib.unique) - (lib.zipAttrsWith (_: lib.flatten) - (builtins.filter (c: 0 < builtins.length c.right) conflictsRes)); - right = conflictMerged.right or []; - wrong = conflictMerged.wrong or []; - deselected' = - throwIf (length right == 0) - "Conflict resolution selected no conflict specifier. Misspelled extra/group?" - throwIf - (length right > 1) - "Conflict resolution selected more than one conflict specifier, resolution still ambigious: ${lib.concatMapStringsSep ", " builtins.toJSON right}" - wrong; - deselected = groupBy (def: def.package) deselected'; - in - # Return rewritten lock without conflicts - assert deselected' != [ ]; - lock - // { - conflicts = [ ]; - package = map ( - pkg: - if !deselected ? ${pkg.name} then - pkg - else - pkg - // { - optional-dependencies = filterAttrs ( - n: _: !any (def: def ? extra && def.extra == n) deselected.${pkg.name} - ) pkg.optional-dependencies; - dev-dependencies = filterAttrs ( - n: _: !any (def: def ? group && def.group == n) deselected.${pkg.name} - ) pkg.dev-dependencies; - } - ) lock.package; - } - ); + let + # Get a list of deselected dependency conflicts to filter + extras' = pkg: + spec.${pkg} or (throw "Package '${spec.package}' not present in resolution specification"); + conflictEntryRelevant = def: elem (def.extra or def.group) (extras' def.package); + # Every element is a uv conflict declaration parsed into two lists: + # all items which apply to this spec, and all which don’t. + # + # [ + # { right = [ ]; wrong = [ ... ]; } + # ... + # ] + conflictsRes = map (partition conflictEntryRelevant) lock.conflicts; + # Any conflict declaration in which there is not a _single_ + # declaration which is relevant to this specification is completely + # irrelevant, and we should just ignore it wholesale. All the rest + # can be merged into a single declaration. + conflictMerged = lib.mapAttrs (_: lib.unique) + (lib.zipAttrsWith (_: lib.flatten) + (builtins.filter (c: 0 < builtins.length c.right) conflictsRes)); + right = conflictMerged.right or []; + wrong = conflictMerged.wrong or []; + deselected' = + throwIf + (length right > 1) + "Conflict resolution selected more than one conflict specifier, resolution still ambigious: ${lib.concatMapStringsSep ", " builtins.toJSON right}" + wrong; + deselected = groupBy (def: def.package) deselected'; + in + lock + // { + conflicts = [ ]; + package = map ( + pkg: + if !deselected ? ${pkg.name} then + pkg + else + pkg + // { + optional-dependencies = filterAttrs ( + n: _: !any (def: def ? extra && def.extra == n) deselected.${pkg.name} + ) pkg.optional-dependencies; + dev-dependencies = filterAttrs ( + n: _: !any (def: def ? group && def.group == n) deselected.${pkg.name} + ) pkg.dev-dependencies; + } + ) lock.package; + }; /* Parse unmarshaled uv.lock From bd34b442dd836e1d274a27eaa0899f41f8e82245 Mon Sep 17 00:00:00 2001 From: Robin Lewis Date: Fri, 28 Mar 2025 07:16:49 -0400 Subject: [PATCH 9/9] fix: dep group with conflicts and multi group --- lib/lock1.nix | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/lock1.nix b/lib/lock1.nix index 107c87b..741eee4 100644 --- a/lib/lock1.nix +++ b/lib/lock1.nix @@ -254,14 +254,15 @@ fix (self: { # can be merged into a single declaration. conflictMerged = lib.mapAttrs (_: lib.unique) (lib.zipAttrsWith (_: lib.flatten) - (builtins.filter (c: 0 < builtins.length c.right) conflictsRes)); - right = conflictMerged.right or []; - wrong = conflictMerged.wrong or []; - deselected' = - throwIf - (length right > 1) - "Conflict resolution selected more than one conflict specifier, resolution still ambigious: ${lib.concatMapStringsSep ", " builtins.toJSON right}" - wrong; + (builtins.filter (c: let + matches = builtins.length c.right; + in + throwIf + (matches > 1) + "Conflict resolution selected more than one conflict specifier, resolution still ambigious: ${lib.concatMapStringsSep ", " builtins.toJSON c.right}" + matches == 1 + ) conflictsRes)); + deselected' = conflictMerged.wrong or []; deselected = groupBy (def: def.package) deselected'; in lock