~ubuntu-branches/ubuntu/karmic/zeroinstall-injector/karmic

« back to all changes in this revision

Viewing changes to zeroinstall/injector/model.py

  • Committer: Bazaar Package Importer
  • Author(s): Thomas Leonard
  • Date: 2007-01-23 21:50:46 UTC
  • Revision ID: james.westby@ubuntu.com-20070123215046-3ya2x81i99m5ya8r
Tags: upstream-0.25
ImportĀ upstreamĀ versionĀ 0.25

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"""In-memory representation of interfaces and other data structures.
 
2
 
 
3
The objects in this module are used to build a representation of an XML interface
 
4
file in memory.
 
5
 
 
6
@see: L{reader} constructs these data-structures.
 
7
 
 
8
@var defaults: Default values for the 'default' attribute for <environment> bindings of
 
9
well-known variables.
 
10
"""
 
11
 
 
12
# Copyright (C) 2006, Thomas Leonard
 
13
# See the README file for details, or visit http://0install.net.
 
14
 
 
15
import os, re
 
16
from zeroinstall import SafeException
 
17
 
 
18
network_offline = 'off-line'
 
19
network_minimal = 'minimal'
 
20
network_full = 'full'
 
21
network_levels = (network_offline, network_minimal, network_full)
 
22
 
 
23
stability_levels = {}   # Name -> Stability
 
24
 
 
25
defaults = {
 
26
        'PATH': '/bin:/usr/bin',
 
27
        'XDG_CONFIG_DIRS': '/etc/xdg',
 
28
        'XDG_DATA_DIRS': '/usr/local/share:/usr/share',
 
29
}
 
30
 
 
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
 
42
 
 
43
def _join_arch(os, machine):
 
44
        if os == machine == None: return None
 
45
        return "%s-%s" % (os or '*', machine or '*')
 
46
        
 
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
 
57
        
 
58
        def __cmp__(self, other):
 
59
                return cmp(self.level, other.level)
 
60
        
 
61
        def __str__(self):
 
62
                return self.name
 
63
 
 
64
        def __repr__(self):
 
65
                return "<Stability: " + self.description + ">"
 
66
 
 
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')
 
73
 
 
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
 
80
        
 
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
 
87
        
 
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
 
99
 
 
100
class Binding(object):
 
101
        """Information about how the choice of a Dependency is made known
 
102
        to the application being run."""
 
103
 
 
104
class EnvironmentBinding(Binding):
 
105
        """Indicate the chosen implementation using an environment variable."""
 
106
        __slots__ = ['name', 'insert', 'default']
 
107
 
 
108
        def __init__(self, name, insert, default = None):
 
109
                self.name = name
 
110
                self.insert = insert
 
111
                self.default = default
 
112
        
 
113
        def __str__(self):
 
114
                return "<environ %s += %s>" % (self.name, self.insert)
 
115
        __repr__ = __str__
 
116
        
 
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
 
124
 
 
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)
 
135
        
 
136
        def __str__(self):
 
137
                return "<Feed from %s>" % self.uri
 
138
        __repr__ = __str__
 
139
 
 
140
        arch = property(lambda self: _join_arch(self.os, self.machine))
 
141
 
 
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}
 
153
        """
 
154
        __slots__ = ['interface', 'restrictions', 'bindings', 'metadata']
 
155
 
 
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
 
168
        
 
169
        def __str__(self):
 
170
                return "<Dependency on %s; bindings: %s%s>" % (self.interface, self.bindings, self.restrictions)
 
171
 
 
172
class RetrievalMethod(object):
 
173
        """A RetrievalMethod provides a way to fetch an implementation."""
 
174
        __slots__ = []
 
175
 
 
176
class DownloadSource(RetrievalMethod):
 
177
        """A DownloadSource provides a way to fetch an implementation."""
 
178
        __slots__ = ['implementation', 'url', 'size', 'extract', 'start_offset', 'type']
 
179
 
 
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
 
188
 
 
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']
 
196
 
 
197
        def __init__(self):
 
198
                self.steps = []
 
199
        
 
200
        size = property(lambda self: sum([x.size for x in self.steps]))
 
201
 
 
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']
 
207
 
 
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]
 
224
        
 
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))
 
228
        
 
229
        def get_stability(self):
 
230
                return self.user_stability or self.upstream_stability or testing
 
231
        
 
232
        def get_version(self):
 
233
                """Return the version as a string.
 
234
                @see: L{format_version}
 
235
                """
 
236
                return format_version(self.version)
 
237
        
 
238
        def __str__(self):
 
239
                return self.id
 
240
 
 
241
        def __cmp__(self, other):
 
242
                """Newer versions come first"""
 
243
                return cmp(other.version, self.version)
 
244
        
 
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)
 
248
        
 
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']
 
254
 
 
255
        # last_local_update is deprecated
 
256
        
 
257
        # stability_policy:
 
258
        # Implementations at this level or higher are preferred.
 
259
        # Lower levels are used only if there is no other choice.
 
260
 
 
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)
 
269
 
 
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 = []
 
283
        
 
284
        def get_name(self):
 
285
                return self.name or '(' + os.path.basename(self.uri) + ')'
 
286
        
 
287
        def __repr__(self):
 
288
                return "<Interface %s>" % self.uri
 
289
        
 
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]
 
294
        
 
295
        def set_stability_policy(self, new):
 
296
                assert new is None or isinstance(new, Stability)
 
297
                self.stability_policy = new
 
298
        
 
299
        def get_feed(self, uri):
 
300
                for x in self.feeds:
 
301
                        if x.uri == uri:
 
302
                                return x
 
303
                return None
 
304
        
 
305
        def add_metadata(self, elem):
 
306
                self.metadata.append(elem)
 
307
        
 
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]
 
311
 
 
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)
 
320
 
 
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'))
 
328
 
 
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
 
334
        """
 
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))
 
345
 
 
346
_version_mod_to_value = {
 
347
        'pre': -2,
 
348
        'rc': -1,
 
349
        '': 0,
 
350
        'post': 1,
 
351
}
 
352
 
 
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
 
357
 
 
358
_version_re = re.compile('-([a-z]*)')
 
359
 
 
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)
 
369
        if parts[-1] == '':
 
370
                del parts[-1]   # Ends with a modifier
 
371
        else:
 
372
                parts.append('')
 
373
        if not parts:
 
374
                raise SafeException("Empty version string!")
 
375
        l = len(parts)
 
376
        try:
 
377
                for x in range(0, l, 2):
 
378
                        part = parts[x]
 
379
                        if part:
 
380
                                parts[x] = map(int, parts[x].split('.'))
 
381
                        else:
 
382
                                parts[x] = []   # (because ''.split('.') == [''], not [])
 
383
                for x in range(1, l, 2):
 
384
                        parts[x] = _version_mod_to_value[parts[x]]
 
385
                return parts
 
386
        except ValueError, ex:
 
387
                raise SafeException("Invalid version format in '%s': %s" % (version_string, ex))
 
388
        except KeyError, ex:
 
389
                raise SafeException("Invalid version modifier in '%s': %s" % (version_string, ex))
 
390
 
 
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}
 
394
        @rtype: str
 
395
        @since: 0.24"""
 
396
        version = version[:]
 
397
        l = len(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)
 
404