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

fix: Separate AbstractResolver and Resolver into different modules #162

Merged
merged 6 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading