Skip to content

Commit

Permalink
Attempt supporting multi-file version constraints
Browse files Browse the repository at this point in the history
  • Loading branch information
knedlsepp committed Nov 11, 2023
1 parent c18d8f2 commit aac263b
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 9 deletions.
103 changes: 103 additions & 0 deletions conda_lock/models/lock_spec.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from __future__ import annotations

import copy
import hashlib
import json
import pathlib
Expand All @@ -24,23 +27,123 @@ class _BaseDependency(StrictModel):
def sorted_extras(cls, v: List[str]) -> List[str]:
return sorted(v)

def _merge_base(self, other: _BaseDependency) -> _BaseDependency:
if other is None:
return self
if (
self.name != other.name
or self.manager != other.manager
or self.category != other.category
):
raise ValueError(
"Cannot merge incompatible dependencies: {self} != {other}"
)
return _BaseDependency(
name=self.name,
manager=self.manager,
category=self.category,
extras=list(set(self.extras + other.extras)),
)


class VersionedDependency(_BaseDependency):
version: str
build: Optional[str] = None
conda_channel: Optional[str] = None

@staticmethod
def _merge_constraints(
constraint1: Optional[str], constraint2: Optional[str]
) -> Optional[str]:
# Must return a copy!
if constraint1 == constraint2:
return copy.copy(constraint1)
if constraint1 is None or constraint1 == "":
return copy.copy(constraint2)
if constraint2 is None or constraint2 == "":
return copy.copy(constraint1)
return f"{constraint1},{constraint2}"

def merge(self, other: Optional[VersionedDependency]) -> VersionedDependency:
if other is None:
return self

if (
self.conda_channel is not None
and other.conda_channel is not None
and self.conda_channel != other.conda_channel
):
raise ValueError(
f"VersionedDependency has two different conda_channels:\n{self}\n{other}"
)
merged_base = self._merge_base(other)
return VersionedDependency(
name=merged_base.name,
manager=merged_base.manager,
category=merged_base.category,
extras=merged_base.extras,
version=self._merge_constraints(self.version, other.version), # type: ignore
build=self._merge_constraints(self.build, other.build),
conda_channel=self.conda_channel or other.conda_channel,
)


class URLDependency(_BaseDependency):
url: str
hashes: List[str]

def merge(self, other: Optional[URLDependency]) -> URLDependency:
if other is None:
return self
if self.url != other.url:
raise ValueError(f"URLDependency has two different urls:\n{self}\n{other}")

if self.hashes != other.hashes:
raise ValueError(
f"URLDependency has two different hashess:\n{self}\n{other}"
)
merged_base = self._merge_base(other)

return URLDependency(
name=merged_base.name,
manager=merged_base.manager,
category=merged_base.category,
extras=merged_base.extras,
url=self.url,
hashes=self.hashes,
)


class VCSDependency(_BaseDependency):
source: str
vcs: str
rev: Optional[str] = None

def merge(self, other: Optional[VCSDependency]) -> VCSDependency:
if other is None:
return self
if self.source != other.source:
raise ValueError(
f"VCSDependency has two different sources:\n{self}\n{other}"
)

if self.vcs != other.vcs:
raise ValueError(f"VCSDependency has two different vcss:\n{self}\n{other}")

if self.rev is not None and other.rev is not None and self.rev != other.rev:
raise ValueError(f"VCSDependency has two different revs:\n{self}\n{other}")
merged_base = self._merge_base(other)

return VCSDependency(
name=merged_base.name,
manager=merged_base.manager,
category=merged_base.category,
extras=merged_base.extras,
source=self.source,
vcs=self.vcs,
rev=self.rev or other.rev,
)


Dependency = Union[VersionedDependency, URLDependency, VCSDependency]

Expand Down
6 changes: 5 additions & 1 deletion conda_lock/src_parser/aggregation.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ def aggregate_lock_specs(
lock_spec.dependencies.get(platform, []) for lock_spec in lock_specs
):
key = (dep.manager, dep.name)
unique_deps[key] = dep
if unique_deps.get(key) is not None and type(unique_deps[key]) != type(dep):
raise ValueError(
f"Unsupported use of different dependency types for same package:\n{dep}\n{unique_deps[key]}"
)
unique_deps[key] = dep.merge(unique_deps.get(key)) # type: ignore

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

Expand Down
22 changes: 14 additions & 8 deletions tests/test_conda_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -1622,22 +1622,28 @@ def test_aggregate_lock_specs():
assert actual.content_hash() == expected.content_hash()


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

override_spec = LockSpecification(
dependencies={"linux-64": [_make_spec("package", "=2.0")]},
second_spec = LockSpecification(
dependencies={"linux-64": [_make_spec("package", "<2.0")]},
channels=[Channel.from_string("internal"), Channel.from_string("conda-forge")],
sources=[Path("additional.yml")],
)

result_spec = LockSpecification(
dependencies={"linux-64": [_make_spec("package", "<2.0,>1.0")]},
channels=[Channel.from_string("internal"), Channel.from_string("conda-forge")],
sources=[Path("override.yml")],
sources=[Path("additional.yml")],
)

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

assert agg_spec.dependencies == override_spec.dependencies
assert agg_spec.dependencies == result_spec.dependencies


def test_aggregate_lock_specs_invalid_channels():
Expand Down

0 comments on commit aac263b

Please sign in to comment.