Commit 7e4668ea by Sébastien Eustace

Add generic dependency resolver

parent c5c7624e
from .dependency_graph import DependencyGraph
from .resolver import Resolver
class Conflict:
def __init__(self,
requirement,
requirements,
existing,
possibility_set,
locked_requirement,
requirement_trees,
activated_by_name,
underlying_error):
self.requirement = requirement
self.requirements = requirements
self.existing = existing
self.possibility_set = possibility_set
self.locked_requirement = locked_requirement
self.requirement_trees = requirement_trees,
self.activated_by_name = activated_by_name
self.underlying_error = underlying_error
@property
def possibility(self):
if self.possibility_set and self.possibility_set.latest_version:
return self.possibility_set.latest_version
from .specification_provider import SpecificationProvider
from .ui import UI
from typing import Any
from typing import Dict
from typing import List
from ..conflict import Conflict
from ..dependency_graph import DependencyGraph
class SpecificationProvider:
"""
Provides information about specifcations and dependencies to the resolver,
allowing the Resolver class to remain generic while still providing power
and flexibility.
This contract contains the methods
that users of Molinillo must implement
using knowledge of their own model classes.
"""
@property
def name_for_explicit_dependency_source(self) -> str:
return 'user-specified dependency'
@property
def name_for_locking_dependency_source(self) -> str:
return 'Lockfile'
def search_for(self, dependency: Any) -> List[Any]:
"""
Search for the specifications that match the given dependency.
The specifications in the returned list will be considered in reverse
order, so the latest version ought to be last.
"""
return []
def dependencies_for(self, specification: Any) -> List[Any]:
"""
Returns the dependencies of `specification`.
"""
return []
def is_requirement_satisfied_by(self,
requirement: Any,
activated: DependencyGraph,
spec: Any) -> bool:
"""
Determines whether the given requirement is satisfied by the given
spec, in the context of the current activated dependency graph.
"""
return True
def name_for(self, dependency: Any) -> str:
"""
Returns the name for the given dependency.
"""
return str(dependency)
def sort_dependencies(self,
dependencies: List[Any],
activated: DependencyGraph,
conflicts: Dict[str, List[Conflict]]) -> List[Any]:
"""
Sort dependencies so that the ones
that are easiest to resolve are first.
Easiest to resolve is (usually) defined by:
1) Is this dependency already activated?
2) How relaxed are the requirements?
3) Are there any conflicts for this dependency?
4) How many possibilities are there to satisfy this dependency?
"""
return sorted(
dependencies,
key=lambda dep: (
activated.vertex_named(self.name_for(dep)).payload is None,
conflicts.get(self.name_for(dep) is None)
)
)
def allow_missing(self, dependency) -> bool:
"""
Returns whether this dependency, which has no possible matching
specifications, can safely be ignored.
"""
return False
import sys
class UI:
def __init__(self, debug=False):
self._debug = debug
@property
def output(self):
return sys.stdout
@property
def progress_rate(self) -> float:
return 0.33
def is_debugging(self) -> bool:
return self._debug
def indicate_progress(self) -> None:
self.output.write('.')
def before_resolution(self) -> None:
self.output.write('Resolving dependencies...\n')
def after_resolution(self) -> None:
self.output.write('')
def debug(self, depth, message) -> None:
if self.is_debugging():
debug_info = str(message)
debug_info = '\n'.join([
':{}: {}'.format(str(depth).rjust(4), s)
for s in debug_info.split('\n')
]) + '\n'
self.output.write(debug_info)
from .exceptions import CircularDependencyError
from .graph.log import Log
class DependencyGraph:
def __init__(self):
self._vertices = {}
self._log = Log()
@property
def vertices(self):
return self._vertices
@property
def log(self):
return self._log
def tag(self, tag):
return self._log.tag(self, tag)
def rewind_to(self, tag):
return self._log.rewind_to(self, tag)
def add_child_vertex(self, name, payload, parent_names, requirement):
root = True
try:
parent_names.index(None)
except ValueError:
root = False
parent_names = [n for n in parent_names if n is not None]
vertex = self.add_vertex(name, payload, root)
if root:
vertex.explicit_requirements.append(requirement)
for parent_name in parent_names:
parent_vertex = self.vertex_named(parent_name)
self.add_edge(parent_vertex, vertex, requirement)
return vertex
def add_vertex(self, name, payload, root=False):
return self._log.add_vertex(self, name, payload, root)
def detach_vertex_named(self, name):
return self._log.detach_vertex_named(self, name)
def vertex_named(self, name):
return self.vertices.get(name)
def root_vertex_named(self, name):
vertex = self.vertex_named(name)
if vertex and vertex.root:
return vertex
def add_edge(self, origin, destination, requirement):
if destination.has_path_to(origin):
raise CircularDependencyError([origin, destination])
return self.add_edge_no_circular(origin, destination, requirement)
def add_edge_no_circular(self, origin, destination, requirement):
self._log.add_edge_no_circular(
self,
origin.name, destination.name,
requirement
)
def delete_edge(self, edge):
return self._log.delete_edge(
self,
edge.origin.name,
edge.destination.name,
edge.requirement
)
def set_payload(self, name, payload):
return self._log.set_payload(self, name, payload)
def to_dot(self):
dot_vertices = []
dot_edges = []
for n, v in self.vertices.items():
dot_vertices.append(' {} [label="{}|{}"]'.format(n, n, v.payload))
for e in v.outgoing_edges:
label = e.requirement
dot_edges.append(
' {} -> {} [label="{}"]'.format(
e.origin.name,
e.destination.name,
label
)
)
dot_vertices = sorted(set(dot_vertices))
dot_edges = sorted(set(dot_edges))
dot_vertices.insert(0, 'digraph G {')
dot_vertices.append('')
dot_edges.append('}')
dot = dot_vertices + dot_edges
return '\n'.join(dot)
def __iter__(self):
return iter(self.vertices.values())
from .helpers import flat_map
class ResolverError(Exception):
pass
class NoSuchDependencyError(ResolverError):
def __init__(self, dependency, required_by=None):
if required_by is None:
required_by = []
sources = ' and '.join(['"{}"'.format(r) for r in required_by])
message = 'Unable to find a specification for "{}"'.format(dependency)
if sources:
message += ' depended upon by {}'.format(sources)
super().__init__(message)
class CircularDependencyError(ResolverError):
def __init__(self, vertices):
super(CircularDependencyError, self).__init__(
'There is a circular dependency between {}'.format(
' and '.join([v.name for v in vertices])
)
)
self._dependencies = [v.payload.possibilities[-1] for v in vertices]
@property
def dependencies(self):
return self._dependencies
class VersionConflict(ResolverError):
def __init__(self, conflicts, specification_provider):
pairs = []
for conflicting in flat_map(
list(conflicts.values()), lambda x: x.requirements
):
for source, conflict_requirements in conflicting:
for c in conflict_requirements:
pairs.append((c, source))
super().__init__(
'Unable to satisfy the following requirements:\n\n'
'{}'.format(
'\n'.join('- "{}" required by "{}"'.format(r, d)
for r, d in pairs)
)
)
self._conflicts = conflicts
self._specification_provider = specification_provider
@property
def conflicts(self):
return self._conflicts
@property
def specification_provider(self):
return self._specification_provider
def message_with_trees(self,
solver_name='Poetry',
possibility_type='possibility named',
reduce_trees=lambda trees: sorted(set(trees), key=str),
printable_requirement=str,
message_for_conflict=None,
version_for_spec=str):
o = []
for name, conflict in sorted(self._conflicts):
o.append(
'\n{} could not find compatible versions for {} "{}"_n'.format(
solver_name, possibility_type, name
)
)
if conflict.locked_requirement:
o.append(
' In snapshot ({}):\n'.format(
self._specification_provider.name_for_locking_dependency_source
)
)
o.append(
' {}\n'.format(
printable_requirement(conflict.locked_requirement)
)
)
o.append('\n')
o.append(
' In {}:\n'.format(
self._specification_provider.name_for_explicit_dependency_source
)
)
trees = reduce_trees(conflict.requirement_trees)
ot = []
for tree in trees:
t = ''
depth = 2
for req in tree:
t += ' ' * depth + str(req)
if tree[-1] != req:
spec = conflict.activated_by_name.get(
self._specification_provider.name_for(req)
)
if spec:
t += ' was resolved to {}, which'.format(
version_for_spec(spec)
)
t += ' depends on'
t += '\n'
depth += 1
ot.append(t)
o.append('\n'.join(ot))
if message_for_conflict:
message_for_conflict(o, name, conflict)
return ''.join(o).strip()
from typing import Any
class Action:
def __init__(self):
self.previous = None
self.next = None
@property
def action_name(self) -> str:
raise NotImplementedError()
def up(self, graph: 'DependencyGraph') -> Any:
"""
Performs the action on the given graph.
"""
raise NotImplementedError()
def down(self, graph: 'DependencyGraph') -> None:
"""
Reverses the action on the given graph.
"""
raise NotImplementedError()
from .action import Action
from .edge import Edge
class AddEdgeNoCircular(Action):
def __init__(self, origin, destination, requirement):
super(AddEdgeNoCircular, self).__init__()
self._origin = origin
self._destination = destination
self._requirement = requirement
@property
def action_name(self):
return 'add_edge_no_circular'
@property
def origin(self):
return self._origin
@property
def destination(self):
return self._destination
@property
def requirement(self):
return self._requirement
def up(self, graph):
edge = self.make_edge(graph)
edge.origin.outgoing_edges.append(edge)
edge.destination.incoming_edges.append(edge)
return edge
def down(self, graph):
edge = self.make_edge(graph)
self._delete_first(edge.origin.outgoing_edges, edge)
self._delete_first(edge.origin.incoming_edges, edge)
def make_edge(self, graph):
return Edge(
graph.vertex_named(self._origin),
graph.vertex_named(self._destination),
self._requirement
)
def _delete_first(self, elements, element):
"""
:type elements: list
"""
index = elements.index(element)
if index != -1:
elements.pop(index)
from .action import Action
from .vertex import Vertex
class AddVertex(Action):
def __init__(self, name, payload, root):
"""
:param name: The name of the vertex.
:type name: str
:param payload: The payload of he vertex
:type payload: Any
:param root: whether the vertex is root or not
:type root: bool
"""
super(AddVertex, self).__init__()
self._name = name
self._payload = payload
self._root = root
self._existing_payload = None
self._existing_root = None
@property
def action_name(self):
return 'add_vertex'
@property
def name(self):
return self._name
@property
def payload(self):
return self._payload
@property
def root(self):
return self._root
def up(self, graph):
existing = graph.vertices.get(self._name)
if existing:
self._existing_payload = existing.payload
self._existing_root = existing.root
vertex = existing or Vertex(self._name, self._payload)
graph.vertices[vertex.name] = vertex
if not vertex.payload:
vertex.payload = self.payload
if not vertex.root:
vertex.root = self.root
return vertex
def down(self, graph):
if self._existing_payload is not None:
vertex = graph.vertices[self._name]
vertex.payload = self._existing_payload
vertex.root = self._existing_root
else:
del graph.vertices[self._name]
from .action import Action
from .edge import Edge
class DeleteEdge(Action):
def __init__(self, origin, destination, requirement):
super(DeleteEdge, self).__init__()
self._origin = origin
self._destination = destination
self._requirement = requirement
@property
def action_name(self):
return 'delete_edge'
@property
def origin(self):
return self._origin
@property
def destination(self):
return self._destination
@property
def requirement(self):
return self._requirement
def up(self, graph):
edge = self.make_edge(graph)
self._delete_first(edge.origin.outgoing_edges, edge)
self._delete_first(edge.destination.incoming_edges, edge)
return edge
def down(self, graph):
edge = self.make_edge(graph)
edge.origin.outgoing_edges.append(edge)
edge.origin.incoming_edges.append(edge)
def make_edge(self, graph):
return Edge(
graph.vertex_named(self._origin),
graph.vertex_named(self._destination),
self._requirement
)
def _delete_first(self, elements, element):
"""
:type elements: list
"""
index = elements.index(element)
if index != -1:
elements.pop(index)
from .action import Action
class DetachVertexNamed(Action):
def __init__(self, name):
super(DetachVertexNamed, self).__init__()
self._name = name
self._vertex = None
@property
def action_name(self):
return 'detach_vertex'
@property
def name(self):
return self._name
def up(self, graph):
if self._name not in graph.vertices:
return []
self._vertex = graph.vertices[self._name]
del graph.vertices[self._name]
removed_vertices = [self._vertex]
for e in self._vertex.outgoing_edges:
v = e.destination
try:
v.incoming_edges.remove(e)
except ValueError:
pass
if not v.root and not v.incoming_edges:
removed_vertices += graph.detach_vertex_named(v.name)
for e in self._vertex.incoming_edges:
v = e.origin
try:
v.outgoing_edges.remove(e)
except ValueError:
pass
return removed_vertices
def down(self, graph):
if self._vertex is None:
return
graph.vertices[self._vertex.name] = self._vertex
for e in self._vertex.outgoing_edges:
e.destination.incoming_edges.append(e)
for e in self._vertex.incoming_edges:
e.origin.outgoing_edges.append(e)
class Edge:
"""
A directed edge of a DependencyGraph
"""
def __init__(self, origin, destination, requirement):
self._origin = origin
self._destination = destination
self._requirement = requirement
@property
def origin(self):
return self._origin
@property
def destination(self):
return self._destination
@property
def requirement(self):
return self._requirement
def __repr__(self):
return '<Edge {} -> {}>'.format(
self._origin.name, self._destination.name
)
from .add_edge_no_circular import AddEdgeNoCircular
from .add_vertex import AddVertex
from .delete_edge import DeleteEdge
from .detach_vertex_named import DetachVertexNamed
from .set_payload import SetPayload
from .tag import Tag
class Log:
"""
A log for dependency graph actions.
"""
def __init__(self):
self._current_action = None
self._first_action = None
def tag(self, graph, tag):
"""
Tags the current state of the dependency as the given tag.
"""
return self._push_action(graph, Tag(tag))
def add_vertex(self, graph, name, payload, root):
return self._push_action(graph, AddVertex(name, payload, root))
def detach_vertex_named(self, graph, name):
return self._push_action(graph, DetachVertexNamed(name))
def add_edge_no_circular(self, graph, origin, destination, requirement):
action = AddEdgeNoCircular(origin, destination, requirement)
return self._push_action(graph, action)
def delete_edge(self, graph, origin, destination, requirement):
action = DeleteEdge(origin, destination, requirement)
return self._push_action(graph, action)
def set_payload(self, graph, name, payload):
return self._push_action(graph, SetPayload(name, payload))
def pop(self, graph):
action = self._current_action
if not action:
return
self._current_action = action.previous
if not self._current_action:
self._first_action = None
action.down(graph)
return action
def rewind_to(self, graph, tag):
while True:
action = self.pop(graph)
if not action:
raise ValueError('No tag "{}" found'.format(tag))
if isinstance(action, Tag) and action.tag == tag:
break
def _push_action(self, graph, action):
"""
Adds the given action to the log, running the action
:param graph: The graph
:param action: The action
:type action: Action
"""
action.previous = self._current_action
if self._current_action:
self._current_action.next = action
self._current_action = action
if not self._first_action:
self._first_action = action
return action.up(graph)
from .action import Action
class SetPayload(Action):
def __init__(self, name, payload):
super(SetPayload, self).__init__()
self._name = name
self._payload = payload
self._old_payload = None
@property
def action_name(self):
return 'set_payload'
@property
def name(self):
return self._name
@property
def payload(self):
return self._payload
def up(self, graph):
vertex = graph.vertex_named(self._name)
self._old_payload = vertex.payload
vertex.payload = self._payload
def down(self, graph):
graph.vertex_named(self._name).payload = self._old_payload
from .action import Action
class Tag(Action):
def __init__(self, tag):
super(Tag, self).__init__()
self._tag = tag
@property
def action_name(self):
return 'tag'
@property
def tag(self):
return self._tag
def up(self, graph):
pass
def down(self, graph):
pass
class Vertex:
def __init__(self, name, payload):
self.name = name
self.payload = payload
self.root = False
self._explicit_requirements = []
self.outgoing_edges = []
self.incoming_edges = []
@property
def explicit_requirements(self):
return self._explicit_requirements
@property
def requirements(self):
return [
edge.requirement for edge in self.incoming_edges
] + self._explicit_requirements
@property
def predecessors(self):
return [edge.origin for edge in self.incoming_edges]
@property
def recursive_predecessors(self):
return self._recursive_predecessors()
def _recursive_predecessors(self, vertices=None):
if vertices is None:
vertices = set()
for edge in self.incoming_edges:
vertex = edge.origin
if vertex in vertices:
continue
vertices.add(vertex)
vertex._recursive_predecessors(vertices)
return vertices
@property
def successors(self):
return [
edge.destination for edge in self.outgoing_edges
]
@property
def recursive_successors(self):
return self._recursive_successors()
def _recursive_successors(self, vertices=None):
if vertices is None:
vertices = set()
for edge in self.outgoing_edges:
vertex = edge.destination
if vertex in vertices:
continue
vertices.add(vertex)
vertex._recursive_predecessors(vertices)
return vertices
def __eq__(self, other):
if not isinstance(other, Vertex):
return NotImplemented
if self is other:
return True
return (
self.name == other.name
and self.payload == other.payload
and set(self.successors) == set(other.successors)
)
def __hash__(self):
return hash(self.name)
def has_path_to(self, other):
return (
self == other
or any([v.has_path_to(other) for v in self.successors])
)
def is_ancestor(self, other):
return other.path_to(self)
def __repr__(self):
return '<Vertex {}>'.format(self.name)
def flat_map(iter, callable):
if not isinstance(iter, (list, tuple)):
yield callable(iter)
else:
for v in iter:
for i in flat_map(v, callable):
yield i
class PossibilitySet:
def __init__(self, dependencies, possibilities):
self.dependencies = dependencies
self.possibilities = possibilities
@property
def latest_version(self):
if self.possibilities:
return self.possibilities[-1]
def __str__(self):
return '[{}]'.format(', '.join([repr(p) for p in self.possibilities]))
from typing import Any
from typing import List
from typing import Union
from .contracts import SpecificationProvider
from .contracts import UI
from .dependency_graph import DependencyGraph
from .resolution import Resolution
class Resolver:
def __init__(self,
specification_provider: SpecificationProvider,
resolver_ui: UI):
self._specification_provider = specification_provider
self._resolver_ui = resolver_ui
@property
def specification_provider(self) -> SpecificationProvider:
return self._specification_provider
@property
def ui(self) -> UI:
return self._resolver_ui
def resolve(self,
requested: List[Any],
base: Union[DependencyGraph, None] = None) -> DependencyGraph:
if base is None:
base = DependencyGraph()
return Resolution(
self._specification_provider,
self._resolver_ui,
requested,
base
).resolve()
from copy import copy
from .dependency_graph import DependencyGraph
class ResolutionState:
def __init__(self, name, requirements, activated,
requirement, possibilities, depth,
conflicts, unused_unwind_options):
self._name = name
self._requirements = requirements
self._activated = activated
self._requirement = requirement
self.possibilities = possibilities
self._depth = depth
self.conflicts = conflicts
self.unused_unwind_options = unused_unwind_options
@property
def name(self):
return self._name
@property
def requirements(self):
return self._requirements
@property
def activated(self):
return self._activated
@property
def requirement(self):
return self._requirement
@property
def depth(self):
return self._depth
@classmethod
def empty(cls):
return cls(None, [], DependencyGraph(), None, None, 0, {}, [])
class PossibilityState(ResolutionState):
pass
class DependencyState(ResolutionState):
def pop_possiblity_state(self):
state = PossibilityState(
self._name,
copy(self._requirements),
self._activated,
self._requirement,
[self._possibilities.pop()],
self._depth + 1,
copy(self._conflicts),
copy(self._unused_unwind_options)
)
state.activated.tag(state)
return state
class UnwindDetails:
def __init__(self,
state_index,
state_requirement,
requirement_tree,
conflicting_requirements,
requirement_trees,
requirements_unwound_to_instead):
self.state_index = state_index
self.state_requirement = state_requirement
self.requirement_tree = requirement_tree
self.conflicting_requirements = conflicting_requirements
self.requirement_trees = requirement_trees
self.requirements_unwound_to_instead = requirements_unwound_to_instead
self._reversed_requirement_tree_index = None
self._sub_dependencies_to_avoid = None
self._all_requirements = None
@property
def reversed_requirement_tree_index(self):
if self._reversed_requirement_tree_index is None:
if self.state_requirement:
self._reversed_requirement_tree_index = list(reversed(
self.requirement_tree
)).index(self.state_requirement)
else:
self._reversed_requirement_tree_index = 999999
return self._reversed_requirement_tree_index
def unwinding_to_primary_requirement(self):
return self.requirement_tree[-1] == self.state_requirement
@property
def sub_dependencies_to_avoid(self):
if self._sub_dependencies_to_avoid is None:
self._sub_dependencies_to_avoid = []
for tree in self.requirement_trees:
index = tree.index(self.state_requirement)
if index and tree[index + 1] is not None:
self._sub_dependencies_to_avoid.append(tree[index + 1])
return self._sub_dependencies_to_avoid
@property
def all_requirements(self):
if self._all_requirements is None:
self._all_requirements = [
x
for tree in self.requirement_trees
for x in tree
]
return self._all_requirements
def __eq__(self, other):
if not isinstance(other, UnwindDetails):
return NotImplemented
return (
self.state_index == other.state_index
and (
self.reversed_requirement_tree_index
== other.reversed_requirement_tree_index
)
)
def __lt__(self, other):
if not isinstance(other, UnwindDetails):
return NotImplemented
return self.state_index < other.state_index
def __le__(self, other):
if not isinstance(other, UnwindDetails):
return NotImplemented
return self.state_index <= other.state_index
def __gt__(self, other):
if not isinstance(other, UnwindDetails):
return NotImplemented
return self.state_index > other.state_index
def __ge__(self, other):
if not isinstance(other, UnwindDetails):
return NotImplemented
return self.state_index >= other.state_index
def unique(l):
used = set()
return [x for x in l if x not in used and (used.add(x) or True)]
from .dependency import Dependency
from .package import Package
from poetry.semver.version_parser import VersionParser
class Dependency:
def __init__(self, name, constraint):
self._name = name.lower()
self._constraint = VersionParser().parse_constraints(constraint)
self._pretty_constraint = constraint
@property
def name(self):
return self._name
@property
def constraint(self):
return self._constraint
@property
def pretty_constraint(self):
return self._pretty_constraint
@property
def pretty_name(self):
return '{} ({})'.format(self._name, self._pretty_constraint)
def accepts_prereleases(self):
return False
def __eq__(self, other):
if not isinstance(other, Dependency):
return NotImplemented
return self._name == other.name and self._constraint == other.constraint
def __hash__(self):
return hash(self._name)
def __repr__(self):
return '<Dependency {}>'.format(self.pretty_name)
from poetry.semver.helpers import parse_stability
class Package:
supported_link_types = {
'require': {
'description': 'requires',
'method': 'requires'
},
'provide': {
'description': 'provides',
'method': 'provides'
}
}
STABILITY_STABLE = 0
STABILITY_RC = 5
STABILITY_BETA = 10
STABILITY_ALPHA = 15
STABILITY_DEV = 20
stabilities = {
'stable': STABILITY_STABLE,
'rc': STABILITY_RC,
'beta': STABILITY_BETA,
'alpha': STABILITY_ALPHA,
'dev': STABILITY_DEV,
}
def __init__(self, name, version, pretty_version):
"""
Creates a new in memory package.
:param name: The package's name
:type name: str
:param version: The package's version
:type version: str
:param pretty_version: The package's non-normalized version
:type pretty_version: str
"""
self._pretty_name = name
self._name = name.lower()
self._id = -1
self._version = version
self._pretty_version = pretty_version
self._stability = parse_stability(version)
self._dev = self._stability == 'dev'
self.source_type = ''
self.source_reference = ''
self.source_url = ''
self.requires = []
self.dev_requires = []
@property
def name(self):
return self._name
@property
def pretty_name(self):
return self._pretty_name
@property
def id(self):
return self._id
@property
def version(self):
return self._version
@property
def pretty_version(self):
return self._pretty_version
@property
def unique_name(self):
return self.name + '-' + self._version
@property
def pretty_string(self):
return self.pretty_name + ' ' + self.pretty_version
@property
def full_pretty_version(self):
if not self._dev and self.source_type not in ['hg', 'git']:
return self._pretty_version
# if source reference is a sha1 hash -- truncate
if len(self.source_reference) == 40:
return '{} {}'.format(self._pretty_version,
self.source_reference[0:7])
return '{} {}'.format(self._pretty_version, self.source_reference)
def is_dev(self):
return self._dev
def is_prerelease(self):
return self._stability != 'stable'
def __hash__(self):
return hash(self._name)
def __eq__(self, other):
if not isinstance(other, Package):
return NotImplemented
return self._name == other.name and self._version == other.version
def __str__(self):
return self.unique_name
def __repr__(self):
return '<Package {}>'.format(self.unique_name)
class BaseConstraint:
def matches(self, provider):
raise NotImplementedError()
...@@ -3,9 +3,10 @@ import operator ...@@ -3,9 +3,10 @@ import operator
from pkg_resources import parse_version from pkg_resources import parse_version
from ..helpers import normalize_version from ..helpers import normalize_version
from .base_constraint import BaseConstraint
class Constraint: class Constraint(BaseConstraint):
OP_EQ = operator.eq OP_EQ = operator.eq
OP_LT = operator.lt OP_LT = operator.lt
...@@ -21,7 +22,6 @@ class Constraint: ...@@ -21,7 +22,6 @@ class Constraint:
'<=': OP_LE, '<=': OP_LE,
'>': OP_GT, '>': OP_GT,
'>=': OP_GE, '>=': OP_GE,
'<>': OP_NE,
'!=': OP_NE '!=': OP_NE
} }
......
class EmptyConstraint: from .base_constraint import BaseConstraint
class EmptyConstraint(BaseConstraint):
pretty_string = None pretty_string = None
......
class MultiConstraint: from .base_constraint import BaseConstraint
class MultiConstraint(BaseConstraint):
def __init__(self, constraints, conjunctive=True): def __init__(self, constraints, conjunctive=True):
self._constraints = tuple(constraints) self._constraints = tuple(constraints)
......
...@@ -9,8 +9,8 @@ _modifier_regex = ( ...@@ -9,8 +9,8 @@ _modifier_regex = (
def normalize_version(version): def normalize_version(version):
""" """
Normalizes a version string to be able to perform comparisons on it. Normalizes a version string to be able to perform comparisons on it.
""" """
version = version.strip() version = version.strip()
# strip off build metadata # strip off build metadata
...@@ -68,6 +68,33 @@ def normalize_stability(stability: str) -> str: ...@@ -68,6 +68,33 @@ def normalize_stability(stability: str) -> str:
return stability return stability
def parse_stability(version: str) -> str:
"""
Returns the stability of a version.
"""
version = re.sub('(?i)#.+$', '', version)
if 'dev-' == version[:4] or '-dev' == version[-4:]:
return 'dev'
m = re.search('(?i){}(?:\+.*)?$'.format(_modifier_regex), version.lower())
if m:
if m.group(3):
return 'dev'
if m.group(1):
if m.group(1) in ['beta', 'b']:
return 'beta'
if m.group(1) in ['alpha', 'a']:
return 'alpha'
if m.group(1) == 'rc':
return 'RC'
return 'stable'
def _expand_stability(stability: str) -> str: def _expand_stability(stability: str) -> str:
stability = stability.lower() stability = stability.lower()
......
...@@ -18,32 +18,6 @@ class VersionParser: ...@@ -18,32 +18,6 @@ class VersionParser:
'stable', 'RC', 'beta', 'alpha', 'dev' 'stable', 'RC', 'beta', 'alpha', 'dev'
] ]
@classmethod
def parse_stability(cls, version: str) -> str:
"""
Returns the stability of a version.
"""
version = re.sub('#.+$i', '', version)
if 'dev-' == version[:4] or '-dev' == version[-4:]:
return 'dev'
m = re.match(f'(?i){cls._modifier_regex}(?:\+.*)?$', version)
if m.group(3):
return 'dev'
if m.group(1):
if m.group(1) in ['beta', 'b']:
return 'beta'
if m.group(1) in ['alpha', 'a']:
return 'alpha'
if m.group(1) == 'rc':
return 'RC'
return 'stable'
def parse_constraints(self, constraints: str): def parse_constraints(self, constraints: str):
""" """
Parses a constraint string into Parses a constraint string into
......
{
"name": "detects circular dependencies",
"index": "circular",
"requested": {
"circular_app": "*"
},
"base": [],
"resolved": [],
"conflicts": [
"foo",
"bar"
]
}
{
"name": "resolves a simple conflict index",
"index": "conflict",
"requested": {
"my_app": "*"
},
"base": [],
"resolved": [
{
"name": "my_app",
"version": "1.0.0",
"dependencies": [
{
"name": "activemodel",
"version": "3.2.11",
"dependencies": [
{
"name": "builder",
"version": "3.0.4",
"dependencies": []
}
]
},
{
"name": "grape",
"version": "0.2.6",
"dependencies": [
{
"name": "builder",
"version": "3.0.4",
"dependencies": []
}
]
}
]
}
],
"conflicts": []
}
{
"name": "resolves a single dependency",
"requested": {
"rack": "*"
},
"base": [],
"resolved": [
{
"name": "rack",
"version": "1.1",
"dependencies": []
}
],
"conflicts": []
}
{
"name": "resolves a single locked dependency",
"requested": {
"rack": "*"
},
"base": [
{
"name": "rack",
"version": "1.0",
"dependencies": []
}
],
"resolved": [
{
"name": "rack",
"version": "1.0",
"dependencies": []
}
],
"conflicts": []
}
{
"name": "resolves a single dependency with dependencies",
"requested": {
"actionpack": "*"
},
"base": [],
"resolved": [
{
"name": "actionpack",
"version": "2.3.5",
"dependencies": [
{
"name": "activesupport",
"version": "2.3.5",
"dependencies": []
},
{
"name": "rack",
"version": "1.0",
"dependencies": []
}
]
}
],
"conflicts": []
}
{
"name": "resolves dependencies with shared dependencies",
"requested": {
"actionpack": "*",
"activerecord": "2.3.5"
},
"base": [],
"resolved": [
{
"name": "actionpack",
"version": "2.3.5",
"dependencies": [
{
"name": "activesupport",
"version": "2.3.5",
"dependencies": []
},
{
"name": "rack",
"version": "1.0",
"dependencies": []
}
]
},
{
"name": "activerecord",
"version": "2.3.5",
"dependencies": [
{
"name": "activesupport",
"version": "2.3.5",
"dependencies": []
}
]
}
],
"conflicts": []
}
{
"rack": [
{
"name": "rack",
"version": "0.8",
"dependencies": {
}
},
{
"name": "rack",
"version": "0.9",
"dependencies": {
}
},
{
"name": "rack",
"version": "0.9.1",
"dependencies": {
}
},
{
"name": "rack",
"version": "0.9.2",
"dependencies": {
}
},
{
"name": "rack",
"version": "1.0",
"dependencies": {
}
},
{
"name": "rack",
"version": "1.1",
"dependencies": {
}
}
],
"rack-mount": [
{
"name": "rack-mount",
"version": "0.4",
"dependencies": {
}
},
{
"name": "rack-mount",
"version": "0.5",
"dependencies": {
}
},
{
"name": "rack-mount",
"version": "0.5.1",
"dependencies": {
}
},
{
"name": "rack-mount",
"version": "0.5.2",
"dependencies": {
}
},
{
"name": "rack-mount",
"version": "0.6",
"dependencies": {
}
}
],
"activesupport": [
{
"name": "activesupport",
"version": "1.2.3",
"dependencies": {
}
},
{
"name": "activesupport",
"version": "2.2.3",
"dependencies": {
}
},
{
"name": "activesupport",
"version": "2.3.5",
"dependencies": {
}
},
{
"name": "activesupport",
"version": "3.0.0-beta",
"dependencies": {
}
},
{
"name": "activesupport",
"version": "3.0.0-beta1",
"dependencies": {
}
}
],
"actionpack": [
{
"name": "actionpack",
"version": "1.2.3",
"dependencies": {
"activesupport": "= 1.2.3"
}
},
{
"name": "actionpack",
"version": "2.2.3",
"dependencies": {
"activesupport": "= 2.2.3",
"rack": "~0.9.0"
}
},
{
"name": "actionpack",
"version": "2.3.5",
"dependencies": {
"activesupport": "= 2.3.5",
"rack": "~1.0.0"
}
},
{
"name": "actionpack",
"version": "3.0.0-beta",
"dependencies": {
"activesupport": "= 3.0.0-beta",
"rack": "~1.1",
"rack-mount": ">= 0.5"
}
},
{
"name": "actionpack",
"version": "3.0.0-beta1",
"dependencies": {
"activesupport": "= 3.0.0-beta1",
"rack": "~1.1",
"rack-mount": ">= 0.5"
}
}
],
"activerecord": [
{
"name": "activerecord",
"version": "1.2.3",
"dependencies": {
"activesupport": "= 1.2.3"
}
},
{
"name": "activerecord",
"version": "2.2.3",
"dependencies": {
"activesupport": "= 2.2.3"
}
},
{
"name": "activerecord",
"version": "2.3.5",
"dependencies": {
"activesupport": "= 2.3.5"
}
},
{
"name": "activerecord",
"version": "3.0.0-beta",
"dependencies": {
"activesupport": "= 3.0.0-beta",
"arel": ">= 0.2"
}
},
{
"name": "activerecord",
"version": "3.0.0-beta1",
"dependencies": {
"activesupport": "= 3.0.0-beta1",
"arel": ">= 0.2"
}
}
],
"actionmailer": [
{
"name": "actionmailer",
"version": "1.2.3",
"dependencies": {
"activesupport": "= 1.2.3",
"actionmailer": "= 1.2.3"
}
},
{
"name": "actionmailer",
"version": "2.2.3",
"dependencies": {
"activesupport": "= 2.2.3",
"actionmailer": "= 2.2.3"
}
},
{
"name": "actionmailer",
"version": "2.3.5",
"dependencies": {
"activesupport": "= 2.3.5",
"actionmailer": "= 2.3.5"
}
},
{
"name": "actionmailer",
"version": "3.0.0-beta",
"dependencies": {
"activesupport": "= 3.0.0-beta",
"actionmailer": "= 3.0.0-beta"
}
},
{
"name": "actionmailer",
"version": "3.0.0-beta1",
"dependencies": {
"activesupport": "= 3.0.0-beta1",
"actionmailer": "= 3.0.0-beta1"
}
}
],
"railties": [
{
"name": "railties",
"version": "1.2.3",
"dependencies": {
"activerecord": "= 1.2.3",
"actionpack": "= 1.2.3",
"actionmailer": "= 1.2.3",
"activesupport": "= 1.2.3"
}
},
{
"name": "railties",
"version": "2.2.3",
"dependencies": {
"activerecord": "= 2.2.3",
"actionpack": "= 2.2.3",
"actionmailer": "= 2.2.3",
"activesupport": "= 2.2.3"
}
},
{
"name": "railties",
"version": "2.3.5",
"dependencies": {
"activerecord": "= 2.3.5",
"actionpack": "= 2.3.5",
"actionmailer": "= 2.3.5",
"activesupport": "= 2.3.5"
}
},
{
"name": "railties",
"version": "3.0.0-beta",
"dependencies": {
}
},
{
"name": "railties",
"version": "3.0.0-beta1",
"dependencies": {
}
}
],
"rails": [
{
"name": "rails",
"version": "3.0.0-beta",
"dependencies": {
"activerecord": "= 3.0.0-beta",
"actionpack": "= 3.0.0-beta",
"actionmailer": "= 3.0.0-beta",
"activesupport": "= 3.0.0-beta",
"railties": "= 3.0.0-beta"
}
},
{
"name": "rails",
"version": "3.0.0-beta1",
"dependencies": {
"activerecord": "= 3.0.0-beta1",
"actionpack": "= 3.0.0-beta1",
"actionmailer": "= 3.0.0-beta1",
"activesupport": "= 3.0.0-beta1",
"railties": "= 3.0.0-beta1"
}
}
],
"nokogiri": [
{
"name": "nokogiri",
"version": "1.0",
"dependencies": {
}
},
{
"name": "nokogiri",
"version": "1.2",
"dependencies": {
}
},
{
"name": "nokogiri",
"version": "1.2.1",
"dependencies": {
}
},
{
"name": "nokogiri",
"version": "1.2.2",
"dependencies": {
}
},
{
"name": "nokogiri",
"version": "1.3",
"dependencies": {
}
},
{
"name": "nokogiri",
"version": "1.3.0-1",
"dependencies": {
}
},
{
"name": "nokogiri",
"version": "1.3.5",
"dependencies": {
}
},
{
"name": "nokogiri",
"version": "1.4.0",
"dependencies": {
}
},
{
"name": "nokogiri",
"version": "1.4.2",
"dependencies": {
}
}
],
"weakling": [
{
"name": "weakling",
"version": "0.0.1",
"dependencies": {
}
},
{
"name": "weakling",
"version": "0.0.2",
"dependencies": {
}
},
{
"name": "weakling",
"version": "0.0.3",
"dependencies": {
}
}
],
"activemerchant": [
{
"name": "activemerchant",
"version": "1.2.3",
"dependencies": {
"activesupport": ">= 1.2.3"
}
},
{
"name": "activemerchant",
"version": "2.2.3",
"dependencies": {
"activesupport": ">= 2.2.3"
}
},
{
"name": "activemerchant",
"version": "2.3.5",
"dependencies": {
"activesupport": ">= 2.3.5"
}
}
]
}
{
"rack": [
{
"name": "rack",
"version": "1.0.1",
"dependencies": {
}
}
],
"foo": [
{
"name": "foo",
"version": "0.2.6",
"dependencies": {
"bar": ">= 0"
}
}
],
"bar": [
{
"name": "bar",
"version": "1.0.0",
"dependencies": {
"foo": ">= 0"
}
}
],
"circular_app": [
{
"name": "circular_app",
"version": "1.0.0",
"dependencies": {
"foo": ">= 0",
"bar": ">= 0"
}
}
]
}
{
"builder": [
{
"name": "builder",
"version": "3.0.4",
"dependencies": {
}
},
{
"name": "builder",
"version": "3.1.4",
"dependencies": {
}
}
],
"grape": [
{
"name": "grape",
"version": "0.2.6",
"dependencies": {
"builder": ">=0"
}
}
],
"activemodel": [
{
"name": "activemodel",
"version": "3.2.8",
"dependencies": {
"builder": "~3.0.0"
}
},
{
"name": "activemodel",
"version": "3.2.9",
"dependencies": {
"builder": "~3.0.0"
}
},
{
"name": "activemodel",
"version": "3.2.10",
"dependencies": {
"builder": "~3.0.0"
}
},
{
"name": "activemodel",
"version": "3.2.11",
"dependencies": {
"builder": "~3.0.0"
}
}
],
"my_app": [
{
"name": "my_app",
"version": "1.0.0",
"dependencies": {
"activemodel": ">=0",
"grape": ">=0"
}
}
]
}
import json
import os
from functools import cmp_to_key
from poetry.mixology.contracts import SpecificationProvider
from poetry.packages import Package, Dependency
from poetry.semver import less_than
from poetry.semver.constraints import Constraint
FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'fixtures')
FIXTURE_INDEX_DIR = os.path.join(FIXTURE_DIR, 'index')
class Index(SpecificationProvider):
_specs_from_fixtures = {}
def __init__(self, packages_by_name):
self._packages = packages_by_name
self._search_for = {}
@property
def packages(self):
return self._packages
@classmethod
def from_fixture(cls, fixture_name):
return cls(cls.specs_from_fixtures(fixture_name))
@classmethod
def specs_from_fixtures(cls, fixture_name):
if fixture_name in cls._specs_from_fixtures:
return cls._specs_from_fixtures[fixture_name]
packages_by_name = {}
with open(os.path.join(FIXTURE_INDEX_DIR, fixture_name + '.json')) as fd:
content = json.load(fd)
for name, releases in content.items():
packages_by_name[name] = []
for release in releases:
package = Package(
name,
release['version'],
release['version']
)
for dependency_name, requirements in release['dependencies'].items():
package.requires.append(
Dependency(dependency_name, requirements)
)
packages_by_name[name].append(package)
packages_by_name[name].sort(
key=cmp_to_key(
lambda x, y:
0 if x.version[1] == y.version[1]
else -1 * int(less_than(x[1], y[1]) or -1)
)
)
return packages_by_name
def is_requirement_satisfied_by(self, requirement, activated, package):
if isinstance(requirement, Package):
return requirement == package
if package.is_prerelease() and not requirement.accepts_prereleases():
vertex = activated.vertex_named(package.name)
if not any([r.accepts_prereleases() for r in vertex.requirements]):
return False
return requirement.constraint.matches(Constraint('==', package.version))
def search_for(self, dependency):
if dependency in self._search_for:
return self._search_for[dependency]
results = []
for spec in self._packages[dependency.name]:
if not dependency.accepts_prereleases() and spec.is_prerelease():
continue
if dependency.constraint.matches(Constraint('==', spec.version)):
results.append(spec)
return results
def name_for(self, dependency):
return dependency.name
def dependencies_for(self, dependency):
return dependency.requires
def sort_dependencies(self,
dependencies,
activated,
conflicts):
return sorted(dependencies, key=lambda d: [
0 if activated.vertex_named(d.name).payload else 1,
0 if d.accepts_prereleases() else 1,
0 if d.name in conflicts else 1,
0 if activated.vertex_named(d.name).payload else len(self.search_for(d))
])
import pytest
from poetry.mixology import DependencyGraph
@pytest.fixture()
def graph():
graph = DependencyGraph()
return graph
@pytest.fixture()
def root(graph):
return graph.add_vertex('Root', 'Root', True)
@pytest.fixture()
def root2(graph):
return graph.add_vertex('Root2', 'Root2', True)
@pytest.fixture()
def child(graph):
return graph.add_child_vertex('Child', 'Child', ['Root'], 'Child')
def test_root_vertex_named(graph, root, root2, child):
assert graph.root_vertex_named('Root') is root
def test_vertex_named(graph, root, root2, child):
assert graph.vertex_named('Root') is root
assert graph.vertex_named('Root2') is root2
assert graph.vertex_named('Child') is child
def test_root_vertex_named_non_existent(graph):
assert graph.root_vertex_named('missing') is None
def test_vertex_named_non_existent(graph):
assert graph.vertex_named('missing') is None
def test_detach_vertex_without_successors(graph):
root = graph.add_vertex('root', 'root', True)
graph.detach_vertex_named(root.name)
assert graph.vertex_named(root.name) is None
assert len(graph.vertices) == 0
def test_detach_vertex_with_successors(graph):
root = graph.add_vertex('root', 'root', True)
child = graph.add_child_vertex('child', 'child', ['root'], 'child')
graph.detach_vertex_named(root.name)
assert graph.vertex_named(root.name) is None
assert graph.vertex_named(child.name) is None
assert len(graph.vertices) == 0
def test_detach_vertex_with_successors_with_other_parents(graph):
root = graph.add_vertex('root', 'root', True)
root2 = graph.add_vertex('root2', 'root2', True)
child = graph.add_child_vertex('child', 'child', ['root', 'root2'], 'child')
graph.detach_vertex_named(root.name)
assert graph.vertex_named(root.name) is None
assert graph.vertex_named(child.name) is child
assert child.predecessors == [root2]
assert len(graph.vertices) == 2
def test_detach_vertex_with_predecessors(graph):
parent = graph.add_vertex('parent', 'parent', True)
child = graph.add_child_vertex('child', 'child', ['parent'], 'child')
graph.detach_vertex_named(child.name)
assert graph.vertex_named(child.name) is None
assert graph.vertices == {parent.name: parent}
assert len(parent.outgoing_edges) == 0
import json
import os
import pytest
from poetry.mixology import DependencyGraph
from poetry.mixology import Resolver
from poetry.mixology.exceptions import ResolverError
from poetry.packages import Dependency
from .index import Index
from .ui import UI
FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'fixtures')
FIXTURE_CASE_DIR = os.path.join(FIXTURE_DIR, 'case')
@pytest.fixture()
def resolver():
return Resolver(Index.from_fixture('awesome'), UI(True))
class Case:
def __init__(self, fixture):
self._fixture = fixture
self.name = fixture['name']
self._requested = None
self._result = None
self._index = None
self._base = None
self._conflicts = None
@property
def requested(self):
if self._requested is not None:
return self._requested
requested = []
for name, requirement in self._fixture['requested'].items():
requested.append(Dependency(name, requirement))
self._requested = requested
return self._requested
@property
def result(self):
if self._result is not None:
return self._result
graph = DependencyGraph()
for resolved in self._fixture['resolved']:
self.add_dependencies_to_graph(graph, None, resolved)
self._result = graph
return self._result
@property
def index(self):
if self._index is None:
self._index = Index.from_fixture(
self._fixture.get('index', 'awesome')
)
return self._index
@property
def base(self):
if self._base is not None:
return self._base
graph = DependencyGraph()
for r in self._fixture['base']:
self.add_dependencies_to_graph(graph, None, r)
self._base = graph
return self._base
@property
def conflicts(self):
if self._conflicts is None:
self._conflicts = self._fixture['conflicts']
return self._conflicts
def add_dependencies_to_graph(self, graph, parent, data, all_parents=None):
if all_parents is None:
all_parents = set()
name = data['name']
version = data['version']
dependency = [s for s in self.index.packages[name] if s.version == version][0]
if parent:
vertex = graph.add_vertex(name, dependency)
graph.add_edge(parent, vertex, dependency)
else:
vertex = graph.add_vertex(name, dependency, True)
if vertex in all_parents:
return
for dep in data['dependencies']:
self.add_dependencies_to_graph(graph, vertex, dep, all_parents)
def case(name):
with open(os.path.join(FIXTURE_CASE_DIR, name + '.json')) as fd:
return Case(json.load(fd))
def assert_graph(dg, result):
packages = sorted(dg.vertices.values(), key=lambda x: x.name)
expected_packages = sorted(result.vertices.values(), key=lambda x: x.name)
assert packages == expected_packages
@pytest.mark.parametrize(
'fixture',
[
'simple',
'simple_with_base',
'simple_with_dependencies',
'simple_with_shared_dependencies',
]
)
def test_resolver(fixture):
c = case(fixture)
resolver = Resolver(c.index, UI())
dg = resolver.resolve(c.requested, base=c.base)
assert_graph(dg, c.result)
import sys
from io import StringIO
from poetry.mixology.contracts import UI as BaseUI
class UI(BaseUI):
def __init__(self, debug=False):
super(UI, self).__init__(debug)
self._output = None
@property
def output(self):
if self._output is None:
if self.debug:
self._output = sys.stderr
else:
self._output = StringIO()
return self._output
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment