Omit 'stability' attribute when writing selections
[zeroinstall/solver.git] / zeroinstall / injector / selections.py
blob9f13c6db5542f6fdb6ec5d8e31c19ea7b03c3ac6
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(XMLNS_IFACE)
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 prefixes.setAttributeNS(selection_elem, ns, localName, value)
219 elif name == 'stability':
220 pass
221 elif name == 'from-feed':
222 # Don't bother writing from-feed attr if it's the same as the interface
223 if value != selection.attrs['interface']:
224 selection_elem.setAttributeNS(None, name, value)
225 elif name not in ('main', 'self-test'): # (replaced by <command>)
226 selection_elem.setAttributeNS(None, name, value)
228 if selection.digests:
229 manifest_digest = doc.createElementNS(XMLNS_IFACE, 'manifest-digest')
230 for digest in selection.digests:
231 aname, avalue = digest.split('=', 1)
232 assert ':' not in aname
233 manifest_digest.setAttribute(aname, avalue)
234 selection_elem.appendChild(manifest_digest)
236 for b in selection.bindings:
237 selection_elem.appendChild(b._toxml(doc))
239 for dep in selection.dependencies:
240 dep_elem = doc.createElementNS(XMLNS_IFACE, 'requires')
241 dep_elem.setAttributeNS(None, 'interface', dep.interface)
242 selection_elem.appendChild(dep_elem)
244 for m in dep.metadata:
245 parts = m.split(' ', 1)
246 if len(parts) == 1:
247 ns = None
248 localName = parts[0]
249 dep_elem.setAttributeNS(None, localName, dep.metadata[m])
250 else:
251 ns, localName = parts
252 prefixes.setAttributeNS(dep_elem, ns, localName, dep.metadata[m])
254 for b in dep.bindings:
255 dep_elem.appendChild(b._toxml(doc))
257 for command in self.commands:
258 root.appendChild(command._toxml(doc, prefixes))
260 for ns, prefix in prefixes.prefixes.items():
261 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:' + prefix, ns)
263 return doc
265 def __repr__(self):
266 return "Selections for " + self.interface
268 def download_missing(self, config, _old = None):
269 """Check all selected implementations are available.
270 Download any that are not present.
271 Note: package implementations (distribution packages) are ignored.
272 @param config: used to get iface_cache, stores and fetcher
273 @return: a L{tasks.Blocker} or None"""
274 from zeroinstall.zerostore import NotStored
276 if _old:
277 config = get_deprecated_singleton_config()
279 iface_cache = config.iface_cache
280 stores = config.stores
282 # Check that every required selection is cached
283 needed_downloads = []
284 for sel in self.selections.values():
285 if (not sel.local_path) and (not sel.id.startswith('package:')):
286 try:
287 stores.lookup_any(sel.digests)
288 except NotStored:
289 needed_downloads.append(sel)
290 if not needed_downloads:
291 return
293 if config.network_use == model.network_offline:
294 from zeroinstall import NeedDownload
295 raise NeedDownload(', '.join([str(x) for x in needed_downloads]))
297 @tasks.async
298 def download():
299 # We're missing some. For each one, get the feed it came from
300 # and find the corresponding <implementation> in that. This will
301 # tell us where to get it from.
302 # Note: we look for an implementation with the same ID. Maybe we
303 # should check it has the same digest(s) too?
304 needed_impls = []
305 for sel in needed_downloads:
306 feed_url = sel.attrs.get('from-feed', None) or sel.attrs['interface']
307 feed = iface_cache.get_feed(feed_url)
308 if feed is None or sel.id not in feed.implementations:
309 fetch_feed = config.fetcher.download_and_import_feed(feed_url, iface_cache)
310 yield fetch_feed
311 tasks.check(fetch_feed)
313 feed = iface_cache.get_feed(feed_url)
314 assert feed, "Failed to get feed for %s" % feed_url
315 impl = feed.implementations[sel.id]
316 needed_impls.append(impl)
318 fetch_impls = config.fetcher.download_impls(needed_impls, stores)
319 yield fetch_impls
320 tasks.check(fetch_impls)
321 return download()
323 # These (deprecated) methods are to make a Selections object look like the old Policy.implementation map...
325 def __getitem__(self, key):
326 # Deprecated
327 if isinstance(key, basestring):
328 return self.selections[key]
329 sel = self.selections[key.uri]
330 return sel and sel.impl
332 def iteritems(self):
333 # Deprecated
334 iface_cache = get_deprecated_singleton_config().iface_cache
335 for (uri, sel) in self.selections.iteritems():
336 yield (iface_cache.get_interface(uri), sel and sel.impl)
338 def values(self):
339 # Deprecated
340 for (uri, sel) in self.selections.iteritems():
341 yield sel and sel.impl
343 def __iter__(self):
344 # Deprecated
345 iface_cache = get_deprecated_singleton_config().iface_cache
346 for (uri, sel) in self.selections.iteritems():
347 yield iface_cache.get_interface(uri)
349 def get(self, iface, if_missing):
350 # Deprecated
351 sel = self.selections.get(iface.uri, None)
352 if sel:
353 return sel.impl
354 return if_missing
356 def copy(self):
357 # Deprecated
358 s = Selections(None)
359 s.interface = self.interface
360 s.selections = self.selections.copy()
361 return s
363 def items(self):
364 # Deprecated
365 return list(self.iteritems())