Fixed handling of missing local feeds
[zeroinstall.git] / zeroinstall / injector / reader.py
blob674dc1a1c340ecbd651407ee7ce39af847efa211
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 update_from_cache(interface, iface_cache = None):
23 """Read a cached interface and any native feeds or user overrides.
24 @param interface: the interface object to update
25 @type interface: L{model.Interface}
26 @return: True if cached version and user overrides loaded OK.
27 False if upstream not cached. Local interfaces (starting with /) are
28 always considered to be cached, although they are not actually stored in the cache.
29 Internal: use L{iface_cache.IfaceCache.get_interface} instread.
30 @rtype: bool"""
31 interface.reset()
32 if iface_cache is None:
33 from zeroinstall.injector import policy
34 iface_cache = policy.get_deprecated_singleton_config().iface_cache
36 # Add the distribution package manager's version, if any
37 path = basedir.load_first_data(config_site, 'native_feeds', model._pretty_escape(interface.uri))
38 if path:
39 # Resolve any symlinks
40 info(_("Adding native packager feed '%s'"), path)
41 interface.extra_feeds.append(Feed(os.path.realpath(path), None, False))
43 update_user_overrides(interface)
45 main_feed = iface_cache.get_feed(interface.uri, force = True)
46 if main_feed:
47 update_user_feed_overrides(main_feed)
49 return main_feed is not None
51 def load_feed_from_cache(url, selections_ok = False):
52 """Load a feed. If the feed is remote, load from the cache. If local, load it directly.
53 @return: the feed, or None if it's remote and not cached."""
54 try:
55 if os.path.isabs(url):
56 debug(_("Loading local feed file '%s'"), url)
57 return load_feed(url, local = True, selections_ok = selections_ok)
58 else:
59 cached = basedir.load_first_cache(config_site, 'interfaces', escape(url))
60 if cached:
61 debug(_("Loading cached information for %(interface)s from %(cached)s"), {'interface': url, 'cached': cached})
62 return load_feed(cached, local = False)
63 else:
64 return None
65 except InvalidInterface as ex:
66 ex.feed_url = url
67 raise
69 def update_user_feed_overrides(feed):
70 """Update a feed with user-supplied information.
71 Sets last_checked and user_stability ratings.
72 @param feed: feed to update
73 @since 0.49
74 """
75 user = basedir.load_first_config(config_site, config_prog,
76 'feeds', model._pretty_escape(feed.url))
77 if user is None:
78 # For files saved by 0launch < 0.49
79 user = basedir.load_first_config(config_site, config_prog,
80 'user_overrides', escape(feed.url))
81 if not user:
82 return
84 try:
85 root = qdom.parse(open(user))
86 except Exception as ex:
87 warn(_("Error reading '%(user)s': %(exception)s"), {'user': user, 'exception': ex})
88 raise
90 last_checked = root.getAttribute('last-checked')
91 if last_checked:
92 feed.last_checked = int(last_checked)
94 for item in root.childNodes:
95 if item.uri != XMLNS_IFACE: continue
96 if item.name == 'implementation':
97 id = item.getAttribute('id')
98 assert id is not None
99 impl = feed.implementations.get(id, None)
100 if not impl:
101 debug(_("Ignoring user-override for unknown implementation %(id)s in %(interface)s"), {'id': id, 'interface': feed})
102 continue
104 user_stability = item.getAttribute('user-stability')
105 if user_stability:
106 impl.user_stability = stability_levels[str(user_stability)]
108 def update_user_overrides(interface):
109 """Update an interface with user-supplied information.
110 Sets preferred stability and updates extra_feeds.
111 @param interface: the interface object to update
112 @type interface: L{model.Interface}
114 user = basedir.load_first_config(config_site, config_prog,
115 'interfaces', model._pretty_escape(interface.uri))
116 if user is None:
117 # For files saved by 0launch < 0.49
118 user = basedir.load_first_config(config_site, config_prog,
119 'user_overrides', escape(interface.uri))
120 if not user:
121 return
123 try:
124 root = qdom.parse(open(user))
125 except Exception as ex:
126 warn(_("Error reading '%(user)s': %(exception)s"), {'user': user, 'exception': ex})
127 raise
129 stability_policy = root.getAttribute('stability-policy')
130 if stability_policy:
131 interface.set_stability_policy(stability_levels[str(stability_policy)])
133 for item in root.childNodes:
134 if item.uri != XMLNS_IFACE: continue
135 if item.name == 'feed':
136 feed_src = item.getAttribute('src')
137 if not feed_src:
138 raise InvalidInterface(_('Missing "src" attribute in <feed>'))
139 interface.extra_feeds.append(Feed(feed_src, item.getAttribute('arch'), True, langs = item.getAttribute('langs')))
141 def check_readable(feed_url, source):
142 """Test whether a feed file is valid.
143 @param feed_url: the feed's expected URL
144 @type feed_url: str
145 @param source: the name of the file to test
146 @type source: str
147 @return: the modification time in src (usually just the mtime of the file)
148 @rtype: int
149 @raise InvalidInterface: If the source's syntax is incorrect,
151 try:
152 feed = load_feed(source, local = False)
154 if feed.url != feed_url:
155 raise InvalidInterface(_("Incorrect URL used for feed.\n\n"
156 "%(feed_url)s is given in the feed, but\n"
157 "%(interface_uri)s was requested") %
158 {'feed_url': feed.url, 'interface_uri': feed_url})
159 return feed.last_modified
160 except InvalidInterface as ex:
161 info(_("Error loading feed:\n"
162 "Interface URI: %(uri)s\n"
163 "Local file: %(source)s\n"
164 "%(exception)s") %
165 {'uri': feed_url, 'source': source, 'exception': ex})
166 raise InvalidInterface(_("Error loading feed '%(uri)s':\n\n%(exception)s") % {'uri': feed_url, 'exception': ex})
168 def update(interface, source, local = False, iface_cache = None):
169 """Read in information about an interface.
170 Deprecated.
171 @param interface: the interface object to update
172 @type interface: L{model.Interface}
173 @param source: the name of the file to read
174 @type source: str
175 @param local: use file's mtime for last-modified, and uri attribute is ignored
176 @raise InvalidInterface: if the source's syntax is incorrect
177 @return: the new feed (since 0.32)
178 @see: L{update_from_cache}, which calls this"""
179 assert isinstance(interface, Interface)
181 feed = load_feed(source, local)
183 if not local:
184 if feed.url != interface.uri:
185 raise InvalidInterface(_("Incorrect URL used for feed.\n\n"
186 "%(feed_url)s is given in the feed, but\n"
187 "%(interface_uri)s was requested") %
188 {'feed_url': feed.url, 'interface_uri': interface.uri})
190 if iface_cache is None:
191 from zeroinstall.injector import policy
192 iface_cache = policy.get_deprecated_singleton_config().iface_cache
193 iface_cache._feeds[unicode(interface.uri)] = feed
195 return feed
197 def load_feed(source, local = False, selections_ok = False):
198 """Load a feed from a local file.
199 @param source: the name of the file to read
200 @type source: str
201 @param local: this is a local feed
202 @type local: bool
203 @param selections_ok: if it turns out to be a local selections document, return that instead
204 @type selections_ok: bool
205 @raise InvalidInterface: if the source's syntax is incorrect
206 @return: the new feed
207 @since: 0.48
208 @see: L{iface_cache.iface_cache}, which uses this to load the feeds"""
209 try:
210 with open(source) as stream:
211 root = qdom.parse(stream)
212 except IOError as ex:
213 if ex.errno == errno.ENOENT and local:
214 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."))
215 raise InvalidInterface(_("Can't read file"), ex)
216 except Exception as ex:
217 raise InvalidInterface(_("Invalid XML"), ex)
219 if local:
220 if selections_ok and root.uri == XMLNS_IFACE and root.name == 'selections':
221 from zeroinstall.injector import selections
222 return selections.Selections(root)
223 local_path = source
224 else:
225 local_path = None
226 feed = ZeroInstallFeed(root, local_path)
227 feed.last_modified = int(os.stat(source).st_mtime)
228 return feed