From 7b6d0605d007236e2146e738ce83864c5809f203 Mon Sep 17 00:00:00 2001 From: Thomas Leonard Date: Wed, 11 Jul 2012 10:12:07 +0100 Subject: [PATCH] Added element --- tests/Conflicts.xml | 27 +++++++++++++++ tests/Versions.xml | 28 +++++++++++++++ tests/testsolver.py | 27 ++++++++++++++- zeroinstall/0launch-gui/iface_browser.py | 2 +- zeroinstall/injector/model.py | 58 +++++++++++++++++++++++--------- zeroinstall/injector/selections.py | 2 ++ zeroinstall/injector/solver.py | 12 +++++-- 7 files changed, 136 insertions(+), 20 deletions(-) create mode 100644 tests/Conflicts.xml create mode 100644 tests/Versions.xml diff --git a/tests/Conflicts.xml b/tests/Conflicts.xml new file mode 100644 index 0000000..c40d663 --- /dev/null +++ b/tests/Conflicts.xml @@ -0,0 +1,27 @@ + + + + Conflicts + test conflicts + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Versions.xml b/tests/Versions.xml new file mode 100644 index 0000000..5ba4613 --- /dev/null +++ b/tests/Versions.xml @@ -0,0 +1,28 @@ + + + Versions + Versions + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/testsolver.py b/tests/testsolver.py index 7bb1aba..8b81884 100755 --- a/tests/testsolver.py +++ b/tests/testsolver.py @@ -4,7 +4,7 @@ import sys, os, locale import unittest sys.path.insert(0, '..') -from zeroinstall.injector import solver, arch +from zeroinstall.injector import solver, arch, model from zeroinstall.injector.requirements import Requirements import logging @@ -212,6 +212,31 @@ class TestSolver(BaseTest): '0.1 Linux-i686', '0.1 Linux-i586', '0.1 Linux-i486'], # ordering of x86 versions selected) + def testRestricts(self): + iface_cache = self.config.iface_cache + s = solver.DefaultSolver(self.config) + uri = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'Conflicts.xml') + versions = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'Versions.xml') + iface = iface_cache.get_interface(uri) + + r = Requirements(uri) + + # Selects 0.2 as the highest version, applying the restriction to versions < 4. + s.solve_for(r) + assert s.ready + self.assertEqual("0.2", s.selections.selections[uri].version) + self.assertEqual("3", s.selections.selections[versions].version) + + s.extra_restrictions[iface] = [model.VersionRestriction(model.parse_version('0.1'))] + s.solve_for(r) + assert s.ready + self.assertEqual("0.1", s.selections.selections[uri].version) + self.assertEqual(None, s.selections.selections.get(versions, None)) + + s.extra_restrictions[iface] = [model.VersionRestriction(model.parse_version('0.3'))] + s.solve_for(r) + assert not s.ready + def testLangs(self): iface_cache = self.config.iface_cache try: diff --git a/zeroinstall/0launch-gui/iface_browser.py b/zeroinstall/0launch-gui/iface_browser.py index 119016e..4fd07e4 100644 --- a/zeroinstall/0launch-gui/iface_browser.py +++ b/zeroinstall/0launch-gui/iface_browser.py @@ -409,7 +409,7 @@ class InterfaceBrowser: iface_cache.get_interface(child.interface), child.get_required_commands(), child.importance == model.Dependency.Essential) - else: + elif not isinstance(child, model.InterfaceRestriction): child_iter = self.model.append(parent) self.model[child_iter][InterfaceBrowser.INTERFACE_NAME] = '?' self.model[child_iter][InterfaceBrowser.SUMMARY] = \ diff --git a/zeroinstall/injector/model.py b/zeroinstall/injector/model.py index 4a61347..2a8fa9a 100644 --- a/zeroinstall/injector/model.py +++ b/zeroinstall/injector/model.py @@ -24,6 +24,8 @@ from zeroinstall import support # Element names for bindings in feed files binding_names = frozenset(['environment', 'overlay', 'executable-in-path', 'executable-in-var']) +_dependency_names = frozenset(['requires', 'restricts']) + network_offline = 'off-line' network_minimal = 'minimal' network_full = 'full' @@ -171,7 +173,11 @@ def process_depends(item, local_feed_dir): attrs['interface'] = dep_iface else: raise InvalidInterface(_('Relative interface URI "%s" in non-local feed') % dep_iface) - dependency = InterfaceDependency(dep_iface, element = item) + + if item.name == 'restricts': + dependency = InterfaceRestriction(dep_iface, element = item) + else: + dependency = InterfaceDependency(dep_iface, element = item) for e in item.childNodes: if e.uri != XMLNS_IFACE: continue @@ -418,8 +424,9 @@ class Dependency(object): """ __slots__ = ['qdom'] - Essential = "essential" - Recommended = "recommended" + Essential = "essential" # Must select a version of the dependency + Recommended = "recommended" # Prefer to select a version + Restricts = "restricts" # Just adds restrictions without expressing any opinion def __init__(self, element): assert isinstance(element, qdom.Element), type(element) # Use InterfaceDependency instead! @@ -429,25 +436,19 @@ class Dependency(object): def metadata(self): return self.qdom.attrs - @property - def importance(self): - return self.qdom.getAttribute("importance") or Dependency.Essential - def get_required_commands(self): """Return a list of command names needed by this dependency""" return [] -class InterfaceDependency(Dependency): - """A Dependency on a Zero Install interface. +class InterfaceRestriction(Dependency): + """A Dependency that restricts the possible choices of a Zero Install interface. @ivar interface: the interface required by this dependency @type interface: str @ivar restrictions: a list of constraints on acceptable implementations @type restrictions: [L{Restriction}] - @ivar bindings: how to make the choice of implementation known - @type bindings: [L{Binding}] - @since: 0.28 + @since: 1.10 """ - __slots__ = ['interface', 'restrictions', 'bindings'] + __slots__ = ['interface', 'restrictions'] def __init__(self, interface, restrictions = None, element = None): Dependency.__init__(self, element) @@ -458,11 +459,36 @@ class InterfaceDependency(Dependency): self.restrictions = [] else: self.restrictions = restrictions + + importance = Dependency.Restricts + bindings = () + + def __str__(self): + return _("") % {'interface': self.interface, 'restrictions': self.restrictions} + +class InterfaceDependency(InterfaceRestriction): + """A Dependency on a Zero Install interface. + @ivar interface: the interface required by this dependency + @type interface: str + @ivar restrictions: a list of constraints on acceptable implementations + @type restrictions: [L{Restriction}] + @ivar bindings: how to make the choice of implementation known + @type bindings: [L{Binding}] + @since: 0.28 + """ + __slots__ = ['bindings'] + + def __init__(self, interface, restrictions = None, element = None): + InterfaceRestriction.__init__(self, interface, restrictions, element) self.bindings = [] def __str__(self): return _("") % {'interface': self.interface, 'bindings': self.bindings, 'restrictions': self.restrictions} + @property + def importance(self): + return self.qdom.getAttribute("importance") or Dependency.Essential + def get_required_commands(self): """Return a list of command names needed by this dependency""" if self.qdom.name == 'runner': @@ -564,7 +590,7 @@ class Command(object): self._runner = None depends = [] for child in self.qdom.childNodes: - if child.name == 'requires': + if child.name in _dependency_names: dep = process_depends(child, self._local_dir) depends.append(dep) elif child.name == 'runner': @@ -768,7 +794,7 @@ class ZeroInstallImplementation(Implementation): # Deprecated dependencies = property(lambda self: dict([(x.interface, x) for x in self.requires - if isinstance(x, InterfaceDependency)])) + if isinstance(x, InterfaceRestriction)])) def add_download_source(self, url, size, extract, start_offset = 0, type = None): """Add a download source.""" @@ -1023,7 +1049,7 @@ class ZeroInstallFeed(object): for child in item.childNodes: if child.uri != XMLNS_IFACE: continue - if child.name == 'requires': + if child.name in _dependency_names: dep = process_depends(child, local_dir) depends.append(dep) elif child.name == 'command': diff --git a/zeroinstall/injector/selections.py b/zeroinstall/injector/selections.py index d4c5782..60f1825 100644 --- a/zeroinstall/injector/selections.py +++ b/zeroinstall/injector/selections.py @@ -297,6 +297,8 @@ class Selections(object): selection_elem.appendChild(b._toxml(doc, prefixes)) for dep in selection.dependencies: + if not isinstance(dep, model.InterfaceDependency): continue + dep_elem = doc.createElementNS(XMLNS_IFACE, 'requires') dep_elem.setAttributeNS(None, 'interface', dep.interface) selection_elem.appendChild(dep_elem) diff --git a/zeroinstall/injector/solver.py b/zeroinstall/injector/solver.py index f76a1ee..2bd0378 100644 --- a/zeroinstall/injector/solver.py +++ b/zeroinstall/injector/solver.py @@ -290,7 +290,9 @@ class SATSolver(Solver): impls_for_iface = {} # Iface -> [impl] - group_clause_for = {} # Iface URI -> AtMostOneClause | bool + # For each interface, the group clause says we can't select two implementations of it at once. + # We use this map at the end to find out what was actually selected. + group_clause_for = {} # Iface URI -> AtMostOneClause group_clause_for_command = {} # (Iface URI, command name) -> AtMostOneClause | bool # Return the dependencies of impl that we should consider. @@ -594,12 +596,18 @@ class SATSolver(Solver): m_groups_clause = None def decide(): - """Recurse through the current selections until we get to an interface with + """This is called by the SAT solver when it cannot simplify the problem further. + Our job is to find the most-optimal next selection to try. + Recurse through the current selections until we get to an interface with no chosen version, then tell the solver to try the best version from that.""" def find_undecided_dep(impl_or_command, arch): # Check for undecided dependencies of impl_or_command for dep in deps_in_use(impl_or_command, arch): + # Restrictions don't express that we do or don't want the + # dependency, so skip them here. + if dep.importance == model.Dependency.Restricts: continue + for c in dep.get_required_commands(): dep_lit = find_undecided_command(dep.interface, c) if dep_lit is not None: -- 2.11.4.GIT