Skip to content

Commit 06a2cff

Browse files
committed
Attempt supporting multi-file version constraints
1 parent c18d8f2 commit 06a2cff

3 files changed

Lines changed: 126 additions & 9 deletions

File tree

conda_lock/models/lock_spec.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
from __future__ import annotations
2+
3+
import copy
14
import hashlib
25
import json
36
import pathlib
47
import typing
58

9+
from fnmatch import fnmatchcase
610
from typing import Dict, List, Optional, Union
711

812
from pydantic import BaseModel, Field, validator
@@ -24,23 +28,126 @@ class _BaseDependency(StrictModel):
2428
def sorted_extras(cls, v: List[str]) -> List[str]:
2529
return sorted(v)
2630

31+
def _merge_base(self, other: _BaseDependency) -> _BaseDependency:
32+
if other is None:
33+
return self
34+
if (
35+
self.name != other.name
36+
or self.manager != other.manager
37+
or self.category != other.category
38+
):
39+
raise ValueError(
40+
"Cannot merge incompatible dependencies: {self} != {other}"
41+
)
42+
return _BaseDependency(
43+
name=self.name,
44+
manager=self.manager,
45+
category=self.category,
46+
extras=list(set(self.extras + other.extras)),
47+
)
48+
2749

2850
class VersionedDependency(_BaseDependency):
2951
version: str
3052
build: Optional[str] = None
3153
conda_channel: Optional[str] = None
3254

55+
@staticmethod
56+
def _merge_matchspec(
57+
matchspec1: Optional[str], matchspec2: Optional[str]
58+
) -> Optional[str]:
59+
if matchspec1 == matchspec2:
60+
return copy.copy(matchspec1)
61+
if matchspec1 is None or matchspec1 == "":
62+
return copy.copy(matchspec2)
63+
if matchspec2 is None or matchspec2 == "":
64+
return copy.copy(matchspec1)
65+
if fnmatchcase(matchspec1, matchspec2):
66+
return copy.copy(matchspec1)
67+
if fnmatchcase(matchspec2, matchspec1):
68+
return copy.copy(matchspec2)
69+
return f"{matchspec1},{matchspec2}"
70+
71+
def merge(self, other: Optional[VersionedDependency]) -> VersionedDependency:
72+
if other is None:
73+
return self
74+
75+
if (
76+
self.conda_channel is not None
77+
and other.conda_channel is not None
78+
and self.conda_channel != other.conda_channel
79+
):
80+
raise ValueError(
81+
f"VersionedDependency has two different conda_channels:\n{self}\n{other}"
82+
)
83+
merged_base = self._merge_base(other)
84+
return VersionedDependency(
85+
name=merged_base.name,
86+
manager=merged_base.manager,
87+
category=merged_base.category,
88+
extras=merged_base.extras,
89+
version=self._merge_matchspec(self.version, other.version), # type: ignore
90+
build=self._merge_matchspec(self.build, other.build),
91+
conda_channel=self.conda_channel or other.conda_channel,
92+
)
93+
3394

3495
class URLDependency(_BaseDependency):
3596
url: str
3697
hashes: List[str]
3798

99+
def merge(self, other: Optional[URLDependency]) -> URLDependency:
100+
if other is None:
101+
return self
102+
if self.url != other.url:
103+
raise ValueError(f"URLDependency has two different urls:\n{self}\n{other}")
104+
105+
if self.hashes != other.hashes:
106+
raise ValueError(
107+
f"URLDependency has two different hashess:\n{self}\n{other}"
108+
)
109+
merged_base = self._merge_base(other)
110+
111+
return URLDependency(
112+
name=merged_base.name,
113+
manager=merged_base.manager,
114+
category=merged_base.category,
115+
extras=merged_base.extras,
116+
url=self.url,
117+
hashes=self.hashes,
118+
)
119+
38120

39121
class VCSDependency(_BaseDependency):
40122
source: str
41123
vcs: str
42124
rev: Optional[str] = None
43125

126+
def merge(self, other: Optional[VCSDependency]) -> VCSDependency:
127+
if other is None:
128+
return self
129+
if self.source != other.source:
130+
raise ValueError(
131+
f"VCSDependency has two different sources:\n{self}\n{other}"
132+
)
133+
134+
if self.vcs != other.vcs:
135+
raise ValueError(f"VCSDependency has two different vcss:\n{self}\n{other}")
136+
137+
if self.rev is not None and other.rev is not None and self.rev != other.rev:
138+
raise ValueError(f"VCSDependency has two different revs:\n{self}\n{other}")
139+
merged_base = self._merge_base(other)
140+
141+
return VCSDependency(
142+
name=merged_base.name,
143+
manager=merged_base.manager,
144+
category=merged_base.category,
145+
extras=merged_base.extras,
146+
source=self.source,
147+
vcs=self.vcs,
148+
rev=self.rev or other.rev,
149+
)
150+
44151

45152
Dependency = Union[VersionedDependency, URLDependency, VCSDependency]
46153

conda_lock/src_parser/aggregation.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ def aggregate_lock_specs(
3434
lock_spec.dependencies.get(platform, []) for lock_spec in lock_specs
3535
):
3636
key = (dep.manager, dep.name)
37-
unique_deps[key] = dep
37+
if unique_deps.get(key) is not None and type(unique_deps[key]) != type(dep):
38+
raise ValueError(
39+
f"Unsupported use of different dependency types for same package:\n{dep}\n{unique_deps[key]}"
40+
)
41+
unique_deps[key] = dep.merge(unique_deps.get(key)) # type: ignore
3842

3943
dependencies[platform] = list(unique_deps.values())
4044

tests/test_conda_lock.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1622,22 +1622,28 @@ def test_aggregate_lock_specs():
16221622
assert actual.content_hash() == expected.content_hash()
16231623

16241624

1625-
def test_aggregate_lock_specs_override_version():
1626-
base_spec = LockSpecification(
1627-
dependencies={"linux-64": [_make_spec("package", "=1.0")]},
1625+
def test_aggregate_lock_specs_combine_version():
1626+
first_spec = LockSpecification(
1627+
dependencies={"linux-64": [_make_spec("package", ">1.0")]},
16281628
channels=[Channel.from_string("conda-forge")],
16291629
sources=[Path("base.yml")],
16301630
)
16311631

1632-
override_spec = LockSpecification(
1633-
dependencies={"linux-64": [_make_spec("package", "=2.0")]},
1632+
second_spec = LockSpecification(
1633+
dependencies={"linux-64": [_make_spec("package", "<2.0")]},
1634+
channels=[Channel.from_string("internal"), Channel.from_string("conda-forge")],
1635+
sources=[Path("additional.yml")],
1636+
)
1637+
1638+
result_spec = LockSpecification(
1639+
dependencies={"linux-64": [_make_spec("package", "<2.0,>1.0")]},
16341640
channels=[Channel.from_string("internal"), Channel.from_string("conda-forge")],
1635-
sources=[Path("override.yml")],
1641+
sources=[Path("additional.yml")],
16361642
)
16371643

1638-
agg_spec = aggregate_lock_specs([base_spec, override_spec], platforms=["linux-64"])
1644+
agg_spec = aggregate_lock_specs([first_spec, second_spec], platforms=["linux-64"])
16391645

1640-
assert agg_spec.dependencies == override_spec.dependencies
1646+
assert agg_spec.dependencies == result_spec.dependencies
16411647

16421648

16431649
def test_aggregate_lock_specs_invalid_channels():

0 commit comments

Comments
 (0)