Minor epydoc fixes.
[zeroinstall.git] / zeroinstall / injector / model.py
blobcb08b7d3c89be4a9a54a0afdbfd0a38e4a85358f
1 """In-memory representation of interfaces and other data structures.
3 The objects in this module are used to build a representation of an XML interface
4 file in memory.
6 @see: L{reader} constructs these data-structures.
8 @var defaults: Default values for the 'default' attribute for <environment> bindings of
9 well-known variables.
10 """
12 # Copyright (C) 2006, Thomas Leonard
13 # See the README file for details, or visit http://0install.net.
15 import os, re
16 from zeroinstall import SafeException
18 network_offline = 'off-line'
19 network_minimal = 'minimal'
20 network_full = 'full'
21 network_levels = (network_offline, network_minimal, network_full)
23 stability_levels = {} # Name -> Stability
25 defaults = {
26 'PATH': '/bin:/usr/bin',
27 'XDG_CONFIG_DIRS': '/etc/xdg',
28 'XDG_DATA_DIRS': '/usr/local/share:/usr/share',
31 def _split_arch(arch):
32 """Split an arch into an (os, machine) tuple. Either or both parts may be None."""
33 if not arch:
34 return None, None
35 elif '-' not in arch:
36 raise SafeException("Malformed arch '%s'" % arch)
37 else:
38 os, machine = arch.split('-', 1)
39 if os == '*': os = None
40 if machine == '*': machine = None
41 return os, machine
43 def _join_arch(os, machine):
44 if os == machine == None: return None
45 return "%s-%s" % (os or '*', machine or '*')
47 class Stability(object):
48 """A stability rating. Each implementation has an upstream stability rating and,
49 optionally, a user-set rating."""
50 __slots__ = ['level', 'name', 'description']
51 def __init__(self, level, name, description):
52 self.level = level
53 self.name = name
54 self.description = description
55 assert name not in stability_levels
56 stability_levels[name] = self
58 def __cmp__(self, other):
59 return cmp(self.level, other.level)
61 def __str__(self):
62 return self.name
64 def __repr__(self):
65 return "<Stability: " + self.description + ">"
67 insecure = Stability(0, 'insecure', 'This is a security risk')
68 buggy = Stability(5, 'buggy', 'Known to have serious bugs')
69 developer = Stability(10, 'developer', 'Work-in-progress - bugs likely')
70 testing = Stability(20, 'testing', 'Stability unknown - please test!')
71 stable = Stability(30, 'stable', 'Tested - no serious problems found')
72 preferred = Stability(40, 'preferred', 'Best of all - must be set manually')
74 class Restriction(object):
75 """A Restriction limits the allowed implementations of an Interface."""
76 __slots__ = ['before', 'not_before']
77 def __init__(self, before, not_before):
78 self.before = before
79 self.not_before = not_before
81 def meets_restriction(self, impl):
82 if self.not_before and impl.version < self.not_before:
83 return False
84 if self.before and impl.version >= self.before:
85 return False
86 return True
88 def __str__(self):
89 if self.not_before is not None or self.before is not None:
90 range = ''
91 if self.not_before is not None:
92 range += format_version(self.not_before) + ' <= '
93 range += 'version'
94 if self.before is not None:
95 range += ' < ' + format_version(self.before)
96 else:
97 range = 'none'
98 return "(restriction: %s)" % range
100 class Binding(object):
101 """Information about how the choice of a Dependency is made known
102 to the application being run."""
104 class EnvironmentBinding(Binding):
105 """Indicate the chosen implementation using an environment variable."""
106 __slots__ = ['name', 'insert', 'default']
108 def __init__(self, name, insert, default = None):
109 self.name = name
110 self.insert = insert
111 self.default = default
113 def __str__(self):
114 return "<environ %s += %s>" % (self.name, self.insert)
115 __repr__ = __str__
117 def get_value(self, path, old_value):
118 extra = os.path.join(path, self.insert)
119 if old_value is None:
120 old_value = self.default or defaults.get(self.name, None)
121 if old_value is None:
122 return extra
123 return extra + ':' + old_value
125 class Feed(object):
126 """An interface's feeds are other interfaces whose implementations can also be
127 used as implementations of this interface."""
128 __slots__ = ['uri', 'os', 'machine', 'user_override']
129 def __init__(self, uri, arch, user_override):
130 self.uri = uri
131 # This indicates whether the feed comes from the user's overrides
132 # file. If true, writer.py will write it when saving.
133 self.user_override = user_override
134 self.os, self.machine = _split_arch(arch)
136 def __str__(self):
137 return "<Feed from %s>" % self.uri
138 __repr__ = __str__
140 arch = property(lambda self: _join_arch(self.os, self.machine))
142 class Dependency(object):
143 """A Dependency indicates that an Implementation requires some additional
144 code to function, specified by another Interface.
145 @ivar interface: the interface required by this dependency
146 @type interface: str
147 @ivar restrictions: a list of constraints on acceptable implementations
148 @type restrictions: [L{Restriction}]
149 @ivar bindings: how to make the choice of implementation known
150 @type bindings: [L{Binding}]
151 @ivar metadata: any extra attributes from the XML element
152 @type metadata: {str: str}
154 __slots__ = ['interface', 'restrictions', 'bindings', 'metadata']
156 def __init__(self, interface, restrictions = None, metadata = None):
157 assert isinstance(interface, (str, unicode))
158 assert interface
159 self.interface = interface
160 if restrictions is None:
161 self.restrictions = []
162 else:
163 self.restrictions = restrictions
164 self.bindings = []
165 if metadata is None:
166 metadata = {}
167 self.metadata = metadata
169 def __str__(self):
170 return "<Dependency on %s; bindings: %s%s>" % (self.interface, self.bindings, self.restrictions)
172 class RetrievalMethod(object):
173 """A RetrievalMethod provides a way to fetch an implementation."""
174 __slots__ = []
176 class DownloadSource(RetrievalMethod):
177 """A DownloadSource provides a way to fetch an implementation."""
178 __slots__ = ['implementation', 'url', 'size', 'extract', 'start_offset', 'type']
180 def __init__(self, implementation, url, size, extract, start_offset = 0, type = None):
181 assert url.startswith('http:') or url.startswith('ftp:') or url.startswith('/')
182 self.implementation = implementation
183 self.url = url
184 self.size = size
185 self.extract = extract
186 self.start_offset = start_offset
187 self.type = type # MIME type - see unpack.py
189 class Recipe(RetrievalMethod):
190 """Get an implementation by following a series of steps.
191 @ivar size: the combined download sizes from all the steps
192 @type size: int
193 @ivar steps: the sequence of steps which must be performed
194 @type steps: [L{RetrievalMethod}]"""
195 __slots__ = ['steps']
197 def __init__(self):
198 self.steps = []
200 size = property(lambda self: sum([x.size for x in self.steps]))
202 class Implementation(object):
203 """An Implementation is a package which implements an Interface."""
204 __slots__ = ['os', 'machine', 'upstream_stability', 'user_stability',
205 'version', 'size', 'dependencies', 'main', 'metadata',
206 'id', 'download_sources', 'released', 'interface']
208 def __init__(self, interface, id):
209 """id can be a local path (string starting with /) or a manifest hash (eg "sha1=XXX")"""
210 assert id
211 self.interface = interface
212 self.id = id
213 self.main = None
214 self.size = None
215 self.version = None
216 self.released = None
217 self.user_stability = None
218 self.upstream_stability = None
219 self.os = None
220 self.machine = None
221 self.metadata = {} # [URI + " "] + localName -> value
222 self.dependencies = {} # URI -> Dependency
223 self.download_sources = [] # [RetrievalMethod]
225 def add_download_source(self, url, size, extract, start_offset = 0, type = None):
226 """Add a download source."""
227 self.download_sources.append(DownloadSource(self, url, size, extract, start_offset, type))
229 def get_stability(self):
230 return self.user_stability or self.upstream_stability or testing
232 def get_version(self):
233 """Return the version as a string.
234 @see: L{format_version}
236 return format_version(self.version)
238 def __str__(self):
239 return self.id
241 def __cmp__(self, other):
242 """Newer versions come first"""
243 return cmp(other.version, self.version)
245 def set_arch(self, arch):
246 self.os, self.machine = _split_arch(arch)
247 arch = property(lambda self: _join_arch(self.os, self.machine), set_arch)
249 class Interface(object):
250 """An Interface represents some contract of behaviour."""
251 __slots__ = ['uri', 'implementations', 'name', 'description', 'summary',
252 'stability_policy', 'last_modified', 'last_local_update', 'last_checked',
253 'main', 'feeds', 'feed_for', 'metadata']
255 # last_local_update is deprecated
257 # stability_policy:
258 # Implementations at this level or higher are preferred.
259 # Lower levels are used only if there is no other choice.
261 def __init__(self, uri):
262 assert uri
263 if uri.startswith('http:') or uri.startswith('/'):
264 self.uri = uri
265 self.reset()
266 else:
267 raise SafeException("Interface name '%s' doesn't start "
268 "with 'http:'" % uri)
270 def reset(self):
271 self.implementations = {} # Path -> Implementation
272 self.name = None
273 self.summary = None
274 self.description = None
275 self.stability_policy = None
276 self.last_modified = None
277 self.last_local_update = None
278 self.last_checked = None
279 self.main = None
280 self.feeds = []
281 self.feed_for = {} # URI -> True
282 self.metadata = []
284 def get_name(self):
285 return self.name or '(' + os.path.basename(self.uri) + ')'
287 def __repr__(self):
288 return "<Interface %s>" % self.uri
290 def get_impl(self, id):
291 if id not in self.implementations:
292 self.implementations[id] = Implementation(self, id)
293 return self.implementations[id]
295 def set_stability_policy(self, new):
296 assert new is None or isinstance(new, Stability)
297 self.stability_policy = new
299 def get_feed(self, uri):
300 for x in self.feeds:
301 if x.uri == uri:
302 return x
303 return None
305 def add_metadata(self, elem):
306 self.metadata.append(elem)
308 def get_metadata(self, uri, name):
309 """Return a list of interface metadata elements with this name and namespace URI."""
310 return [m for m in self.metadata if m.name == name and m.uri == uri]
312 def unescape(uri):
313 """Convert each %20 to a space, etc.
314 @rtype: str"""
315 if '%' not in uri: return uri
316 import re
317 return re.sub('%[0-9a-fA-F][0-9a-fA-F]',
318 lambda match: chr(int(match.group(0)[1:], 16)),
319 uri)
321 def escape(uri):
322 """Convert each space to %20, etc
323 @rtype: str"""
324 import re
325 return re.sub('[^-_.a-zA-Z0-9]',
326 lambda match: '%%%02x' % ord(match.group(0)),
327 uri.encode('utf-8'))
329 def canonical_iface_uri(uri):
330 """If uri is a relative path, convert to an absolute one.
331 Otherwise, return it unmodified.
332 @rtype: str
333 @raise SafeException: if uri isn't valid
335 if uri.startswith('http:'):
336 return uri
337 else:
338 iface_uri = os.path.realpath(uri)
339 if os.path.isfile(iface_uri):
340 return iface_uri
341 raise SafeException("Bad interface name '%s'.\n"
342 "(doesn't start with 'http:', and "
343 "doesn't exist as a local file '%s' either)" %
344 (uri, iface_uri))
346 _version_mod_to_value = {
347 'pre': -2,
348 'rc': -1,
349 '': 0,
350 'post': 1,
353 # Reverse mapping
354 _version_value_to_mod = {}
355 for x in _version_mod_to_value: _version_value_to_mod[_version_mod_to_value[x]] = x
356 del x
358 _version_re = re.compile('-([a-z]*)')
360 def parse_version(version_string):
361 """Convert a version string to an internal representation.
362 The parsed format can be compared quickly using the standard Python functions.
363 - Version := DottedList ("-" Mod DottedList?)*
364 - DottedList := (Integer ("." Integer)*)
365 @rtype: tuple (opaque)
366 @raise SafeException: if the string isn't a valid version
367 @since: 0.24 (moved from L{reader}, from where it is still available):"""
368 if version_string is None: return None
369 parts = _version_re.split(version_string)
370 if parts[-1] == '':
371 del parts[-1] # Ends with a modifier
372 else:
373 parts.append('')
374 if not parts:
375 raise SafeException("Empty version string!")
376 l = len(parts)
377 try:
378 for x in range(0, l, 2):
379 part = parts[x]
380 if part:
381 parts[x] = map(int, parts[x].split('.'))
382 else:
383 parts[x] = [] # (because ''.split('.') == [''], not [])
384 for x in range(1, l, 2):
385 parts[x] = _version_mod_to_value[parts[x]]
386 return parts
387 except ValueError, ex:
388 raise SafeException("Invalid version format in '%s': %s" % (version_string, ex))
389 except KeyError, ex:
390 raise SafeException("Invalid version modifier in '%s': %s" % (version_string, ex))
392 def format_version(version):
393 """Format a parsed version for display. Undoes the effect of L{parse_version}.
394 @see: L{Implementation.get_version}
395 @rtype: str
396 @since: 0.24"""
397 version = version[:]
398 l = len(version)
399 for x in range(0, l, 2):
400 version[x] = '.'.join(map(str, version[x]))
401 for x in range(1, l, 2):
402 version[x] = '-' + _version_value_to_mod[version[x]]
403 if version[-1] == '-': del version[-1]
404 return ''.join(version)