diff --git a/conda_lock/src_parser/__init__.py b/conda_lock/src_parser/__init__.py index 98734ac18..edcaf06c2 100644 --- a/conda_lock/src_parser/__init__.py +++ b/conda_lock/src_parser/__init__.py @@ -537,29 +537,43 @@ def aggregate_lock_specs( [lock_spec.dependencies for lock_spec in lock_specs] ): key = (dep.manager, dep.name) + new_dep = dep.copy() if key in unique_deps: prev_dep = unique_deps[key] # Override existing, but merge selectors previous_selectors = prev_dep.selectors previous_selectors |= dep.selectors - dep.selectors = previous_selectors + new_dep.selectors = previous_selectors - # If bold old and new are VersionedDependency, combine versions + # If bold old and new are VersionedDependency, combine version strings together + # If there are conflicting versions, they will be handled by the solver if isinstance(prev_dep, VersionedDependency) and isinstance( dep, VersionedDependency ): + assert isinstance(new_dep, VersionedDependency) + + # MatchSpecs are expressions in DNF form: `|` is OR and `,` is AND prev_versions = [ - set(sec.split(",")) for sec in prev_dep.version.split("|") + set(sec.split(",")) + for sec in prev_dep.version.split("|") + if sec != "" + ] + new_versions = [ + set(sec.split(",")) for sec in dep.version.split("|") if sec != "" ] - new_versions = [set(sec.split(",")) for sec in dep.version.split("|")] + # To AND two DNF expressions, we essentially perform the distributive law + # ORing every pair of terms in the individual expressions + # Ex: (AB | CD) (AC | BD) = ABAC | ABBD | CDAC | CDBC = ABC | ABD | ACD | BCD cross_versions = [ prev | new for prev, new in product(prev_versions, new_versions) ] - final_versions = set(",".join(subset) for subset in cross_versions) - dep.version = "|".join(final_versions) + final_versions = set( + ",".join(sorted(subset, reverse=True)) for subset in cross_versions + ) + new_dep.version = "|".join(sorted(final_versions, reverse=True)) - unique_deps[key] = dep + unique_deps[key] = new_dep dependencies = list(unique_deps.values()) try: diff --git a/tests/test_conda_lock.py b/tests/test_conda_lock.py index 22c7d96b5..4133c3366 100644 --- a/tests/test_conda_lock.py +++ b/tests/test_conda_lock.py @@ -1123,7 +1123,7 @@ def test_aggregate_lock_specs_multiple_platforms(): assert actual.content_hash() == expected.content_hash() -def test_aggregate_lock_specs_override_version(): +def test_aggregate_lock_specs_combine_version(): base_spec = LockSpecification( dependencies=[_make_spec("package", "=1.0")], channels=[Channel.from_string("conda-forge")], @@ -1139,8 +1139,28 @@ def test_aggregate_lock_specs_override_version(): ) agg_spec = aggregate_lock_specs([base_spec, override_spec]) + assert agg_spec.dependencies == [_make_spec("package", "=2.0,=1.0")] - assert agg_spec.dependencies == override_spec.dependencies + +def test_aggregate_lock_specs_with_union_version(): + base_spec = LockSpecification( + dependencies=[_make_spec("package", "=1.0|>2")], + channels=[Channel.from_string("conda-forge")], + platforms=["linux-64"], + sources=[Path("base.yml")], + ) + + override_spec = LockSpecification( + dependencies=[_make_spec("package", "=2.0,<3.0.0")], + channels=[Channel.from_string("internal"), Channel.from_string("conda-forge")], + platforms=["linux-64"], + sources=[Path("override.yml")], + ) + + agg_spec = aggregate_lock_specs([base_spec, override_spec]) + assert agg_spec.dependencies == [ + _make_spec("package", ">2,=2.0,<3.0.0|=2.0,=1.0,<3.0.0") + ] def test_aggregate_lock_specs_invalid_channels():