Import _ into each module rather than using a builtin
[zeroinstall/zeroinstall-rsl.git] / zeroinstall / injector / selections.py
blob8c8c3681c8541da5f78f622fc335e4da9e38d11c
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 version: the implementation's version number
23 @type version: str"""
24 __slots__ = ['bindings', 'dependencies', 'attrs']
26 def __init__(self, dependencies, bindings = None, attrs = None):
27 if bindings is None: bindings = []
28 self.dependencies = dependencies
29 self.bindings = bindings
30 self.attrs = attrs
32 assert self.interface
33 assert self.id
34 assert self.version
35 assert self.feed
37 interface = property(lambda self: self.attrs['interface'])
38 id = property(lambda self: self.attrs['id'])
39 version = property(lambda self: self.attrs['version'])
40 feed = property(lambda self: self.attrs.get('from-feed', self.interface))
41 main = property(lambda self: self.attrs.get('main', None))
43 def __repr__(self):
44 return self.id
46 class Selections(object):
47 """
48 A selected set of components which will make up a complete program.
49 @ivar interface: the interface of the program
50 @type interface: str
51 @ivar selections: the selected implementations
52 @type selections: {str: L{Selection}}
53 """
54 __slots__ = ['interface', 'selections']
56 def __init__(self, source):
57 """Constructor.
58 @param source: a map of implementations, policy or selections document
59 @type source: {str: L{Selection}} | L{Policy} | L{Element}
60 """
61 if isinstance(source, dict):
62 self.selections = source
63 elif isinstance(source, Policy):
64 self.selections = {}
65 self._init_from_policy(source)
66 elif isinstance(source, Element):
67 self.selections = {}
68 self._init_from_qdom(source)
69 else:
70 raise Exception(_("Source not a Policy or qdom.Element!"))
72 def _init_from_policy(self, policy):
73 """Set the selections from a policy.
74 @param policy: the policy giving the selected implementations."""
75 self.interface = policy.root
77 for needed_iface in policy.implementation:
78 impl = policy.implementation[needed_iface]
79 assert impl
81 attrs = impl.metadata.copy()
82 attrs['id'] = impl.id
83 attrs['version'] = impl.get_version()
84 attrs['interface'] = needed_iface.uri
85 attrs['from-feed'] = impl.feed.url
87 self.selections[needed_iface.uri] = Selection(impl.requires, impl.bindings, attrs)
89 def _init_from_qdom(self, root):
90 """Parse and load a selections document.
91 @param root: a saved set of selections."""
92 self.interface = root.getAttribute('interface')
93 assert self.interface
95 for selection in root.childNodes:
96 if selection.uri != XMLNS_IFACE:
97 continue
98 if selection.name != 'selection':
99 continue
101 requires = []
102 bindings = []
103 for dep_elem in selection.childNodes:
104 if dep_elem.uri != XMLNS_IFACE:
105 continue
106 if dep_elem.name in binding_names:
107 bindings.append(process_binding(dep_elem))
108 elif dep_elem.name == 'requires':
109 dep = process_depends(dep_elem)
110 requires.append(dep)
112 s = Selection(requires, bindings, selection.attrs)
113 self.selections[selection.attrs['interface']] = s
115 def toDOM(self):
116 """Create a DOM document for the selected implementations.
117 The document gives the URI of the root, plus each selected implementation.
118 For each selected implementation, we record the ID, the version, the URI and
119 (if different) the feed URL. We also record all the bindings needed.
120 @return: a new DOM Document"""
121 from xml.dom import minidom, XMLNS_NAMESPACE
123 assert self.interface
125 impl = minidom.getDOMImplementation()
127 doc = impl.createDocument(XMLNS_IFACE, "selections", None)
129 root = doc.documentElement
130 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', XMLNS_IFACE)
132 root.setAttributeNS(None, 'interface', self.interface)
134 def ensure_prefix(prefixes, ns):
135 prefix = prefixes.get(ns, None)
136 if prefix:
137 return prefix
138 prefix = 'ns%d' % len(prefixes)
139 prefixes[ns] = prefix
140 return prefix
142 prefixes = {}
144 for iface, selection in sorted(self.selections.items()):
145 selection_elem = doc.createElementNS(XMLNS_IFACE, 'selection')
146 selection_elem.setAttributeNS(None, 'interface', selection.interface)
147 root.appendChild(selection_elem)
149 for name, value in selection.attrs.iteritems():
150 if ' ' in name:
151 ns, localName = name.split(' ', 1)
152 selection_elem.setAttributeNS(ns, ensure_prefix(prefixes, ns) + ':' + localName, value)
153 elif name != 'from-feed':
154 selection_elem.setAttributeNS(None, name, value)
155 elif value != selection.attrs['interface']:
156 # Don't bother writing from-feed attr if it's the same as the interface
157 selection_elem.setAttributeNS(None, name, value)
159 for b in selection.bindings:
160 selection_elem.appendChild(b._toxml(doc))
162 for dep in selection.dependencies:
163 dep_elem = doc.createElementNS(XMLNS_IFACE, 'requires')
164 dep_elem.setAttributeNS(None, 'interface', dep.interface)
165 selection_elem.appendChild(dep_elem)
167 for m in dep.metadata:
168 parts = m.split(' ', 1)
169 if len(parts) == 1:
170 ns = None
171 localName = parts[0]
172 dep_elem.setAttributeNS(None, localName, dep.metadata[m])
173 else:
174 ns, localName = parts
175 dep_elem.setAttributeNS(ns, ensure_prefix(prefixes, ns) + ':' + localName, dep.metadata[m])
177 for b in dep.bindings:
178 dep_elem.appendChild(b._toxml(doc))
180 for ns, prefix in prefixes.items():
181 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:' + prefix, ns)
183 return doc
185 def __repr__(self):
186 return "Selections for " + self.interface
188 def download_missing(self, iface_cache, fetcher):
189 """Cache all selected implementations are available.
190 Download any that are not present.
191 @param iface_cache: cache to find feeds with download information
192 @param fetcher: used to download missing implementations
193 @return: a L{tasks.Blocker} or None"""
194 from zeroinstall.zerostore import NotStored
196 # Check that every required selection is cached
197 needed_downloads = []
198 for sel in self.selections.values():
199 iid = sel.id
200 if not iid.startswith('/'):
201 try:
202 iface_cache.stores.lookup(iid)
203 except NotStored, ex:
204 needed_downloads.append(sel)
205 if not needed_downloads:
206 return
208 @tasks.async
209 def download():
210 # We're missing some. For each one, get the feed it came from
211 # and find the corresponding <implementation> in that. This will
212 # tell us where to get it from.
213 needed_impls = []
214 for sel in needed_downloads:
215 feed_url = sel.attrs.get('from-feed', None) or sel.attrs['interface']
216 feed = iface_cache.get_feed(feed_url)
217 if feed is None or sel.id not in feed.implementations:
218 yield fetcher.download_and_import_feed(feed_url, iface_cache)
219 feed = iface_cache.get_feed(feed_url)
220 assert feed, "Failed to get feed for %s" % feed_url
221 impl = feed.implementations[sel.id]
222 needed_impls.append(impl)
224 yield fetcher.download_impls(needed_impls, iface_cache.stores)
225 return download()