Source code for versions.constraints

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]