Solver.selections is now a real Selections object
[zeroinstall/zeroinstall-afb.git] / zeroinstall / injector / selections.py
blob8fe862f6600532a1b82ccacc5af91debf3675530
1 """
2 Load and save a set of chosen implementations.
3 @since: 0.27
4 """
6 # Copyright (C) 2009, Thomas Leonard
7 # See the README file for details, or visit http://0install.net.
9 from zeroinstall import _
10 from zeroinstall.injector.policy import Policy
11 from zeroinstall.injector.model import process_binding, process_depends, binding_names
12 from zeroinstall.injector.namespaces import XMLNS_IFACE
13 from zeroinstall.injector.qdom import Element
14 from zeroinstall.support import tasks
16 class Selection(object):
17 """A single selected implementation in a L{Selections} set.
18 @ivar dependencies: list of dependencies
19 @type dependencies: [L{model.Dependency}]
20 @ivar attrs: XML attributes map (name is in the format "{namespace} {localName}")
21 @type attrs: {str: str}
22 @ivar digests: a list of manifest digests
23 @type digests: [str]
24 @ivar version: the implementation's version number
25 @type version: str"""
27 interface = property(lambda self: self.attrs['interface'])
28 id = property(lambda self: self.attrs['id'])
29 version = property(lambda self: self.attrs['version'])
30 feed = property(lambda self: self.attrs.get('from-feed', self.interface))
31 main = property(lambda self: self.attrs.get('main', None))
33 @property
34 def local_path(self):
35 local_path = self.attrs.get('local-path', None)
36 if local_path:
37 return local_path
38 if self.id.startswith('/'):
39 return self.id
40 return None
42 def __repr__(self):
43 return self.id
45 class ImplSelection(Selection):
46 __slots__ = ['impl', 'dependencies', 'attrs']
48 def __init__(self, iface_uri, impl, dependencies):
49 assert impl
50 self.impl = impl
51 self.dependencies = dependencies
53 attrs = impl.metadata.copy()
54 attrs['id'] = impl.id
55 attrs['version'] = impl.get_version()
56 attrs['interface'] = iface_uri
57 attrs['from-feed'] = impl.feed.url
58 if impl.local_path:
59 attrs['local-path'] = impl.local_path
60 self.attrs = attrs
62 @property
63 def bindings(self): return self.impl.bindings
65 @property
66 def digests(self): return self.impl.digests
68 class XMLSelection(Selection):
69 __slots__ = ['bindings', 'dependencies', 'attrs', 'digests']
71 def __init__(self, dependencies, bindings = None, attrs = None, digests = None):
72 if bindings is None: bindings = []
73 if digests is None: digests = []
74 self.dependencies = dependencies
75 self.bindings = bindings
76 self.attrs = attrs
77 self.digests = digests
79 assert self.interface
80 assert self.id
81 assert self.version
82 assert self.feed
84 class Selections(object):
85 """
86 A selected set of components which will make up a complete program.
87 @ivar interface: the interface of the program
88 @type interface: str
89 @ivar selections: the selected implementations
90 @type selections: {str: L{Selection}}
91 """
92 __slots__ = ['interface', 'selections']
94 def __init__(self, source):
95 """Constructor.
96 @param source: a map of implementations, policy or selections document
97 @type source: {str: L{Selection}} | L{Policy} | L{Element}
98 """
99 self.selections = {}
101 if source is None:
102 pass
103 elif isinstance(source, Policy):
104 self._init_from_policy(source)
105 elif isinstance(source, Element):
106 self._init_from_qdom(source)
107 else:
108 raise Exception(_("Source not a Policy or qdom.Element!"))
110 def _init_from_policy(self, policy):
111 """Set the selections from a policy.
112 @deprecated: use Solver.selections instead
113 @param policy: the policy giving the selected implementations."""
114 self.interface = policy.root
115 self.selections = policy.solver.selections.selections
117 def _init_from_qdom(self, root):
118 """Parse and load a selections document.
119 @param root: a saved set of selections."""
120 self.interface = root.getAttribute('interface')
121 assert self.interface
123 for selection in root.childNodes:
124 if selection.uri != XMLNS_IFACE:
125 continue
126 if selection.name != 'selection':
127 continue
129 requires = []
130 bindings = []
131 digests = []
132 for dep_elem in selection.childNodes:
133 if dep_elem.uri != XMLNS_IFACE:
134 continue
135 if dep_elem.name in binding_names:
136 bindings.append(process_binding(dep_elem))
137 elif dep_elem.name == 'requires':
138 dep = process_depends(dep_elem)
139 requires.append(dep)
140 elif dep_elem.name == 'manifest-digest':
141 for aname, avalue in dep_elem.attrs.iteritems():
142 digests.append('%s=%s' % (aname, avalue))
144 # For backwards compatibility, allow getting the digest from the ID
145 sel_id = selection.attrs['id']
146 local_path = selection.attrs.get("local-path", None)
147 if (not digests and not local_path) and '=' in sel_id:
148 alg = sel_id.split('=', 1)[0]
149 if alg in ('sha1', 'sha1new', 'sha256'):
150 digests.append(sel_id)
152 s = XMLSelection(requires, bindings, selection.attrs, digests)
153 self.selections[selection.attrs['interface']] = s
155 def toDOM(self):
156 """Create a DOM document for the selected implementations.
157 The document gives the URI of the root, plus each selected implementation.
158 For each selected implementation, we record the ID, the version, the URI and
159 (if different) the feed URL. We also record all the bindings needed.
160 @return: a new DOM Document"""
161 from xml.dom import minidom, XMLNS_NAMESPACE
163 assert self.interface
165 impl = minidom.getDOMImplementation()
167 doc = impl.createDocument(XMLNS_IFACE, "selections", None)
169 root = doc.documentElement
170 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', XMLNS_IFACE)
172 root.setAttributeNS(None, 'interface', self.interface)
174 def ensure_prefix(prefixes, ns):
175 prefix = prefixes.get(ns, None)
176 if prefix:
177 return prefix
178 prefix = 'ns%d' % len(prefixes)
179 prefixes[ns] = prefix
180 return prefix
182 prefixes = {}
184 for iface, selection in sorted(self.selections.items()):
185 selection_elem = doc.createElementNS(XMLNS_IFACE, 'selection')
186 selection_elem.setAttributeNS(None, 'interface', selection.interface)
187 root.appendChild(selection_elem)
189 for name, value in selection.attrs.iteritems():
190 if ' ' in name:
191 ns, localName = name.split(' ', 1)
192 selection_elem.setAttributeNS(ns, ensure_prefix(prefixes, ns) + ':' + localName, value)
193 elif name != 'from-feed':
194 selection_elem.setAttributeNS(None, name, value)
195 elif value != selection.attrs['interface']:
196 # Don't bother writing from-feed attr if it's the same as the interface
197 selection_elem.setAttributeNS(None, name, value)
199 if selection.digests:
200 manifest_digest = doc.createElementNS(XMLNS_IFACE, 'manifest-digest')
201 for digest in selection.digests:
202 aname, avalue = digest.split('=', 1)
203 assert ':' not in aname
204 manifest_digest.setAttribute(aname, avalue)
205 selection_elem.appendChild(manifest_digest)
207 for b in selection.bindings:
208 selection_elem.appendChild(b._toxml(doc))
210 for dep in selection.dependencies:
211 dep_elem = doc.createElementNS(XMLNS_IFACE, 'requires')
212 dep_elem.setAttributeNS(None, 'interface', dep.interface)
213 selection_elem.appendChild(dep_elem)
215 for m in dep.metadata:
216 parts = m.split(' ', 1)
217 if len(parts) == 1:
218 ns = None
219 localName = parts[0]
220 dep_elem.setAttributeNS(None, localName, dep.metadata[m])
221 else:
222 ns, localName = parts
223 dep_elem.setAttributeNS(ns, ensure_prefix(prefixes, ns) + ':' + localName, dep.metadata[m])
225 for b in dep.bindings:
226 dep_elem.appendChild(b._toxml(doc))
228 for ns, prefix in prefixes.items():
229 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:' + prefix, ns)
231 return doc
233 def __repr__(self):
234 return "Selections for " + self.interface
236 def download_missing(self, iface_cache, fetcher):
237 """Check all selected implementations are available.
238 Download any that are not present.
239 Note: package implementations (distribution packages) are ignored.
240 @param iface_cache: cache to find feeds with download information
241 @param fetcher: used to download missing implementations
242 @return: a L{tasks.Blocker} or None"""
243 from zeroinstall.zerostore import NotStored
245 # Check that every required selection is cached
246 needed_downloads = []
247 for sel in self.selections.values():
248 if (not sel.local_path) and (not sel.id.startswith('package:')):
249 try:
250 iface_cache.stores.lookup_any(sel.digests)
251 except NotStored, ex:
252 needed_downloads.append(sel)
253 if not needed_downloads:
254 return
256 @tasks.async
257 def download():
258 # We're missing some. For each one, get the feed it came from
259 # and find the corresponding <implementation> in that. This will
260 # tell us where to get it from.
261 # Note: we look for an implementation with the same ID. Maybe we
262 # should check it has the same digest(s) too?
263 needed_impls = []
264 for sel in needed_downloads:
265 feed_url = sel.attrs.get('from-feed', None) or sel.attrs['interface']
266 feed = iface_cache.get_feed(feed_url)
267 if feed is None or sel.id not in feed.implementations:
268 fetch_feed = fetcher.download_and_import_feed(feed_url, iface_cache)
269 yield fetch_feed
270 tasks.check(fetch_feed)
272 feed = iface_cache.get_feed(feed_url)
273 assert feed, "Failed to get feed for %s" % feed_url
274 impl = feed.implementations[sel.id]
275 needed_impls.append(impl)
277 fetch_impls = fetcher.download_impls(needed_impls, iface_cache.stores)
278 yield fetch_impls
279 tasks.check(fetch_impls)
280 return download()
282 # These (deprecated) methods are to make a Selections object look like the old Policy.implementation map...
284 def __getitem__(self, key):
285 # Deprecated
286 if isinstance(key, basestring):
287 return self.selections[key]
288 sel = self.selections[key.uri]
289 return sel and sel.impl
291 def iteritems(self):
292 # Deprecated
293 from zeroinstall.injector.iface_cache import iface_cache
294 for (uri, sel) in self.selections.iteritems():
295 yield (iface_cache.get_interface(uri), sel and sel.impl)
297 def values(self):
298 # Deprecated
299 from zeroinstall.injector.iface_cache import iface_cache
300 for (uri, sel) in self.selections.iteritems():
301 yield sel and sel.impl
303 def __iter__(self):
304 # Deprecated
305 from zeroinstall.injector.iface_cache import iface_cache
306 for (uri, sel) in self.selections.iteritems():
307 yield iface_cache.get_interface(uri)
309 def get(self, iface, if_missing):
310 # Deprecated
311 sel = self.selections.get(iface.uri, None)
312 if sel:
313 return sel.impl
314 return if_missing
316 def copy(self):
317 # Deprecated
318 s = Selections(None)
319 s.interface = self.interface
320 s.selections = self.selections.copy()
321 return s
323 def items(self):
324 # Deprecated
325 return list(self.iteritems())