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