updated translations
[zeroinstall/zeroinstall-afb.git] / zeroinstall / injector / selections.py
blob0dfd4a9261844bfe6ce83f3820b617fe769161ed
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 import os
10 from zeroinstall import _
11 from zeroinstall.injector import model
12 from zeroinstall.injector.policy import Policy, get_deprecated_singleton_config
13 from zeroinstall.injector.model import process_binding, process_depends, binding_names, Command
14 from zeroinstall.injector.namespaces import XMLNS_IFACE
15 from zeroinstall.injector.qdom import Element, Prefixes
16 from zeroinstall.support import tasks
18 class Selection(object):
19 """A single selected implementation in a L{Selections} set.
20 @ivar dependencies: list of dependencies
21 @type dependencies: [L{model.Dependency}]
22 @ivar attrs: XML attributes map (name is in the format "{namespace} {localName}")
23 @type attrs: {str: 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 def is_available(self, stores):
46 """Is this implementation available locally?
47 (a local implementation or a cached ZeroInstallImplementation)
48 @rtype: bool
49 @since: 0.53"""
50 path = self.local_path
51 if path is not None:
52 return os.path.exists(path)
53 path = stores.lookup_maybe(self.digests)
54 return path is not None
56 class ImplSelection(Selection):
57 """A Selection created from an Implementation"""
59 __slots__ = ['impl', 'dependencies', 'attrs']
61 def __init__(self, iface_uri, impl, dependencies):
62 assert impl
63 self.impl = impl
64 self.dependencies = dependencies
66 attrs = impl.metadata.copy()
67 attrs['id'] = impl.id
68 attrs['version'] = impl.get_version()
69 attrs['interface'] = iface_uri
70 attrs['from-feed'] = impl.feed.url
71 if impl.local_path:
72 attrs['local-path'] = impl.local_path
73 self.attrs = attrs
75 @property
76 def bindings(self): return self.impl.bindings
78 @property
79 def digests(self): return self.impl.digests
81 class XMLSelection(Selection):
82 """A Selection created by reading an XML selections document.
83 @ivar digests: a list of manifest digests
84 @type digests: [str]
85 """
86 __slots__ = ['bindings', 'dependencies', 'attrs', 'digests']
88 def __init__(self, dependencies, bindings = None, attrs = None, digests = None):
89 if bindings is None: bindings = []
90 if digests is None: digests = []
91 self.dependencies = dependencies
92 self.bindings = bindings
93 self.attrs = attrs
94 self.digests = digests
96 assert self.interface
97 assert self.id
98 assert self.version
99 assert self.feed
101 class Selections(object):
103 A selected set of components which will make up a complete program.
104 @ivar interface: the interface of the program
105 @type interface: str
106 @ivar commands: how to run this selection (will contain more than one item if runners are used)
107 @type commands: [{L{Command}}]
108 @ivar selections: the selected implementations
109 @type selections: {str: L{Selection}}
111 __slots__ = ['interface', 'selections', 'commands']
113 def __init__(self, source):
114 """Constructor.
115 @param source: a map of implementations, policy or selections document
116 @type source: {str: L{Selection}} | L{Policy} | L{Element}
118 self.selections = {}
120 if source is None:
121 self.commands = []
122 # (Solver will fill everything in)
123 elif isinstance(source, Policy):
124 self._init_from_policy(source)
125 elif isinstance(source, Element):
126 self._init_from_qdom(source)
127 else:
128 raise Exception(_("Source not a Policy or qdom.Element!"))
130 def _init_from_policy(self, policy):
131 """Set the selections from a policy.
132 @deprecated: use Solver.selections instead
133 @param policy: the policy giving the selected implementations."""
134 self.interface = policy.root
135 self.selections = policy.solver.selections.selections
136 self.commands = policy.solver.selections.commands
138 def _init_from_qdom(self, root):
139 """Parse and load a selections document.
140 @param root: a saved set of selections."""
141 self.interface = root.getAttribute('interface')
142 assert self.interface
143 self.commands = []
145 for selection in root.childNodes:
146 if selection.uri != XMLNS_IFACE:
147 continue
148 if selection.name != 'selection':
149 if selection.name == 'command':
150 self.commands.append(Command(selection, None))
151 continue
153 requires = []
154 bindings = []
155 digests = []
156 for dep_elem in selection.childNodes:
157 if dep_elem.uri != XMLNS_IFACE:
158 continue
159 if dep_elem.name in binding_names:
160 bindings.append(process_binding(dep_elem))
161 elif dep_elem.name == 'requires':
162 dep = process_depends(dep_elem, None)
163 requires.append(dep)
164 elif dep_elem.name == 'manifest-digest':
165 for aname, avalue in dep_elem.attrs.iteritems():
166 digests.append('%s=%s' % (aname, avalue))
168 # For backwards compatibility, allow getting the digest from the ID
169 sel_id = selection.attrs['id']
170 local_path = selection.attrs.get("local-path", None)
171 if (not digests and not local_path) and '=' in sel_id:
172 alg = sel_id.split('=', 1)[0]
173 if alg in ('sha1', 'sha1new', 'sha256'):
174 digests.append(sel_id)
176 iface_uri = selection.attrs['interface']
178 s = XMLSelection(requires, bindings, selection.attrs, digests)
179 self.selections[iface_uri] = s
181 if not self.commands:
182 # Old-style selections document; use the main attribute
183 if iface_uri == self.interface:
184 root_sel = self.selections[self.interface]
185 main = root_sel.attrs.get('main', None)
186 if main is not None:
187 self.commands = [Command(Element(XMLNS_IFACE, 'command', {'path': main}), None)]
189 def toDOM(self):
190 """Create a DOM document for the selected implementations.
191 The document gives the URI of the root, plus each selected implementation.
192 For each selected implementation, we record the ID, the version, the URI and
193 (if different) the feed URL. We also record all the bindings needed.
194 @return: a new DOM Document"""
195 from xml.dom import minidom, XMLNS_NAMESPACE
197 assert self.interface
199 impl = minidom.getDOMImplementation()
201 doc = impl.createDocument(XMLNS_IFACE, "selections", None)
203 root = doc.documentElement
204 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', XMLNS_IFACE)
206 root.setAttributeNS(None, 'interface', self.interface)
208 prefixes = Prefixes()
210 for iface, selection in sorted(self.selections.items()):
211 selection_elem = doc.createElementNS(XMLNS_IFACE, 'selection')
212 selection_elem.setAttributeNS(None, 'interface', selection.interface)
213 root.appendChild(selection_elem)
215 for name, value in selection.attrs.iteritems():
216 if ' ' in name:
217 ns, localName = name.split(' ', 1)
218 selection_elem.setAttributeNS(ns, prefixes.get(ns) + ':' + localName, value)
219 elif name == 'from-feed':
220 # Don't bother writing from-feed attr if it's the same as the interface
221 if value != selection.attrs['interface']:
222 selection_elem.setAttributeNS(None, name, value)
223 elif name not in ('main', 'self-test'): # (replaced by <command>)
224 selection_elem.setAttributeNS(None, name, value)
226 if selection.digests:
227 manifest_digest = doc.createElementNS(XMLNS_IFACE, 'manifest-digest')
228 for digest in selection.digests:
229 aname, avalue = digest.split('=', 1)
230 assert ':' not in aname
231 manifest_digest.setAttribute(aname, avalue)
232 selection_elem.appendChild(manifest_digest)
234 for b in selection.bindings:
235 selection_elem.appendChild(b._toxml(doc))
237 for dep in selection.dependencies:
238 dep_elem = doc.createElementNS(XMLNS_IFACE, 'requires')
239 dep_elem.setAttributeNS(None, 'interface', dep.interface)
240 selection_elem.appendChild(dep_elem)
242 for m in dep.metadata:
243 parts = m.split(' ', 1)
244 if len(parts) == 1:
245 ns = None
246 localName = parts[0]
247 dep_elem.setAttributeNS(None, localName, dep.metadata[m])
248 else:
249 ns, localName = parts
250 dep_elem.setAttributeNS(ns, prefixes.get(ns) + ':' + localName, dep.metadata[m])
252 for b in dep.bindings:
253 dep_elem.appendChild(b._toxml(doc))
255 for command in self.commands:
256 root.appendChild(command._toxml(doc, prefixes))
258 for ns, prefix in prefixes.prefixes.items():
259 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:' + prefix, ns)
261 return doc
263 def __repr__(self):
264 return "Selections for " + self.interface
266 def download_missing(self, config, _old = None):
267 """Check all selected implementations are available.
268 Download any that are not present.
269 Note: package implementations (distribution packages) are ignored.
270 @param config: used to get iface_cache, stores and fetcher
271 @return: a L{tasks.Blocker} or None"""
272 from zeroinstall.zerostore import NotStored
274 if _old:
275 config = get_deprecated_singleton_config()
277 iface_cache = config.iface_cache
278 stores = config.stores
280 # Check that every required selection is cached
281 needed_downloads = []
282 for sel in self.selections.values():
283 if (not sel.local_path) and (not sel.id.startswith('package:')):
284 try:
285 stores.lookup_any(sel.digests)
286 except NotStored:
287 needed_downloads.append(sel)
288 if not needed_downloads:
289 return
291 if config.network_use == model.network_offline:
292 from zeroinstall import NeedDownload
293 raise NeedDownload(', '.join([str(x) for x in needed_downloads]))
295 @tasks.async
296 def download():
297 # We're missing some. For each one, get the feed it came from
298 # and find the corresponding <implementation> in that. This will
299 # tell us where to get it from.
300 # Note: we look for an implementation with the same ID. Maybe we
301 # should check it has the same digest(s) too?
302 needed_impls = []
303 for sel in needed_downloads:
304 feed_url = sel.attrs.get('from-feed', None) or sel.attrs['interface']
305 feed = iface_cache.get_feed(feed_url)
306 if feed is None or sel.id not in feed.implementations:
307 fetch_feed = config.fetcher.download_and_import_feed(feed_url, iface_cache)
308 yield fetch_feed
309 tasks.check(fetch_feed)
311 feed = iface_cache.get_feed(feed_url)
312 assert feed, "Failed to get feed for %s" % feed_url
313 impl = feed.implementations[sel.id]
314 needed_impls.append(impl)
316 fetch_impls = config.fetcher.download_impls(needed_impls, stores)
317 yield fetch_impls
318 tasks.check(fetch_impls)
319 return download()
321 # These (deprecated) methods are to make a Selections object look like the old Policy.implementation map...
323 def __getitem__(self, key):
324 # Deprecated
325 if isinstance(key, basestring):
326 return self.selections[key]
327 sel = self.selections[key.uri]
328 return sel and sel.impl
330 def iteritems(self):
331 # Deprecated
332 iface_cache = get_deprecated_singleton_config().iface_cache
333 for (uri, sel) in self.selections.iteritems():
334 yield (iface_cache.get_interface(uri), sel and sel.impl)
336 def values(self):
337 # Deprecated
338 for (uri, sel) in self.selections.iteritems():
339 yield sel and sel.impl
341 def __iter__(self):
342 # Deprecated
343 iface_cache = get_deprecated_singleton_config().iface_cache
344 for (uri, sel) in self.selections.iteritems():
345 yield iface_cache.get_interface(uri)
347 def get(self, iface, if_missing):
348 # Deprecated
349 sel = self.selections.get(iface.uri, None)
350 if sel:
351 return sel.impl
352 return if_missing
354 def copy(self):
355 # Deprecated
356 s = Selections(None)
357 s.interface = self.interface
358 s.selections = self.selections.copy()
359 return s
361 def items(self):
362 # Deprecated
363 return list(self.iteritems())