Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add render-lock-spec subcommand for exporting lock specification to pixi.toml #664

Merged
merged 30 commits into from
Sep 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4074718
Add a very rough export of lock spec to pixi.toml
maresb Sep 13, 2024
af61d7b
Improve pixi.toml generation
maresb Sep 7, 2024
864e41f
Move to separate module
maresb Sep 10, 2024
96b6bdc
Remove unused imports
maresb Sep 10, 2024
989dcc9
Add doctests for make_pixi_spec
maresb Sep 11, 2024
9aaf416
Remove unused channels and sorted_categories
maresb Sep 11, 2024
904e1e5
Bundle all the arrangement logic into arrange_for_toml
maresb Sep 11, 2024
04b3250
Code cleanup
maresb Sep 11, 2024
06ca32b
Use "line" terminology
maresb Sep 12, 2024
e3be00c
Make TomlTableKey more explicit
maresb Sep 12, 2024
43108a3
Move indexing into aggregate_platform_independent_deps
maresb Sep 12, 2024
c5e60d2
Rework aggregation
maresb Sep 12, 2024
8f45fea
Split platform independent computation into separate module
maresb Sep 12, 2024
b23fb6e
More simplification
maresb Sep 12, 2024
e5ffd26
Define environments
maresb Sep 12, 2024
5fdf092
lockspec → lock_spec
maresb Sep 12, 2024
6b25ac2
Add render_lock_spec command
maresb Sep 12, 2024
0114cb9
Clean up rendering lock spec
maresb Sep 13, 2024
8991a0a
More cleanup
maresb Sep 13, 2024
07a9048
Add tomlkit as a direct dependency of conda-lock
maresb Sep 14, 2024
d0c94da
Switch from raw text generation to tomlkit
maresb Sep 14, 2024
b10c864
Update for mapping_url
maresb Sep 15, 2024
06c6a2e
Clean up CLI annotations
maresb Sep 15, 2024
0c4d953
Sort the platforms
maresb Sep 15, 2024
e226834
Fix TOML generation
maresb Sep 15, 2024
c689e47
Support adding editable dependencies via render-lock-spec CLI
maresb Sep 15, 2024
cac726d
Fix unify_platform_independent_deps signature
maresb Sep 15, 2024
901b747
Add and use a warn helper
maresb Sep 15, 2024
a3c737e
Avoid double line ending
maresb Sep 15, 2024
e4f47d1
Prepend == to exact versions in pypi-dependencies
maresb Sep 15, 2024
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
149 changes: 149 additions & 0 deletions conda_lock/_export_lock_spec_compute_platform_indep.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"""This module contains the `unify_platform_independent_deps` function.

This is deliberately placed in a separate module since it doesn't directly have to
do with the logic for exporting a lock specification, and the logic is a bit involved.
"""

from collections import defaultdict
from typing import Dict, List, NamedTuple, Optional, Union

from conda_lock.models.lock_spec import Dependency


class EditableDependency(NamedTuple):
name: str
path: str


class DepKey(NamedTuple):
"""A hashable key for a `Dependency` object and the platform under consideration.

There are two main motivations for this class.

1. A `Dependency` does not have its own `platform` attribute, but we need to
categorize dependencies by platform. This class allows us to do that.

2. A `Dependency` is a Pydantic model, so it isn't hashable and can't be used as a
key in a dictionary. This class is hashable and can be used as a key, enabling
us to index dependencies by their attributes.

When `platform` is `None`, this signifies a platform-independent dependency.
"""

name: str
category: str
platform: Optional[str]
manager: str

def drop_platform(self) -> "DepKey":
return self._replace(platform=None)


def unify_platform_independent_deps(
dependencies: Dict[str, List[Dependency]],
*,
editables: Optional[List[EditableDependency]] = None,
) -> Dict[DepKey, Union[Dependency, EditableDependency]]:
"""Combine identical dependencies for all platforms into a single dependency.

Returns a tuple of two dictionaries:

>>> from conda_lock.models.lock_spec import VersionedDependency
>>> numpy1a = VersionedDependency(name="numpy", version="1.2.3")
>>> numpy1b = VersionedDependency(name="numpy", version="1.2.3")
>>> pandas1 = VersionedDependency(name="pandas", version="4.5.6")
>>> pandas2 = VersionedDependency(name="pandas", version="7.8.9")
>>> xarray = VersionedDependency(name="xarray", version="1.2.3")
>>> dependencies = {
... "linux-64": [numpy1a, pandas1],
... "osx-64": [numpy1b, pandas1, xarray],
... "win-64": [numpy1a, pandas2],
... }

Since `numpy1a` and `numpy1b` are equal, `numpy` is platform-independent.

`xarray` only appears on `osx-64`.

`pandas` is present on all platforms, but the versions aren't all the same.

>>> unified = unify_platform_independent_deps(dependencies)
>>> for key in unified:
... print(key)
DepKey(name='numpy', category='main', platform=None, manager='conda')
DepKey(name='pandas', category='main', platform='linux-64', manager='conda')
DepKey(name='pandas', category='main', platform='osx-64', manager='conda')
DepKey(name='xarray', category='main', platform='osx-64', manager='conda')
DepKey(name='pandas', category='main', platform='win-64', manager='conda')

The full result:

>>> unified # doctest: +NORMALIZE_WHITESPACE
{DepKey(name='numpy', category='main', platform=None, manager='conda'):
VersionedDependency(name='numpy', manager='conda', category='main', extras=[],
markers=None, version='1.2.3', build=None, conda_channel=None, hash=None),
DepKey(name='pandas', category='main', platform='linux-64', manager='conda'):
VersionedDependency(name='pandas', manager='conda', category='main', extras=[],
markers=None, version='4.5.6', build=None, conda_channel=None, hash=None),
DepKey(name='pandas', category='main', platform='osx-64', manager='conda'):
VersionedDependency(name='pandas', manager='conda', category='main', extras=[],
markers=None, version='4.5.6', build=None, conda_channel=None, hash=None),
DepKey(name='xarray', category='main', platform='osx-64', manager='conda'):
VersionedDependency(name='xarray', manager='conda', category='main', extras=[],
markers=None, version='1.2.3', build=None, conda_channel=None, hash=None),
DepKey(name='pandas', category='main', platform='win-64', manager='conda'):
VersionedDependency(name='pandas', manager='conda', category='main', extras=[],
markers=None, version='7.8.9', build=None, conda_channel=None, hash=None)}
"""
indexed_deps: Dict[DepKey, Dependency] = {}
for platform, deps in dependencies.items():
for dep in deps:
key = DepKey(
name=dep.name,
category=dep.category,
platform=platform,
manager=dep.manager,
)
if key in indexed_deps:
raise ValueError(
f"Duplicate dependency {key}: {dep}, {indexed_deps[key]}"
)
# In the beginning each dep has a platform.
assert key.platform is not None
indexed_deps[key] = dep

# Collect deps which differ only by platform
collected_deps: Dict[DepKey, List[Dependency]] = defaultdict(list)
for key, dep in indexed_deps.items():
collected_deps[key.drop_platform()].append(dep)

editable_deps: Dict[DepKey, EditableDependency] = {}
for editable in editables or []:
key = DepKey(name=editable.name, category="main", platform=None, manager="pip")
if key in collected_deps:
raise ValueError(
f"Editable dependency {editable.name} conflicts with existing "
f"dependency {collected_deps[key][0]}"
)
editable_deps[key] = editable

# Check for platform-independent dependencies
num_platforms = len(dependencies.keys())
platform_independent_deps: Dict[DepKey, Dependency] = {
np_key: deps[0]
for np_key, deps in collected_deps.items()
# It's independent if there's a dep for each platform and they're all the same.
if len(deps) == num_platforms
and all(curr == next for curr, next in zip(deps, deps[1:]))
}
assert all(key.platform is None for key in platform_independent_deps)

# The platform-specific dependencies are now those not in platform_independent_deps.
platform_specific_deps: Dict[DepKey, Dependency] = {
key: dep
for key, dep in indexed_deps.items()
if key.drop_platform() not in platform_independent_deps
}
assert all(key.platform is not None for key in platform_specific_deps)

combined = {**platform_independent_deps, **editable_deps, **platform_specific_deps}
return combined
5 changes: 5 additions & 0 deletions conda_lock/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pathlib
import tempfile
import typing
import warnings

from contextlib import contextmanager
from itertools import chain
Expand Down Expand Up @@ -86,3 +87,7 @@ def relative_path(source: pathlib.Path, target: pathlib.Path) -> str:
up = [".."] * len(source.resolve().relative_to(common).parents)
down = target.resolve().relative_to(common).parts
return str(pathlib.PurePosixPath(*up) / pathlib.PurePosixPath(*down))


def warn(msg: str) -> None:
warnings.warn(msg, stacklevel=2)
Loading
Loading