Add support for self-rewrites: <rewrite> not inside a <requires>
[zeroinstall/zeroinstall-mseaborn.git] / zeroinstall / injector / selections.py
blob72a630ebe749a00855112a0a50213c078f8a3a07
1 """
2 Load and save a set of chosen implementations.
3 @since: 0.27
4 """
6 # Copyright (C) 2007, Thomas Leonard
7 # See the README file for details, or visit http://0install.net.
9 from zeroinstall.injector.policy import Policy
10 from zeroinstall.injector.model import process_binding, process_depends, binding_names
11 from zeroinstall.injector.namespaces import XMLNS_IFACE
12 from zeroinstall.injector.qdom import Element
13 from zeroinstall.support import tasks
15 class Selection(object):
16 """A single selected implementation in a L{Selections} set.
17 @ivar dependencies: list of dependencies
18 @type dependencies: [L{model.Dependency}]
19 @ivar attrs: XML attributes map (name is in the format "{namespace} {localName}")
20 @type attrs: {str: str}
21 @ivar version: the implementation's version number
22 @type version: str"""
23 __slots__ = ['bindings', 'dependencies', 'self_rewrites', 'attrs']
25 def __init__(self, dependencies, bindings, self_rewrites, attrs):
26 if bindings is None: bindings = []
27 self.dependencies = dependencies
28 self.bindings = bindings
29 self.self_rewrites = self_rewrites
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 = {'id': impl.id,
82 'version': impl.get_version(),
83 'interface': needed_iface.uri,
84 'from-feed': impl.feed.url}
85 if impl.main:
86 attrs['main'] = impl.main
88 self.selections[needed_iface.uri] = Selection(impl.requires, impl.bindings, impl.self_rewrites, attrs)
90 def _init_from_qdom(self, root):
91 """Parse and load a selections document.
92 @param root: a saved set of selections."""
93 self.interface = root.getAttribute('interface')
94 assert self.interface
96 for selection in root.childNodes:
97 if selection.uri != XMLNS_IFACE:
98 continue
99 if selection.name != 'selection':
100 continue
102 requires = []
103 bindings = []
104 for dep_elem in selection.childNodes:
105 if dep_elem.uri != XMLNS_IFACE:
106 continue
107 if dep_elem.name in binding_names:
108 bindings.append(process_binding(dep_elem))
109 elif dep_elem.name == 'requires':
110 dep = process_depends(dep_elem)
111 requires.append(dep)
113 # TODO: read self-rewrites
114 s = Selection(requires, bindings, [], selection.attrs)
115 self.selections[selection.attrs['interface']] = s
117 def toDOM(self):
118 """Create a DOM document for the selected implementations.
119 The document gives the URI of the root, plus each selected implementation.
120 For each selected implementation, we record the ID, the version, the URI and
121 (if different) the feed URL. We also record all the bindings needed.
122 @return: a new DOM Document"""
123 from xml.dom import minidom, XMLNS_NAMESPACE
125 assert self.interface
127 impl = minidom.getDOMImplementation()
129 doc = impl.createDocument(XMLNS_IFACE, "selections", None)
131 root = doc.documentElement
132 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', XMLNS_IFACE)
134 root.setAttributeNS(None, 'interface', self.interface)
136 def ensure_prefix(prefixes, ns):
137 prefix = prefixes.get(ns, None)
138 if prefix:
139 return prefix
140 prefix = 'ns%d' % len(prefixes)
141 prefixes[ns] = prefix
142 return prefix
144 prefixes = {}
146 for iface, selection in sorted(self.selections.items()):
147 selection_elem = doc.createElementNS(XMLNS_IFACE, 'selection')
148 selection_elem.setAttributeNS(None, 'interface', selection.interface)
149 root.appendChild(selection_elem)
151 for name, value in selection.attrs.iteritems():
152 if ' ' in name:
153 ns, localName = name.split(' ', 1)
154 selection_elem.setAttributeNS(ns, ensure_prefix(prefixes, ns) + ':' + localName, value)
155 elif name != 'from-feed':
156 selection_elem.setAttributeNS(None, name, value)
157 elif value != selection.attrs['interface']:
158 # Don't bother writing from-feed attr if it's the same as the interface
159 selection_elem.setAttributeNS(None, name, value)
161 for b in selection.bindings:
162 selection_elem.appendChild(b._toxml(doc))
164 for dep in selection.dependencies:
165 dep_elem = doc.createElementNS(XMLNS_IFACE, 'requires')
166 dep_elem.setAttributeNS(None, 'interface', dep.interface)
167 selection_elem.appendChild(dep_elem)
169 for m in dep.metadata:
170 parts = m.split(' ', 1)
171 if len(parts) == 1:
172 ns = None
173 localName = parts[0]
174 dep_elem.setAttributeNS(None, localName, dep.metadata[m])
175 else:
176 ns, localName = parts
177 dep_elem.setAttributeNS(ns, ensure_prefix(prefixes, ns) + ':' + localName, dep.metadata[m])
179 for b in dep.bindings:
180 dep_elem.appendChild(b._toxml(doc))
182 for ns, prefix in prefixes.items():
183 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:' + prefix, ns)
185 return doc
187 def __repr__(self):
188 return "Selections for " + self.interface
190 def download_missing(self, iface_cache, fetcher):
191 """Cache all selected implementations are available.
192 Download any that are not present.
193 @param iface_cache: cache to find feeds with download information
194 @param fetcher: used to download missing implementations
195 @return: a L{tasks.Blocker} or None"""
196 from zeroinstall.zerostore import NotStored
198 # Check that every required selection is cached
199 needed_downloads = []
200 for sel in self.selections.values():
201 iid = sel.id
202 if not iid.startswith('/'):
203 try:
204 iface_cache.stores.lookup(iid)
205 except NotStored, ex:
206 needed_downloads.append(sel)
207 if not needed_downloads:
208 return
210 @tasks.async
211 def download():
212 # We're missing some. For each one, get the feed it came from
213 # and find the corresponding <implementation> in that. This will
214 # tell us where to get it from.
215 needed_impls = []
216 for sel in needed_downloads:
217 feed_url = sel.attrs.get('from-feed', None) or sel.attrs['interface']
218 feed = iface_cache.get_feed(feed_url)
219 if feed is None or self.id not in feed.implementations:
220 yield fetcher.download_and_import_feed(feed_url, iface_cache)
221 feed = iface_cache.get_feed(feed_url)
222 assert feed, "Failed to get feed for %s" % feed_url
223 impl = feed.implementations[sel.id]
224 needed_impls.append(impl)
226 yield fetcher.download_impls(needed_impls, iface_cache.stores)
227 return download()