Removed 'sources' from model.Interface. It is covered by metadata now.
[zeroinstall.git] / zeroinstall / injector / model.py
blobe44697498260660867a76b98bd5a80040e485081
1 # Copyright (C) 2006, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
4 """In-memory representation of the dependency graph."""
6 import os
7 from zeroinstall import SafeException
9 network_offline = 'off-line'
10 network_minimal = 'minimal'
11 network_full = 'full'
12 network_levels = (network_offline, network_minimal, network_full)
14 stability_levels = {} # Name -> Stability
16 # Default values for the 'default' attribute for <environment> bindings of
17 # well-known variables:
18 defaults = {
19 'PATH': '/bin:/usr/bin',
20 'XDG_CONFIG_DIRS': '/etc/xdg',
21 'XDG_DATA_DIRS': '/usr/local/share:/usr/share',
24 def _split_arch(arch):
25 """Split an arch into an (os, machine) tuple. Either or both parts may be None."""
26 if not arch:
27 return None, None
28 elif '-' not in arch:
29 raise SafeException("Malformed arch '%s'" % arch)
30 else:
31 os, machine = arch.split('-', 1)
32 if os == '*': os = None
33 if machine == '*': machine = None
34 return os, machine
36 def _join_arch(os, machine):
37 if os == machine == None: return None
38 return "%s-%s" % (os or '*', machine or '*')
40 class Stability(object):
41 __slots__ = ['level', 'name', 'description']
42 def __init__(self, level, name, description):
43 self.level = level
44 self.name = name
45 self.description = description
46 assert name not in stability_levels
47 stability_levels[name] = self
49 def __cmp__(self, other):
50 return cmp(self.level, other.level)
52 def __str__(self):
53 return self.name
55 insecure = Stability(0, 'insecure', 'This is a security risk')
56 buggy = Stability(5, 'buggy', 'Known to have serious bugs')
57 developer = Stability(10, 'developer', 'Work-in-progress - bugs likely')
58 testing = Stability(20, 'testing', 'Stability unknown - please test!')
59 stable = Stability(30, 'stable', 'Tested - no serious problems found')
60 preferred = Stability(40, 'preferred', 'Best of all - must be set manually')
62 class Restriction(object):
63 """A Restriction limits the allowed implementations of an Interface."""
65 class Binding(object):
66 """Information about how the choice of a Dependency is made known
67 to the application being run."""
69 class EnvironmentBinding(Binding):
70 __slots__ = ['name', 'insert', 'default']
72 def __init__(self, name, insert, default = None):
73 self.name = name
74 self.insert = insert
75 self.default = default
77 def __str__(self):
78 return "<environ %s += %s>" % (self.name, self.insert)
79 __repr__ = __str__
81 def get_value(self, path, old_value):
82 extra = os.path.join(path, self.insert)
83 if old_value is None:
84 old_value = self.default or defaults.get(self.name, None)
85 if old_value is None:
86 return extra
87 return extra + ':' + old_value
89 class Feed(object):
90 """An interface's feeds are other interfaces whose implementations can also be
91 used as implementations of this interface."""
92 __slots__ = ['uri', 'os', 'machine', 'user_override']
93 def __init__(self, uri, arch, user_override):
94 self.uri = uri
95 # This indicates whether the feed comes from the user's overrides
96 # file. If true, writer.py will write it when saving.
97 self.user_override = user_override
98 self.os, self.machine = _split_arch(arch)
100 def __str__(self):
101 return "<Feed from %s>" % self.uri
102 __repr__ = __str__
104 arch = property(lambda self: _join_arch(self.os, self.machine))
106 class Dependency(object):
107 """A Dependency indicates that an Implementation requires some additional
108 code to function, specified by another Interface."""
109 __slots__ = ['interface', 'restrictions', 'bindings']
111 def __init__(self, interface):
112 assert isinstance(interface, (str, unicode))
113 assert interface
114 self.interface = interface
115 self.restrictions = []
116 self.bindings = []
118 def __str__(self):
119 return "<Dependency on %s; bindings: %d %s>" % (self.interface, len(self.bindings), self.bindings)
121 class DownloadSource(object):
122 """A DownloadSource provides a way to fetch an implementation."""
123 __slots__ = ['implementation', 'url', 'size', 'extract']
125 def __init__(self, implementation, url, size, extract):
126 assert url.startswith('http:') or url.startswith('ftp:') or url.startswith('/')
127 self.implementation = implementation
128 self.url = url
129 self.size = size
130 self.extract = extract
132 class Implementation(object):
133 """An Implementation is a package which implements an Interface."""
134 __slots__ = ['os', 'machine', 'upstream_stability', 'user_stability',
135 'version', 'size', 'dependencies', 'main',
136 'id', 'download_sources', 'released', 'interface']
138 def __init__(self, interface, id):
139 """id can be a local path (string starting with /) or a manifest hash (eg "sha1=XXX")"""
140 assert id
141 self.interface = interface
142 self.id = id
143 self.main = None
144 self.size = None
145 self.version = None
146 self.released = None
147 self.user_stability = None
148 self.upstream_stability = None
149 self.os = None
150 self.machine = None
151 self.dependencies = {} # URI -> Dependency
152 self.download_sources = [] # [DownloadSource]
154 def add_download_source(self, url, size, extract):
155 self.download_sources.append(DownloadSource(self, url, size, extract))
157 def get_stability(self):
158 return self.user_stability or self.upstream_stability or testing
160 def get_version(self):
161 return '.'.join(map(str, self.version))
163 def __str__(self):
164 return self.id
166 def __cmp__(self, other):
167 """Newer versions come first"""
168 return cmp(other.version, self.version)
170 def set_arch(self, arch):
171 self.os, self.machine = _split_arch(arch)
172 arch = property(lambda self: _join_arch(self.os, self.machine), set_arch)
174 class Interface(object):
175 """An Interface represents some contract of behaviour."""
176 __slots__ = ['uri', 'implementations', 'name', 'description', 'summary',
177 'stability_policy', 'last_modified', 'last_local_update', 'last_checked',
178 'main', 'feeds', 'feed_for', 'metadata']
180 # last_local_update is deprecated
182 # stability_policy:
183 # Implementations at this level or higher are preferred.
184 # Lower levels are used only if there is no other choice.
186 def __init__(self, uri):
187 assert uri
188 if uri.startswith('http:') or uri.startswith('/'):
189 self.uri = uri
190 self.reset()
191 else:
192 raise SafeException("Interface name '%s' doesn't start "
193 "with 'http:'" % uri)
195 def reset(self):
196 self.implementations = {} # Path -> Implementation
197 self.name = None
198 self.summary = None
199 self.description = None
200 self.stability_policy = None
201 self.last_modified = None
202 self.last_local_update = None
203 self.last_checked = None
204 self.main = None
205 self.feeds = []
206 self.feed_for = {} # URI -> True
207 self.metadata = []
209 def get_name(self):
210 return self.name or '(' + os.path.basename(self.uri) + ')'
212 def __repr__(self):
213 return "<Interface %s>" % self.uri
215 def get_impl(self, id):
216 if id not in self.implementations:
217 self.implementations[id] = Implementation(self, id)
218 return self.implementations[id]
220 def set_stability_policy(self, new):
221 assert new is None or isinstance(new, Stability)
222 self.stability_policy = new
224 def get_feed(self, uri):
225 for x in self.feeds:
226 if x.uri == uri:
227 return x
228 return None
230 def add_metadata(self, elem):
231 self.metadata.append(elem)
233 def get_metadata(self, uri, name):
234 """Return a list of interface metadata elements with this name and namespace URI."""
235 return [m for m in self.metadata if m.name == name and m.uri == uri]
237 def unescape(uri):
238 "Convert each %20 to a space, etc"
239 if '%' not in uri: return uri
240 import re
241 return re.sub('%[0-9a-fA-F][0-9a-fA-F]',
242 lambda match: chr(int(match.group(0)[1:], 16)),
243 uri)
245 def escape(uri):
246 "Convert each space to %20, etc"
247 import re
248 return re.sub('[^-_.a-zA-Z0-9]',
249 lambda match: '%%%02x' % ord(match.group(0)),
250 uri.encode('utf-8'))
252 def canonical_iface_uri(uri):
253 if uri.startswith('http:'):
254 return uri
255 else:
256 iface_uri = os.path.realpath(uri)
257 if os.path.isfile(iface_uri):
258 return iface_uri
259 raise SafeException("Bad interface name '%s'.\n"
260 "(doesn't start with 'http:', and "
261 "doesn't exist as a local file '%s' either)" %
262 (uri, iface_uri))