sdispater / mixology

A generic dependency-resolution library written in pure Python
MIT License
79 stars 11 forks source link
dependency-resolution python

Mixology

A generic dependency-resolution library written in pure Python. It is based on the PubGrub algorithm.

Installation

If you are using poetry, it's as simple as:

poetry add mixology

If not you can use pip:

pip install mixology

Usage

Mixology is a dependency resolution algorithm.

In order to start using Mixology you need to initialize a VersionSolver instance with a PackageSource which should be adapted to work with your system.

Then, you need to call VersionSolver.solve() which will return a result with the list of decisions or raise a SolveFailure which will give a detailed explanation of the reason why the resolution failed.

Example

This example is extracted from the test suite of Mixology and uses the poetry-semver library.

First we need to have our own PackageSource class which implements the required methods and a simple Dependency class. Packages will be represented by simple strings.

from semver import Version
from semver import VersionRange
from semver import parse_constraint

from mixology.constraint import Constraint
from mixology.package_source import PackageSource as BasePackageSource
from mixology.range import Range
from mixology.union import Union

class Dependency:

    def __init__(self, name, constraint):  # type: (str, str) -> None
        self.name = name
        self.constraint = parse_constraint(constraint)
        self.pretty_constraint = constraint

    def __str__(self):  # type: () -> str
        return self.pretty_constraint

class PackageSource(BasePackageSource):

    def __init__(self):  # type: () -> None
        self._root_version = Version.parse("0.0.0")
        self._root_dependencies = []
        self._packages = {}

        super(PackageSource, self).__init__()

    @property
    def root_version(self):
        return self._root_version

    def add(
        self, name, version, deps=None
    ):  # type: (str, str, Optional[Dict[str, str]]) -> None
        if deps is None:
            deps = {}

        version = Version.parse(version)
        if name not in self._packages:
            self._packages[name] = {}

        if version in self._packages[name]:
            raise ValueError("{} ({}) already exists".format(name, version))

        dependencies = []
        for dep_name, spec in deps.items():
            dependencies.append(Dependency(dep_name, spec))

        self._packages[name][version] = dependencies

    def root_dep(self, name, constraint):  # type: (str, str) -> None
        self._root_dependencies.append(Dependency(name, constraint))

    def _versions_for(
        self, package, constraint=None
    ):  # type: (Hashable, Any) -> List[Hashable]
        if package not in self._packages:
            return []

        versions = []
        for version in self._packages[package].keys():
            if not constraint or constraint.allows_any(
                Range(version, version, True, True)
            ):
                versions.append(version)

        return sorted(versions, reverse=True)

    def dependencies_for(self, package, version):  # type: (Hashable, Any) -> List[Any]
        if package == self.root:
            return self._root_dependencies

        return self._packages[package][version]

    def convert_dependency(self, dependency):  # type: (Dependency) -> Constraint
        if isinstance(dependency.constraint, VersionRange):
            constraint = Range(
                dependency.constraint.min,
                dependency.constraint.max,
                dependency.constraint.include_min,
                dependency.constraint.include_max,
                dependency.pretty_constraint,
            )
        else:
            # VersionUnion
            ranges = [
                Range(
                    range.min,
                    range.max,
                    range.include_min,
                    range.include_max,
                    str(range),
                )
                for range in dependency.constraint.ranges
            ]
            constraint = Union.of(ranges)

        return Constraint(dependency.name, constraint)

Now, we need to specify our root dependencies and the available packages.

source = PackageSource()

source.root_dep("a", "1.0.0")
source.root_dep("b", "1.0.0")

source.add("a", "1.0.0", deps={"shared": ">=2.0.0 <4.0.0"})
source.add("b", "1.0.0", deps={"shared": ">=3.0.0 <5.0.0"})
source.add("shared", "2.0.0")
source.add("shared", "3.0.0")
source.add("shared", "3.6.9")
source.add("shared", "4.0.0")
source.add("shared", "5.0.0")

Now that everything is in place we can create a VersionSolver instance with the newly created PackageSource and call solve() to retrieve a SolverResult instance.

from mixology.version_solver import VersionSolver

solver = VersionSolver(source)
result = solver.solve()
result.decisions
# {Package("_root_"): <Version 0.0.0>, 'b': <Version 1.0.0>, 'a': <Version 1.0.0>, 'shared': <Version 3.6.9>}
result.attempted_solutions
# 1

Contributing

To work on the Mixology codebase, you'll want to fork the project, clone the fork locally and install the required dependencies via poetry <https://poetry.eustace.io>_.

git clone git@github.com:sdispater/mixology.git
poetry install

Then, create your feature branch:

git checkout -b my-new-feature

Make your modifications, add tests accordingly and execute the test suite:

poetry run pytest tests/

When you are ready, commit your changes:

git commit -am 'Add new feature'

push your branch:

git push origin my-new-feature

and finally create a pull request.