Skip to content

Commit

Permalink
Merge pull request #162 from sarugaku/fix/separate-resolvers-module
Browse files Browse the repository at this point in the history
  • Loading branch information
frostming authored Aug 1, 2024
2 parents 2410b7e + ac8ed26 commit 7961d60
Show file tree
Hide file tree
Showing 16 changed files with 133 additions and 191 deletions.
7 changes: 2 additions & 5 deletions examples/reporter_demo.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from collections import namedtuple

import resolvelib
from packaging.specifiers import SpecifierSet
from packaging.version import Version

import resolvelib

index = """
first 1.0.0
second == 1.0.0
Expand Down Expand Up @@ -53,9 +52,7 @@ def read_spec(lines):
candidates[latest] = set()
else:
if latest is None:
raise RuntimeError(
"Spec has dependencies before first candidate"
)
raise RuntimeError("Spec has dependencies before first candidate")
name, specifier = splitstrip(line, 2)
specifier = SpecifierSet(specifier)
candidates[latest].add(Requirement(name, specifier))
Expand Down
20 changes: 5 additions & 15 deletions examples/visualization/reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,7 @@ def _get_subgraph(self, name, *, must_exist_already=True):
if subgraph is None:
if must_exist_already:
existing = [s.name for s in self.graph.subgraphs_iter()]
raise RuntimeError(
f"Graph for {name} not found. Existing: {existing}"
)
raise RuntimeError(f"Graph for {name} not found. Existing: {existing}")
else:
subgraph = self.graph.add_subgraph(name=c_name, label=name)

Expand Down Expand Up @@ -151,9 +149,7 @@ def adding_requirement(self, req, parent):
# We're seeing the parent candidate (which is being "evaluated"), so
# color all "active" requirements pointing to the it.
# TODO: How does this interact with revisited candidates?
for parent_req in self._active_requirements[
canonicalize_name(parent.name)
]:
for parent_req in self._active_requirements[canonicalize_name(parent.name)]:
self._ensure_edge(parent_req, to=parent, color="#80CC80")

def backtracking(self, candidate, internal=False):
Expand All @@ -175,9 +171,7 @@ def backtracking(self, candidate, internal=False):

# Trim "active" requirements to remove anything not relevant now.
for requirement in self._dependencies[candidate]:
active = self._active_requirements[
canonicalize_name(requirement.name)
]
active = self._active_requirements[canonicalize_name(requirement.name)]
active[requirement] -= 1
if not active[requirement]:
del active[requirement]
Expand All @@ -194,12 +188,8 @@ def pinning(self, candidate):
node.attr.update(color="#80CC80")

# Requirement -> Candidate edges, from this candidate.
for req in self._active_requirements[
canonicalize_name(candidate.name)
]:
self._ensure_edge(
req, to=candidate, arrowhead="vee", color="#80CC80"
)
for req in self._active_requirements[canonicalize_name(candidate.name)]:
self._ensure_edge(req, to=candidate, arrowhead="vee", color="#80CC80")

# Candidate -> Requirement edges, from this candidate.
for edge in self.graph.out_edges_iter([node_name]):
Expand Down
4 changes: 1 addition & 3 deletions examples/visualization/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ def process_arguments(function, args):
to_convert, _, args = args.partition(", ")
value = int(to_convert)
elif arg_type == "requirement":
match = re.match(
r"^<Requirement\('?([\w\-\._~]+)(.*?)'?\)>(.*)", args
)
match = re.match(r"^<Requirement\('?([\w\-\._~]+)(.*?)'?\)>(.*)", args)
assert match, repr(args)
name, spec, args = match.groups()
value = Requirement(name, spec)
Expand Down
4 changes: 1 addition & 3 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@
@nox.session
def lint(session):
session.install(".[lint, test]")

session.run("black", "--check", ".")
session.run("isort", ".")
session.run("ruff", "format", "--check", ".")
session.run("ruff", "check", ".")
session.run("mypy", "src", "tests")

Expand Down
15 changes: 3 additions & 12 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ Homepage = "https://github.com/sarugaku/resolvelib"

[project.optional-dependencies]
lint = [
"black==23.12.1",
"ruff",
"isort",
"mypy",
"types-requests",
]
Expand Down Expand Up @@ -57,16 +55,6 @@ version = {attr = "resolvelib.__version__"}
[tool.distutils.bdist_wheel]
universal = true


[tool.black]
line-length = 79
include = '^/(docs|examples|src|tasks|tests)/.+\.py$'

[tool.isort]
profile = "black"
line_length = 79
multi_line_output = 3

[tool.towncrier]
package = 'resolvelib'
package_dir = 'src'
Expand Down Expand Up @@ -108,6 +96,9 @@ exclude = [
"*.pyi"
]

[tool.ruff.lint.isort]
known-first-party = ["resolvelib"]

[tool.mypy]
warn_unused_configs = true

Expand Down
5 changes: 2 additions & 3 deletions src/resolvelib/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
from typing import Any, Protocol

class Preference(Protocol):
def __lt__(self, __other: Any) -> bool:
...
def __lt__(self, __other: Any) -> bool: ...


class AbstractProvider(Generic[RT, CT, KT]):
Expand Down Expand Up @@ -88,7 +87,7 @@ def find_matches(
identifier: KT,
requirements: Mapping[KT, Iterator[RT]],
incompatibilities: Mapping[KT, Iterator[CT]],
) -> Matches:
) -> Matches[CT]:
"""Find all possible candidates that satisfy the given constraints.
:param identifier: An identifier as returned by ``identify()``. All
Expand Down
4 changes: 1 addition & 3 deletions src/resolvelib/reporters.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,7 @@ def resolving_conflicts(
:param causes: The information on the collision that caused the backtracking.
"""

def rejecting_candidate(
self, criterion: Criterion[RT, CT], candidate: CT
) -> None:
def rejecting_candidate(self, criterion: Criterion[RT, CT], candidate: CT) -> None:
"""Called when rejecting a candidate during backtracking."""

def pinning(self, candidate: CT) -> None:
Expand Down
24 changes: 24 additions & 0 deletions src/resolvelib/resolvers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from ..structs import RequirementInformation
from .abstract import AbstractResolver, Result
from .criterion import (
InconsistentCandidate,
RequirementsConflicted,
ResolutionError,
ResolutionImpossible,
ResolutionTooDeep,
Resolver,
ResolverException,
)

__all__ = [
"AbstractResolver",
"InconsistentCandidate",
"Resolver",
"RequirementsConflicted",
"ResolutionError",
"ResolutionImpossible",
"ResolutionTooDeep",
"RequirementInformation",
"ResolverException",
"Result",
]
47 changes: 47 additions & 0 deletions src/resolvelib/resolvers/abstract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from __future__ import annotations

import collections
from typing import TYPE_CHECKING, Any, Generic, Iterable, Mapping, NamedTuple

from resolvelib.providers import AbstractProvider
from resolvelib.reporters import BaseReporter

from ..structs import CT, KT, RT, Criterion, DirectedGraph

if TYPE_CHECKING:

class Result(NamedTuple, Generic[RT, CT, KT]):
mapping: Mapping[KT, CT]
graph: DirectedGraph[KT | None]
criteria: Mapping[KT, Criterion[RT, CT]]

else:
Result = collections.namedtuple("Result", ["mapping", "graph", "criteria"])


class AbstractResolver(Generic[RT, CT, KT]):
"""The thing that performs the actual resolution work."""

base_exception = Exception

def __init__(
self,
provider: AbstractProvider[RT, CT, KT],
reporter: BaseReporter[RT, CT, KT],
) -> None:
self.provider = provider
self.reporter = reporter

def resolve(self, requirements: Iterable[RT], **kwargs: Any) -> Result[RT, CT, KT]:
"""Take a collection of constraints, spit out the resolution result.
This returns a representation of the final resolution state, with one
guarenteed attribute ``mapping`` that contains resolved candidates as
values. The keys are their respective identifiers.
:param requirements: A collection of constraints.
:param kwargs: Additional keyword arguments that subclasses may accept.
:raises: ``self.base_exception`` or its subclass.
"""
raise NotImplementedError
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,11 @@
import collections
import itertools
import operator
from typing import (
TYPE_CHECKING,
Any,
Collection,
Generic,
Iterable,
Mapping,
NamedTuple,
)
from typing import TYPE_CHECKING, Collection, Generic, Iterable, Mapping

from .providers import AbstractProvider
from .reporters import BaseReporter
from .structs import (
from ..providers import AbstractProvider
from ..reporters import BaseReporter
from ..structs import (
CT,
KT,
RT,
Expand All @@ -27,17 +19,10 @@
State,
build_iter_view,
)
from .abstract import AbstractResolver, Result

if TYPE_CHECKING:
from .providers import Preference

class Result(NamedTuple, Generic[RT, CT, KT]):
mapping: Mapping[KT, CT]
graph: DirectedGraph[KT | None]
criteria: Mapping[KT, Criterion[RT, CT]]

else:
Result = collections.namedtuple("Result", ["mapping", "graph", "criteria"])
from ..providers import Preference


class ResolverException(Exception):
Expand Down Expand Up @@ -224,9 +209,7 @@ def _is_current_pin_satisfying(
for r in criterion.iter_requirement()
)

def _get_updated_criteria(
self, candidate: CT
) -> dict[KT, Criterion[RT, CT]]:
def _get_updated_criteria(self, candidate: CT) -> dict[KT, Criterion[RT, CT]]:
criteria = self.state.criteria.copy()
for requirement in self._p.get_dependencies(candidate=candidate):
self._add_to_criteria(criteria, requirement, parent=candidate)
Expand Down Expand Up @@ -260,7 +243,7 @@ def _attempt_to_pin_criterion(self, name: KT) -> list[Criterion[RT, CT]]:

# Put newly-pinned candidate at the end. This is essential because
# backtracking looks at this mapping to get the last pin.
self.state.mapping.pop(name, None) # type: ignore[arg-type]
self.state.mapping.pop(name, None)
self.state.mapping[name] = candidate

return []
Expand Down Expand Up @@ -362,8 +345,7 @@ def _backjump(self, causes: list[RequirementInformation[RT, CT]]) -> bool:
# If the current dependencies and the incompatible dependencies
# are overlapping then we have found a cause of the incompatibility
current_dependencies = {
self._p.identify(d)
for d in self._p.get_dependencies(candidate)
self._p.identify(d) for d in self._p.get_dependencies(candidate)
}
if not current_dependencies.isdisjoint(incompatible_deps):
break
Expand All @@ -375,8 +357,7 @@ def _backjump(self, causes: list[RequirementInformation[RT, CT]]) -> bool:
break

incompatibilities_from_broken = [
(k, list(v.incompatibilities))
for k, v in broken_state.criteria.items()
(k, list(v.incompatibilities)) for k, v in broken_state.criteria.items()
]

# Also mark the newly known incompatibility.
Expand All @@ -399,13 +380,9 @@ def _extract_causes(
self, criteron: list[Criterion[RT, CT]]
) -> list[RequirementInformation[RT, CT]]:
"""Extract causes from list of criterion and deduplicate"""
return list(
{id(i): i for c in criteron for i in c.information}.values()
)
return list({id(i): i for c in criteron for i in c.information}.values())

def resolve(
self, requirements: Iterable[RT], max_rounds: int
) -> State[RT, CT, KT]:
def resolve(self, requirements: Iterable[RT], max_rounds: int) -> State[RT, CT, KT]:
if self._states:
raise RuntimeError("already resolved")

Expand Down Expand Up @@ -445,9 +422,7 @@ def resolve(
return self.state

# keep track of satisfied names to calculate diff after pinning
satisfied_names = set(self.state.criteria.keys()) - set(
unsatisfied_names
)
satisfied_names = set(self.state.criteria.keys()) - set(unsatisfied_names)

# Choose the most preferred unpinned criterion to try.
name = min(unsatisfied_names, key=self._get_preference)
Expand Down Expand Up @@ -539,36 +514,6 @@ def _build_result(state: State[RT, CT, KT]) -> Result[RT, CT, KT]:
)


class AbstractResolver(Generic[RT, CT, KT]):
"""The thing that performs the actual resolution work."""

base_exception = Exception

def __init__(
self,
provider: AbstractProvider[RT, CT, KT],
reporter: BaseReporter[RT, CT, KT],
) -> None:
self.provider = provider
self.reporter = reporter

def resolve(
self, requirements: Iterable[RT], **kwargs: Any
) -> Result[RT, CT, KT]:
"""Take a collection of constraints, spit out the resolution result.
This returns a representation of the final resolution state, with one
guarenteed attribute ``mapping`` that contains resolved candidates as
values. The keys are their respective identifiers.
:param requirements: A collection of constraints.
:param kwargs: Additional keyword arguments that subclasses may accept.
:raises: ``self.base_exception`` or its subclass.
"""
raise NotImplementedError


class Resolver(AbstractResolver[RT, CT, KT]):
"""The thing that performs the actual resolution work."""

Expand Down
Loading

0 comments on commit 7961d60

Please sign in to comment.