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 @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
)
371 del parts
[-1] # Ends with a modifier
375 raise SafeException("Empty version string!")
378 for x
in range(0, l
, 2):
381 parts
[x
] = map(int, parts
[x
].split('.'))
383 parts
[x
] = [] # (because ''.split('.') == [''], not [])
384 for x
in range(1, l
, 2):
385 parts
[x
] = _version_mod_to_value
[parts
[x
]]
387 except ValueError, ex
:
388 raise SafeException("Invalid version format in '%s': %s" % (version_string
, 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}
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
)