Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 78 additions & 1 deletion dev/checks.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -144,10 +152,79 @@ 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
'';
};

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;
spec = {
dependency-group-conflicts = [ "group-a" ];
};
check = ''
python -c 'import urllib3'
python -c 'import arpeggio' && exit 1
python -c 'import tqdm' && 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'
python -c 'import tqdm' && exit 1
'';
};

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 {
Expand Down Expand Up @@ -188,7 +265,7 @@ let
};
# Check that arpeggio _isn't_ available
check = ''
! python -c "import arpeggio"
python -c "import arpeggio" && exit 1
'';
};

Expand Down
Empty file.
30 changes: 30 additions & 0 deletions lib/fixtures/dependency-group-conflicts/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
71 changes: 71 additions & 0 deletions lib/fixtures/dependency-group-conflicts/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

100 changes: 49 additions & 51 deletions lib/lock1.nix
Original file line number Diff line number Diff line change
Expand Up @@ -235,57 +235,55 @@ fix (self: {
lock,
spec,
}:
if lock.conflicts == [ ] then
lock
else
(
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)
"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;
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 = [ <me> ]; wrong = [ <not me> <not me 2> ... ]; }
# ...
# ]
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: 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
// {
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
Expand Down
20 changes: 20 additions & 0 deletions lib/test_workspace.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading