Large-scale API cleanup
[zeroinstall/zeroinstall-afb.git] / zeroinstall / injector / selections.py
blob1358f6f3b6a8572d24ca16eaa34e96cf2eca5b1b
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 import model
11 from zeroinstall.injector.policy import Policy, get_deprecated_singleton_config
12 from zeroinstall.injector.model import process_binding, process_depends, binding_names, Command
13 from zeroinstall.injector.namespaces import XMLNS_IFACE
14 from zeroinstall.injector.qdom import Element, Prefixes
15 from zeroinstall.support import tasks
17 class Selection(object):
18 """A single selected implementation in a L{Selections} set.
19 @ivar dependencies: list of dependencies
20 @type dependencies: [L{model.Dependency}]
21 @ivar attrs: XML attributes map (name is in the format "{namespace} {localName}")
22 @type attrs: {str: str}
23 @ivar digests: a list of manifest digests
24 @type digests: [str]
25 @ivar version: the implementation's version number
26 @type version: str"""
28 interface = property(lambda self: self.attrs['interface'])
29 id = property(lambda self: self.attrs['id'])
30 version = property(lambda self: self.attrs['version'])
31 feed = property(lambda self: self.attrs.get('from-feed', self.interface))
32 main = property(lambda self: self.attrs.get('main', None))
34 @property
35 def local_path(self):
36 local_path = self.attrs.get('local-path', None)
37 if local_path:
38 return local_path
39 if self.id.startswith('/'):
40 return self.id
41 return None
43 def __repr__(self):
44 return self.id
46 class ImplSelection(Selection):
47 __slots__ = ['impl', 'dependencies', 'attrs']
49 def __init__(self, iface_uri, impl, dependencies):
50 assert impl
51 self.impl = impl
52 self.dependencies = dependencies
54 attrs = impl.metadata.copy()
55 attrs['id'] = impl.id
56 attrs['version'] = impl.get_version()
57 attrs['interface'] = iface_uri
58 attrs['from-feed'] = impl.feed.url
59 if impl.local_path:
60 attrs['local-path'] = impl.local_path
61 self.attrs = attrs
63 @property
64 def bindings(self): return self.impl.bindings
66 @property
67 def digests(self): return self.impl.digests
69 class XMLSelection(Selection):
70 __slots__ = ['bindings', 'dependencies', 'attrs', 'digests']
72 def __init__(self, dependencies, bindings = None, attrs = None, digests = None):
73 if bindings is None: bindings = []
74 if digests is None: digests = []
75 self.dependencies = dependencies
76 self.bindings = bindings
77 self.attrs = attrs
78 self.digests = digests
80 assert self.interface
81 assert self.id
82 assert self.version
83 assert self.feed
85 class Selections(object):
86 """
87 A selected set of components which will make up a complete program.
88 @ivar interface: the interface of the program
89 @type interface: str
90 @ivar commands: how to run this selection (will contain more than one item if runners are used)
91 @type commands: [{L{Command}}]
92 @ivar selections: the selected implementations
93 @type selections: {str: L{Selection}}
94 """
95 __slots__ = ['interface', 'selections', 'commands']
97 def __init__(self, source):
98 """Constructor.
99 @param source: a map of implementations, policy or selections document
100 @type source: {str: L{Selection}} | L{Policy} | L{Element}
102 self.selections = {}
104 if source is None:
105 self.commands = []
106 # (Solver will fill everything in)
107 elif isinstance(source, Policy):
108 self._init_from_policy(source)
109 elif isinstance(source, Element):
110 self._init_from_qdom(source)
111 else:
112 raise Exception(_("Source not a Policy or qdom.Element!"))
114 def _init_from_policy(self, policy):
115 """Set the selections from a policy.
116 @deprecated: use Solver.selections instead
117 @param policy: the policy giving the selected implementations."""
118 self.interface = policy.root
119 self.selections = policy.solver.selections.selections
120 self.commands = policy.solver.selections.commands
122 def _init_from_qdom(self, root):
123 """Parse and load a selections document.
124 @param root: a saved set of selections."""
125 self.interface = root.getAttribute('interface')
126 assert self.interface
127 self.commands = []
129 for selection in root.childNodes:
130 if selection.uri != XMLNS_IFACE:
131 continue
132 if selection.name != 'selection':
133 if selection.name == 'command':
134 self.commands.append(Command(selection, None))
135 continue
137 requires = []
138 bindings = []
139 digests = []
140 for dep_elem in selection.childNodes:
141 if dep_elem.uri != XMLNS_IFACE:
142 continue
143 if dep_elem.name in binding_names:
144 bindings.append(process_binding(dep_elem))
145 elif dep_elem.name == 'requires':
146 dep = process_depends(dep_elem, None)
147 requires.append(dep)
148 elif dep_elem.name == 'manifest-digest':
149 for aname, avalue in dep_elem.attrs.iteritems():
150 digests.append('%s=%s' % (aname, avalue))
152 # For backwards compatibility, allow getting the digest from the ID
153 sel_id = selection.attrs['id']
154 local_path = selection.attrs.get("local-path", None)
155 if (not digests and not local_path) and '=' in sel_id:
156 alg = sel_id.split('=', 1)[0]
157 if alg in ('sha1', 'sha1new', 'sha256'):
158 digests.append(sel_id)
160 iface_uri = selection.attrs['interface']
162 s = XMLSelection(requires, bindings, selection.attrs, digests)
163 self.selections[iface_uri] = s
165 if not self.commands:
166 # Old-style selections document; use the main attribute
167 if iface_uri == self.interface:
168 root_sel = self.selections[self.interface]
169 main = root_sel.attrs.get('main', None)
170 if main is not None:
171 self.commands = [Command(Element(XMLNS_IFACE, 'command', {'path': main}), None)]
173 def toDOM(self):
174 """Create a DOM document for the selected implementations.
175 The document gives the URI of the root, plus each selected implementation.
176 For each selected implementation, we record the ID, the version, the URI and
177 (if different) the feed URL. We also record all the bindings needed.
178 @return: a new DOM Document"""
179 from xml.dom import minidom, XMLNS_NAMESPACE
181 assert self.interface
183 impl = minidom.getDOMImplementation()
185 doc = impl.createDocument(XMLNS_IFACE, "selections", None)
187 root = doc.documentElement
188 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', XMLNS_IFACE)
190 root.setAttributeNS(None, 'interface', self.interface)
192 prefixes = Prefixes()
194 for iface, selection in sorted(self.selections.items()):
195 selection_elem = doc.createElementNS(XMLNS_IFACE, 'selection')
196 selection_elem.setAttributeNS(None, 'interface', selection.interface)
197 root.appendChild(selection_elem)
199 for name, value in selection.attrs.iteritems():
200 if ' ' in name:
201 ns, localName = name.split(' ', 1)
202 selection_elem.setAttributeNS(ns, prefixes.get(ns) + ':' + localName, value)
203 elif name == 'from-feed':
204 # Don't bother writing from-feed attr if it's the same as the interface
205 if value != selection.attrs['interface']:
206 selection_elem.setAttributeNS(None, name, value)
207 elif name not in ('main', 'self-test'): # (replaced by <command>)
208 selection_elem.setAttributeNS(None, name, value)
210 if selection.digests:
211 manifest_digest = doc.createElementNS(XMLNS_IFACE, 'manifest-digest')
212 for digest in selection.digests:
213 aname, avalue = digest.split('=', 1)
214 assert ':' not in aname
215 manifest_digest.setAttribute(aname, avalue)
216 selection_elem.appendChild(manifest_digest)
218 for b in selection.bindings:
219 selection_elem.appendChild(b._toxml(doc))
221 for dep in selection.dependencies:
222 dep_elem = doc.createElementNS(XMLNS_IFACE, 'requires')
223 dep_elem.setAttributeNS(None, 'interface', dep.interface)
224 selection_elem.appendChild(dep_elem)
226 for m in dep.metadata:
227 parts = m.split(' ', 1)
228 if len(parts) == 1:
229 ns = None
230 localName = parts[0]
231 dep_elem.setAttributeNS(None, localName, dep.metadata[m])
232 else:
233 ns, localName = parts
234 dep_elem.setAttributeNS(ns, prefixes.get(ns) + ':' + localName, dep.metadata[m])
236 for b in dep.bindings:
237 dep_elem.appendChild(b._toxml(doc))
239 for command in self.commands:
240 root.appendChild(command._toxml(doc, prefixes))
242 for ns, prefix in prefixes.prefixes.items():
243 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:' + prefix, ns)
245 return doc
247 def __repr__(self):
248 return "Selections for " + self.interface
250 def download_missing(self, config, _old = None):
251 """Check all selected implementations are available.
252 Download any that are not present.
253 Note: package implementations (distribution packages) are ignored.
254 @param config: used to get iface_cache, stores and fetcher
255 @return: a L{tasks.Blocker} or None"""
256 from zeroinstall.zerostore import NotStored
258 if _old:
259 config = get_deprecated_singleton_config()
261 iface_cache = config.iface_cache
262 stores = config.stores
264 # Check that every required selection is cached
265 needed_downloads = []
266 for sel in self.selections.values():
267 if (not sel.local_path) and (not sel.id.startswith('package:')):
268 try:
269 stores.lookup_any(sel.digests)
270 except NotStored:
271 needed_downloads.append(sel)
272 if not needed_downloads:
273 return
275 if config.network_use == model.network_offline:
276 from zeroinstall import NeedDownload
277 raise NeedDownload(', '.join([str(x) for x in needed_downloads]))
279 @tasks.async
280 def download():
281 # We're missing some. For each one, get the feed it came from
282 # and find the corresponding <implementation> in that. This will
283 # tell us where to get it from.
284 # Note: we look for an implementation with the same ID. Maybe we
285 # should check it has the same digest(s) too?
286 needed_impls = []
287 for sel in needed_downloads:
288 feed_url = sel.attrs.get('from-feed', None) or sel.attrs['interface']
289 feed = iface_cache.get_feed(feed_url)
290 if feed is None or sel.id not in feed.implementations:
291 fetch_feed = config.fetcher.download_and_import_feed(feed_url, iface_cache)
292 yield fetch_feed
293 tasks.check(fetch_feed)
295 feed = iface_cache.get_feed(feed_url)
296 assert feed, "Failed to get feed for %s" % feed_url
297 impl = feed.implementations[sel.id]
298 needed_impls.append(impl)
300 fetch_impls = config.fetcher.download_impls(needed_impls, stores)
301 yield fetch_impls
302 tasks.check(fetch_impls)
303 return download()
305 # These (deprecated) methods are to make a Selections object look like the old Policy.implementation map...
307 def __getitem__(self, key):
308 # Deprecated
309 if isinstance(key, basestring):
310 return self.selections[key]
311 sel = self.selections[key.uri]
312 return sel and sel.impl
314 def iteritems(self):
315 # Deprecated
316 iface_cache = get_deprecated_singleton_config().iface_cache
317 for (uri, sel) in self.selections.iteritems():
318 yield (iface_cache.get_interface(uri), sel and sel.impl)
320 def values(self):
321 # Deprecated
322 for (uri, sel) in self.selections.iteritems():
323 yield sel and sel.impl
325 def __iter__(self):
326 # Deprecated
327 iface_cache = get_deprecated_singleton_config().iface_cache
328 for (uri, sel) in self.selections.iteritems():
329 yield iface_cache.get_interface(uri)
331 def get(self, iface, if_missing):
332 # Deprecated
333 sel = self.selections.get(iface.uri, None)
334 if sel:
335 return sel.impl
336 return if_missing
338 def copy(self):
339 # Deprecated
340 s = Selections(None)
341 s.interface = self.interface
342 s.selections = self.selections.copy()
343 return s
345 def items(self):
346 # Deprecated
347 return list(self.iteritems())