More updates to support Python 3
[zeroinstall/solver.git] / zeroinstall / injector / reader.py
blobe0a325a8fce3f537061cd12919ad92f43b9bfec7
1 """
2 Parses an XML feed into a Python representation. You should probably use L{iface_cache.iface_cache} rather than the functions here.
3 """
5 # Copyright (C) 2009, Thomas Leonard
6 # See the README file for details, or visit http://0install.net.
8 from zeroinstall import _
9 import os
10 from logging import debug, info, warn
11 import errno
13 from zeroinstall.support import basedir
14 from zeroinstall.injector import qdom
15 from zeroinstall.injector.namespaces import config_site, config_prog, XMLNS_IFACE
16 from zeroinstall.injector.model import Interface, InvalidInterface, ZeroInstallFeed, escape, Feed, stability_levels
17 from zeroinstall.injector import model
19 class MissingLocalFeed(InvalidInterface):
20 pass
22 def _add_site_packages(interface, site_packages, known_site_feeds):
23 for impl in os.listdir(site_packages):
24 if impl.startswith('.'): continue
25 feed = os.path.join(site_packages, impl, '0install', 'feed.xml')
26 if not os.path.exists(feed):
27 warn(_("Site-local feed {path} not found").format(path = feed))
28 debug("Adding site-local feed '%s'", feed)
30 # (we treat these as user overrides in order to let old versions of 0install
31 # find them)
32 interface.extra_feeds.append(Feed(feed, None, user_override = True, site_package = True))
33 known_site_feeds.add(feed)
35 def update_from_cache(interface, iface_cache = None):
36 """Read a cached interface and any native feeds or user overrides.
37 @param interface: the interface object to update
38 @type interface: L{model.Interface}
39 @return: True if cached version and user overrides loaded OK.
40 False if upstream not cached. Local interfaces (starting with /) are
41 always considered to be cached, although they are not actually stored in the cache.
42 Internal: use L{iface_cache.IfaceCache.get_interface} instread.
43 @rtype: bool"""
44 interface.reset()
45 if iface_cache is None:
46 from zeroinstall.injector import policy
47 iface_cache = policy.get_deprecated_singleton_config().iface_cache
49 escaped_uri = model._pretty_escape(interface.uri)
50 # Add the distribution package manager's version, if any
51 path = basedir.load_first_data(config_site, 'native_feeds', escaped_uri)
52 if path:
53 # Resolve any symlinks
54 info(_("Adding native packager feed '%s'"), path)
55 interface.extra_feeds.append(Feed(os.path.realpath(path), None, False))
57 # Add locally-compiled binaries, if any
58 known_site_feeds = set()
59 for path in basedir.load_data_paths(config_site, 'site-packages', escaped_uri):
60 try:
61 _add_site_packages(interface, path, known_site_feeds)
62 except Exception as ex:
63 warn("Error loading site packages from {path}: {ex}".format(path = path, ex = ex))
65 update_user_overrides(interface, known_site_feeds)
67 main_feed = iface_cache.get_feed(interface.uri, force = True)
68 if main_feed:
69 update_user_feed_overrides(main_feed)
71 return main_feed is not None
73 def load_feed_from_cache(url, selections_ok = False):
74 """Load a feed. If the feed is remote, load from the cache. If local, load it directly.
75 @return: the feed, or None if it's remote and not cached."""
76 try:
77 if os.path.isabs(url):
78 debug(_("Loading local feed file '%s'"), url)
79 return load_feed(url, local = True, selections_ok = selections_ok)
80 else:
81 cached = basedir.load_first_cache(config_site, 'interfaces', escape(url))
82 if cached:
83 debug(_("Loading cached information for %(interface)s from %(cached)s"), {'interface': url, 'cached': cached})
84 return load_feed(cached, local = False)
85 else:
86 return None
87 except InvalidInterface as ex:
88 ex.feed_url = url
89 raise
91 def update_user_feed_overrides(feed):
92 """Update a feed with user-supplied information.
93 Sets last_checked and user_stability ratings.
94 @param feed: feed to update
95 @since 0.49
96 """
97 user = basedir.load_first_config(config_site, config_prog,
98 'feeds', model._pretty_escape(feed.url))
99 if user is None:
100 # For files saved by 0launch < 0.49
101 user = basedir.load_first_config(config_site, config_prog,
102 'user_overrides', escape(feed.url))
103 if not user:
104 return
106 try:
107 with open(user, 'rb') as stream:
108 root = qdom.parse(stream)
109 except Exception as ex:
110 warn(_("Error reading '%(user)s': %(exception)s"), {'user': user, 'exception': ex})
111 raise
113 last_checked = root.getAttribute('last-checked')
114 if last_checked:
115 feed.last_checked = int(last_checked)
117 for item in root.childNodes:
118 if item.uri != XMLNS_IFACE: continue
119 if item.name == 'implementation':
120 id = item.getAttribute('id')
121 assert id is not None
122 impl = feed.implementations.get(id, None)
123 if not impl:
124 debug(_("Ignoring user-override for unknown implementation %(id)s in %(interface)s"), {'id': id, 'interface': feed})
125 continue
127 user_stability = item.getAttribute('user-stability')
128 if user_stability:
129 impl.user_stability = stability_levels[str(user_stability)]
131 def update_user_overrides(interface, known_site_feeds = frozenset()):
132 """Update an interface with user-supplied information.
133 Sets preferred stability and updates extra_feeds.
134 @param interface: the interface object to update
135 @type interface: L{model.Interface}
136 @param known_site_feeds: feeds to ignore (for backwards compatibility)
138 user = basedir.load_first_config(config_site, config_prog,
139 'interfaces', model._pretty_escape(interface.uri))
140 if user is None:
141 # For files saved by 0launch < 0.49
142 user = basedir.load_first_config(config_site, config_prog,
143 'user_overrides', escape(interface.uri))
144 if not user:
145 return
147 try:
148 with open(user, 'rb') as stream:
149 root = qdom.parse(stream)
150 except Exception as ex:
151 warn(_("Error reading '%(user)s': %(exception)s"), {'user': user, 'exception': ex})
152 raise
154 stability_policy = root.getAttribute('stability-policy')
155 if stability_policy:
156 interface.set_stability_policy(stability_levels[str(stability_policy)])
158 for item in root.childNodes:
159 if item.uri != XMLNS_IFACE: continue
160 if item.name == 'feed':
161 feed_src = item.getAttribute('src')
162 if not feed_src:
163 raise InvalidInterface(_('Missing "src" attribute in <feed>'))
164 if item.getAttribute('site-package'):
165 # Site packages are detected earlier. This test isn't completely reliable,
166 # since older versions will remove the attribute when saving the config
167 # (hence the next test).
168 continue
169 if feed_src in known_site_feeds:
170 continue
171 interface.extra_feeds.append(Feed(feed_src, item.getAttribute('arch'), True, langs = item.getAttribute('langs')))
173 def check_readable(feed_url, source):
174 """Test whether a feed file is valid.
175 @param feed_url: the feed's expected URL
176 @type feed_url: str
177 @param source: the name of the file to test
178 @type source: str
179 @return: the modification time in src (usually just the mtime of the file)
180 @rtype: int
181 @raise InvalidInterface: If the source's syntax is incorrect,
183 try:
184 feed = load_feed(source, local = False)
186 if feed.url != feed_url:
187 raise InvalidInterface(_("Incorrect URL used for feed.\n\n"
188 "%(feed_url)s is given in the feed, but\n"
189 "%(interface_uri)s was requested") %
190 {'feed_url': feed.url, 'interface_uri': feed_url})
191 return feed.last_modified
192 except InvalidInterface as ex:
193 info(_("Error loading feed:\n"
194 "Interface URI: %(uri)s\n"
195 "Local file: %(source)s\n"
196 "%(exception)s") %
197 {'uri': feed_url, 'source': source, 'exception': ex})
198 raise InvalidInterface(_("Error loading feed '%(uri)s':\n\n%(exception)s") % {'uri': feed_url, 'exception': ex})
200 def update(interface, source, local = False, iface_cache = None):
201 """Read in information about an interface.
202 Deprecated.
203 @param interface: the interface object to update
204 @type interface: L{model.Interface}
205 @param source: the name of the file to read
206 @type source: str
207 @param local: use file's mtime for last-modified, and uri attribute is ignored
208 @raise InvalidInterface: if the source's syntax is incorrect
209 @return: the new feed (since 0.32)
210 @see: L{update_from_cache}, which calls this"""
211 assert isinstance(interface, Interface)
213 feed = load_feed(source, local)
215 if not local:
216 if feed.url != interface.uri:
217 raise InvalidInterface(_("Incorrect URL used for feed.\n\n"
218 "%(feed_url)s is given in the feed, but\n"
219 "%(interface_uri)s was requested") %
220 {'feed_url': feed.url, 'interface_uri': interface.uri})
222 if iface_cache is None:
223 from zeroinstall.injector import policy
224 iface_cache = policy.get_deprecated_singleton_config().iface_cache
225 iface_cache._feeds[model.unicode(interface.uri)] = feed
227 return feed
229 def load_feed(source, local = False, selections_ok = False):
230 """Load a feed from a local file.
231 @param source: the name of the file to read
232 @type source: str
233 @param local: this is a local feed
234 @type local: bool
235 @param selections_ok: if it turns out to be a local selections document, return that instead
236 @type selections_ok: bool
237 @raise InvalidInterface: if the source's syntax is incorrect
238 @return: the new feed
239 @since: 0.48
240 @see: L{iface_cache.iface_cache}, which uses this to load the feeds"""
241 try:
242 with open(source, 'rb') as stream:
243 root = qdom.parse(stream)
244 except IOError as ex:
245 if ex.errno == errno.ENOENT and local:
246 raise MissingLocalFeed(_("Feed not found. Perhaps this is a local feed that no longer exists? You can remove it from the list of feeds in that case."))
247 raise InvalidInterface(_("Can't read file"), ex)
248 except Exception as ex:
249 raise InvalidInterface(_("Invalid XML"), ex)
251 if local:
252 if selections_ok and root.uri == XMLNS_IFACE and root.name == 'selections':
253 from zeroinstall.injector import selections
254 return selections.Selections(root)
255 local_path = source
256 else:
257 local_path = None
258 feed = ZeroInstallFeed(root, local_path)
259 feed.last_modified = int(os.stat(source).st_mtime)
260 return feed