import re
import logging
from collections import defaultdict
from .constraint import Constraint
from .operators import eq, lt, gt, le, ge, ne
from .errors import Error
LOGGER = logging.getLogger(__name__)
[docs]class ExclusiveConstraints(Error):
"""Raised when cannot merge a new constraint with pre-existing
constraints.
"""
def __init__(self, constraint, constraints):
#: The conflicting constraint.
self.constraint = constraint
#: The constraints with which it conflicts.
self.constraints = constraints
message = 'Constraint %s conflicts with constraints %s' % (
constraint, ', '.join(str(c) for c in constraints))
super(ExclusiveConstraints, self).__init__(message)
[docs]class Constraints(object):
"""A collection of :class:`Constraint` objects.
"""
def __init__(self, constraints=None):
#: List of :class:`Constraint`.
self.constraints = list(constraints) if constraints else []
def __eq__(self, other):
if isinstance(other, str):
try:
other = Constraints.parse(other)
except Error:
return False
return hash(self) == hash(other)
def __hash__(self):
return hash(tuple(self.constraints))
[docs] def match(self, version):
"""Match ``version`` with this collection of constraints.
:param version: Version to match against the constraint.
:type version: :ref:`version expression <version-expressions>` or \
:class:`.Version`
:rtype: ``True`` if ``version`` satisfies the constraint, \
``False`` if it doesn't.
"""
return all(constraint.match(version)
for constraint in self.constraints)
__contains__ = match
def __str__(self):
return ','.join(str(constraint) for constraint in self.constraints)
def __repr__(self):
if self.constraints:
return 'Constraints.parse(%r)' % str(self)
else:
return 'Constraints()'
def _merge(self, constraint):
"""Wrapper for :func:`merge`.
It merges `constraint` with current ones and returns the list of
merged constraints.
It does not modify the current object.
:param constraint: The constraint(s) to merge with current constraints.
:type: :class:`Constraint`, :class:`Constraints` or `str`
:returns: List of :class:`Constraint` objects.
:rtype: list
"""
if isinstance(constraint, str):
constraints = Constraints.parse(constraint).constraints
elif isinstance(constraint, Constraint):
constraints = [constraint]
elif isinstance(constraint, Constraints):
constraints = constraint.constraints
else:
raise TypeError(constraint)
return merge(self.constraints + constraints)
def __iadd__(self, constraint):
self.constraints = self._merge(constraint)
return self
def __add__(self, constraint):
return Constraints(self._merge(constraint))
@classmethod
[docs] def parse(cls, constraints_expression):
"""Parses a :ref:`constraints_expression <constraints-expressions>`
and returns a :class:`Constraints` object.
"""
constraint_exprs = re.split(r'\s*,\s*', constraints_expression)
return Constraints(merge(Constraint.parse(constraint_expr)
for constraint_expr in constraint_exprs))
def merge(constraints):
"""Merge ``constraints``.
It removes dupplicate, pruned and merged constraints.
:param constraints: Current constraints.
:type constraints: Iterable of :class:`.Constraint` objects.
:rtype: :func:`list` of :class:`.Constraint` objects.
:raises: :exc:`.ExclusiveConstraints`
"""
# Dictionary :class:`Operator`: set of :class:`Version`.
operators = defaultdict(set)
for constraint in constraints:
operators[constraint.operator].add(constraint.version)
# Get most recent version required by > constraints.
if gt in operators:
gt_ver = sorted(operators[gt])[-1]
else:
gt_ver = None
# Get most recent version required by >= constraints.
if ge in operators:
ge_ver = sorted(operators[ge])[-1]
else:
ge_ver = None
# Get least recent version required by < constraints.
if lt in operators:
lt_ver = sorted(operators[lt])[0]
else:
lt_ver = None
# Get least recent version required by <= constraints.
if le in operators:
le_ver = sorted(operators[le])[0]
else:
le_ver = None
# Most restrictive LT/LE constraint.
l_constraint = None
if le_ver:
if lt_ver:
le_constraint = Constraint(le, le_ver)
lt_constraint = Constraint(lt, lt_ver)
if le_ver < lt_ver:
# <= 1, < 2
l_constraint = le_constraint
l_less_restrictive_c = lt_constraint
else:
# <= 2, < 1
# <= 2, < 2
l_constraint = lt_constraint
l_less_restrictive_c = le_constraint
LOGGER.debug('Removed constraint %s because it is less '
'restrictive than %s', l_less_restrictive_c,
l_constraint)
else:
l_constraint = Constraint(le, le_ver)
elif lt_ver:
l_constraint = Constraint(lt, lt_ver)
# Most restrictive GT/GE constraint.
g_constraint = None
if ge_ver:
if gt_ver:
gt_constraint = Constraint(gt, gt_ver)
ge_constraint = Constraint(ge, ge_ver)
if ge_ver <= gt_ver:
# >= 1, > 2
# >= 2, > 2
g_constraint = gt_constraint
g_less_restrictive_c = ge_constraint
else:
# >= 2, > 1
g_constraint = ge_constraint
g_less_restrictive_c = gt_constraint
LOGGER.debug('Removed constraint %s because it is less '
'restrictive than %s', g_less_restrictive_c,
g_constraint)
else:
g_constraint = Constraint(ge, ge_ver)
elif gt_ver:
g_constraint = Constraint(gt, gt_ver)
# Check if g_constraint and l_constraint are conflicting
if g_constraint and l_constraint:
if g_constraint.version == l_constraint.version:
if g_constraint.operator == ge and l_constraint.operator == le:
# Merge >= and <= constraints on same version to a ==
# constraint
operators[eq].add(g_constraint.version)
LOGGER.debug('Merged constraints: %s and %s into ==%s',
l_constraint, g_constraint, g_constraint.version)
l_constraint, g_constraint = None, None
else:
raise ExclusiveConstraints(g_constraint, [l_constraint])
elif g_constraint.version > l_constraint.version:
raise ExclusiveConstraints(g_constraint, [l_constraint])
ne_constraints = [Constraint(ne, v) for v in operators[ne]]
eq_constraints = [Constraint(eq, v) for v in operators[eq]]
if eq_constraints:
eq_constraint = eq_constraints.pop()
# An eq constraint conflicts with other constraints
if g_constraint or l_constraint or ne_constraints or eq_constraints:
conflict_list = [c for c in (g_constraint, l_constraint) if c]
conflict_list.extend(ne_constraints)
conflict_list.extend(eq_constraints)
raise ExclusiveConstraints(eq_constraint, conflict_list)
return [eq_constraint]
else:
constraints = ne_constraints + [g_constraint, l_constraint]
return [c for c in constraints if c]