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
6
@see: L{reader} constructs these data-structures.
8
@var defaults: Default values for the 'default' attribute for <environment> bindings of
12
# Copyright (C) 2006, Thomas Leonard
13
# See the README file for details, or visit http://0install.net.
16
from zeroinstall import SafeException
18
network_offline = 'off-line'
19
network_minimal = 'minimal'
21
network_levels = (network_offline, network_minimal, network_full)
23
stability_levels = {} # Name -> Stability
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."""
36
raise SafeException("Malformed arch '%s'" % arch)
38
os, machine = arch.split('-', 1)
39
if os == '*': os = None
40
if machine == '*': machine = None
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):
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)
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):
79
self.not_before = not_before
81
def meets_restriction(self, impl):
82
if self.not_before and impl.version < self.not_before:
84
if self.before and impl.version >= self.before:
89
if self.not_before is not None or self.before is not None:
91
if self.not_before is not None:
92
range += format_version(self.not_before) + ' <= '
94
if self.before is not None:
95
range += ' < ' + format_version(self.before)
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):
111
self.default = default
114
return "<environ %s += %s>" % (self.name, self.insert)
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:
123
return extra + ':' + old_value
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):
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)
137
return "<Feed from %s>" % self.uri
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
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))
159
self.interface = interface
160
if restrictions is None:
161
self.restrictions = []
163
self.restrictions = restrictions
167
self.metadata = metadata
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."""
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
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
193
@ivar steps: the sequence of steps which must be performed
194
@type steps: [L{RetrievalMethod}]"""
195
__slots__ = ['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")"""
211
self.interface = interface
217
self.user_stability = None
218
self.upstream_stability = 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)
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
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):
263
if uri.startswith('http:') or uri.startswith('/'):
267
raise SafeException("Interface name '%s' doesn't start "
268
"with 'http:'" % uri)
271
self.implementations = {} # Path -> Implementation
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
281
self.feed_for = {} # URI -> True
285
return self.name or '(' + os.path.basename(self.uri) + ')'
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):
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]
313
"""Convert each %20 to a space, etc.
315
if '%' not in uri: return uri
317
return re.sub('%[0-9a-fA-F][0-9a-fA-F]',
318
lambda match: chr(int(match.group(0)[1:], 16)),
322
"""Convert each space to %20, etc
325
return re.sub('[^-_.a-zA-Z0-9]',
326
lambda match: '%%%02x' % ord(match.group(0)),
329
def canonical_iface_uri(uri):
330
"""If uri is a relative path, convert to an absolute one.
331
Otherwise, return it unmodified.
333
@raise SafeException: if uri isn't valid
335
if uri.startswith('http:'):
338
iface_uri = os.path.realpath(uri)
339
if os.path.isfile(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)" %
346
_version_mod_to_value = {
354
_version_value_to_mod = {}
355
for x in _version_mod_to_value: _version_value_to_mod[_version_mod_to_value[x]] = 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
@since: 0.24 (moved from L{reader}, from where it is still available):"""
367
if version_string is None: return None
368
parts = _version_re.split(version_string)
370
del parts[-1] # Ends with a modifier
374
raise SafeException("Empty version string!")
377
for x in range(0, l, 2):
380
parts[x] = map(int, parts[x].split('.'))
382
parts[x] = [] # (because ''.split('.') == [''], not [])
383
for x in range(1, l, 2):
384
parts[x] = _version_mod_to_value[parts[x]]
386
except ValueError, ex:
387
raise SafeException("Invalid version format in '%s': %s" % (version_string, ex))
389
raise SafeException("Invalid version modifier in '%s': %s" % (version_string, ex))
391
def format_version(version):
392
"""Format a parsed version for display. Undoes the effect of L{parse_version}.
393
@see: L{Implementation.get_version}
398
for x in range(0, l, 2):
399
version[x] = '.'.join(map(str, version[x]))
400
for x in range(1, l, 2):
401
version[x] = '-' + _version_value_to_mod[version[x]]
402
if version[-1] == '-': del version[-1]
403
return ''.join(version)