##// END OF EJS Templates
extensions: support callbacks after another extension loads...
Gregory Szorc -
r24065:d8837ad6 default
parent child Browse files
Show More
@@ -1,380 +1,409
1 1 # extensions.py - extension handling for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 import imp, os
9 9 import util, cmdutil, error
10 10 from i18n import _, gettext
11 11
12 12 _extensions = {}
13 _aftercallbacks = {}
13 14 _order = []
14 15 _ignore = ['hbisect', 'bookmarks', 'parentrevspec', 'interhg', 'inotify']
15 16
16 17 def extensions(ui=None):
17 18 if ui:
18 19 def enabled(name):
19 20 for format in ['%s', 'hgext.%s']:
20 21 conf = ui.config('extensions', format % name)
21 22 if conf is not None and not conf.startswith('!'):
22 23 return True
23 24 else:
24 25 enabled = lambda name: True
25 26 for name in _order:
26 27 module = _extensions[name]
27 28 if module and enabled(name):
28 29 yield name, module
29 30
30 31 def find(name):
31 32 '''return module with given extension name'''
32 33 mod = None
33 34 try:
34 35 mod = _extensions[name]
35 36 except KeyError:
36 37 for k, v in _extensions.iteritems():
37 38 if k.endswith('.' + name) or k.endswith('/' + name):
38 39 mod = v
39 40 break
40 41 if not mod:
41 42 raise KeyError(name)
42 43 return mod
43 44
44 45 def loadpath(path, module_name):
45 46 module_name = module_name.replace('.', '_')
46 47 path = util.normpath(util.expandpath(path))
47 48 if os.path.isdir(path):
48 49 # module/__init__.py style
49 50 d, f = os.path.split(path)
50 51 fd, fpath, desc = imp.find_module(f, [d])
51 52 return imp.load_module(module_name, fd, fpath, desc)
52 53 else:
53 54 try:
54 55 return imp.load_source(module_name, path)
55 56 except IOError, exc:
56 57 if not exc.filename:
57 58 exc.filename = path # python does not fill this
58 59 raise
59 60
60 61 def load(ui, name, path):
61 62 if name.startswith('hgext.') or name.startswith('hgext/'):
62 63 shortname = name[6:]
63 64 else:
64 65 shortname = name
65 66 if shortname in _ignore:
66 67 return None
67 68 if shortname in _extensions:
68 69 return _extensions[shortname]
69 70 _extensions[shortname] = None
70 71 if path:
71 72 # the module will be loaded in sys.modules
72 73 # choose an unique name so that it doesn't
73 74 # conflicts with other modules
74 75 mod = loadpath(path, 'hgext.%s' % name)
75 76 else:
76 77 def importh(name):
77 78 mod = __import__(name)
78 79 components = name.split('.')
79 80 for comp in components[1:]:
80 81 mod = getattr(mod, comp)
81 82 return mod
82 83 try:
83 84 mod = importh("hgext.%s" % name)
84 85 except ImportError, err:
85 86 ui.debug('could not import hgext.%s (%s): trying %s\n'
86 87 % (name, err, name))
87 88 mod = importh(name)
88 89 _extensions[shortname] = mod
89 90 _order.append(shortname)
91 for fn in _aftercallbacks.get(shortname, []):
92 fn(loaded=True)
90 93 return mod
91 94
92 95 def loadall(ui):
93 96 result = ui.configitems("extensions")
94 97 newindex = len(_order)
95 98 for (name, path) in result:
96 99 if path:
97 100 if path[0] == '!':
98 101 continue
99 102 try:
100 103 load(ui, name, path)
101 104 except KeyboardInterrupt:
102 105 raise
103 106 except Exception, inst:
104 107 if path:
105 108 ui.warn(_("*** failed to import extension %s from %s: %s\n")
106 109 % (name, path, inst))
107 110 else:
108 111 ui.warn(_("*** failed to import extension %s: %s\n")
109 112 % (name, inst))
110 113
111 114 for name in _order[newindex:]:
112 115 uisetup = getattr(_extensions[name], 'uisetup', None)
113 116 if uisetup:
114 117 uisetup(ui)
115 118
116 119 for name in _order[newindex:]:
117 120 extsetup = getattr(_extensions[name], 'extsetup', None)
118 121 if extsetup:
119 122 try:
120 123 extsetup(ui)
121 124 except TypeError:
122 125 if extsetup.func_code.co_argcount != 0:
123 126 raise
124 127 extsetup() # old extsetup with no ui argument
125 128
129 # Call aftercallbacks that were never met.
130 for shortname in _aftercallbacks:
131 if shortname in _extensions:
132 continue
133
134 for fn in _aftercallbacks[shortname]:
135 fn(loaded=False)
136
137 def afterloaded(extension, callback):
138 '''Run the specified function after a named extension is loaded.
139
140 If the named extension is already loaded, the callback will be called
141 immediately.
142
143 If the named extension never loads, the callback will be called after
144 all extensions have been loaded.
145
146 The callback receives the named argument ``loaded``, which is a boolean
147 indicating whether the dependent extension actually loaded.
148 '''
149
150 if extension in _extensions:
151 callback(loaded=False)
152 else:
153 _aftercallbacks.setdefault(extension, []).append(callback)
154
126 155 def wrapcommand(table, command, wrapper):
127 156 '''Wrap the command named `command' in table
128 157
129 158 Replace command in the command table with wrapper. The wrapped command will
130 159 be inserted into the command table specified by the table argument.
131 160
132 161 The wrapper will be called like
133 162
134 163 wrapper(orig, *args, **kwargs)
135 164
136 165 where orig is the original (wrapped) function, and *args, **kwargs
137 166 are the arguments passed to it.
138 167 '''
139 168 assert callable(wrapper)
140 169 aliases, entry = cmdutil.findcmd(command, table)
141 170 for alias, e in table.iteritems():
142 171 if e is entry:
143 172 key = alias
144 173 break
145 174
146 175 origfn = entry[0]
147 176 def wrap(*args, **kwargs):
148 177 return util.checksignature(wrapper)(
149 178 util.checksignature(origfn), *args, **kwargs)
150 179
151 180 wrap.__doc__ = getattr(origfn, '__doc__')
152 181 wrap.__module__ = getattr(origfn, '__module__')
153 182
154 183 newentry = list(entry)
155 184 newentry[0] = wrap
156 185 table[key] = tuple(newentry)
157 186 return entry
158 187
159 188 def wrapfunction(container, funcname, wrapper):
160 189 '''Wrap the function named funcname in container
161 190
162 191 Replace the funcname member in the given container with the specified
163 192 wrapper. The container is typically a module, class, or instance.
164 193
165 194 The wrapper will be called like
166 195
167 196 wrapper(orig, *args, **kwargs)
168 197
169 198 where orig is the original (wrapped) function, and *args, **kwargs
170 199 are the arguments passed to it.
171 200
172 201 Wrapping methods of the repository object is not recommended since
173 202 it conflicts with extensions that extend the repository by
174 203 subclassing. All extensions that need to extend methods of
175 204 localrepository should use this subclassing trick: namely,
176 205 reposetup() should look like
177 206
178 207 def reposetup(ui, repo):
179 208 class myrepo(repo.__class__):
180 209 def whatever(self, *args, **kwargs):
181 210 [...extension stuff...]
182 211 super(myrepo, self).whatever(*args, **kwargs)
183 212 [...extension stuff...]
184 213
185 214 repo.__class__ = myrepo
186 215
187 216 In general, combining wrapfunction() with subclassing does not
188 217 work. Since you cannot control what other extensions are loaded by
189 218 your end users, you should play nicely with others by using the
190 219 subclass trick.
191 220 '''
192 221 assert callable(wrapper)
193 222 def wrap(*args, **kwargs):
194 223 return wrapper(origfn, *args, **kwargs)
195 224
196 225 origfn = getattr(container, funcname)
197 226 assert callable(origfn)
198 227 setattr(container, funcname, wrap)
199 228 return origfn
200 229
201 230 def _disabledpaths(strip_init=False):
202 231 '''find paths of disabled extensions. returns a dict of {name: path}
203 232 removes /__init__.py from packages if strip_init is True'''
204 233 import hgext
205 234 extpath = os.path.dirname(os.path.abspath(hgext.__file__))
206 235 try: # might not be a filesystem path
207 236 files = os.listdir(extpath)
208 237 except OSError:
209 238 return {}
210 239
211 240 exts = {}
212 241 for e in files:
213 242 if e.endswith('.py'):
214 243 name = e.rsplit('.', 1)[0]
215 244 path = os.path.join(extpath, e)
216 245 else:
217 246 name = e
218 247 path = os.path.join(extpath, e, '__init__.py')
219 248 if not os.path.exists(path):
220 249 continue
221 250 if strip_init:
222 251 path = os.path.dirname(path)
223 252 if name in exts or name in _order or name == '__init__':
224 253 continue
225 254 exts[name] = path
226 255 return exts
227 256
228 257 def _moduledoc(file):
229 258 '''return the top-level python documentation for the given file
230 259
231 260 Loosely inspired by pydoc.source_synopsis(), but rewritten to
232 261 handle triple quotes and to return the whole text instead of just
233 262 the synopsis'''
234 263 result = []
235 264
236 265 line = file.readline()
237 266 while line[:1] == '#' or not line.strip():
238 267 line = file.readline()
239 268 if not line:
240 269 break
241 270
242 271 start = line[:3]
243 272 if start == '"""' or start == "'''":
244 273 line = line[3:]
245 274 while line:
246 275 if line.rstrip().endswith(start):
247 276 line = line.split(start)[0]
248 277 if line:
249 278 result.append(line)
250 279 break
251 280 elif not line:
252 281 return None # unmatched delimiter
253 282 result.append(line)
254 283 line = file.readline()
255 284 else:
256 285 return None
257 286
258 287 return ''.join(result)
259 288
260 289 def _disabledhelp(path):
261 290 '''retrieve help synopsis of a disabled extension (without importing)'''
262 291 try:
263 292 file = open(path)
264 293 except IOError:
265 294 return
266 295 else:
267 296 doc = _moduledoc(file)
268 297 file.close()
269 298
270 299 if doc: # extracting localized synopsis
271 300 return gettext(doc).splitlines()[0]
272 301 else:
273 302 return _('(no help text available)')
274 303
275 304 def disabled():
276 305 '''find disabled extensions from hgext. returns a dict of {name: desc}'''
277 306 try:
278 307 from hgext import __index__
279 308 return dict((name, gettext(desc))
280 309 for name, desc in __index__.docs.iteritems()
281 310 if name not in _order)
282 311 except (ImportError, AttributeError):
283 312 pass
284 313
285 314 paths = _disabledpaths()
286 315 if not paths:
287 316 return {}
288 317
289 318 exts = {}
290 319 for name, path in paths.iteritems():
291 320 doc = _disabledhelp(path)
292 321 if doc:
293 322 exts[name] = doc
294 323
295 324 return exts
296 325
297 326 def disabledext(name):
298 327 '''find a specific disabled extension from hgext. returns desc'''
299 328 try:
300 329 from hgext import __index__
301 330 if name in _order: # enabled
302 331 return
303 332 else:
304 333 return gettext(__index__.docs.get(name))
305 334 except (ImportError, AttributeError):
306 335 pass
307 336
308 337 paths = _disabledpaths()
309 338 if name in paths:
310 339 return _disabledhelp(paths[name])
311 340
312 341 def disabledcmd(ui, cmd, strict=False):
313 342 '''import disabled extensions until cmd is found.
314 343 returns (cmdname, extname, module)'''
315 344
316 345 paths = _disabledpaths(strip_init=True)
317 346 if not paths:
318 347 raise error.UnknownCommand(cmd)
319 348
320 349 def findcmd(cmd, name, path):
321 350 try:
322 351 mod = loadpath(path, 'hgext.%s' % name)
323 352 except Exception:
324 353 return
325 354 try:
326 355 aliases, entry = cmdutil.findcmd(cmd,
327 356 getattr(mod, 'cmdtable', {}), strict)
328 357 except (error.AmbiguousCommand, error.UnknownCommand):
329 358 return
330 359 except Exception:
331 360 ui.warn(_('warning: error finding commands in %s\n') % path)
332 361 ui.traceback()
333 362 return
334 363 for c in aliases:
335 364 if c.startswith(cmd):
336 365 cmd = c
337 366 break
338 367 else:
339 368 cmd = aliases[0]
340 369 return (cmd, name, mod)
341 370
342 371 ext = None
343 372 # first, search for an extension with the same name as the command
344 373 path = paths.pop(cmd, None)
345 374 if path:
346 375 ext = findcmd(cmd, cmd, path)
347 376 if not ext:
348 377 # otherwise, interrogate each extension until there's a match
349 378 for name, path in paths.iteritems():
350 379 ext = findcmd(cmd, name, path)
351 380 if ext:
352 381 break
353 382 if ext and 'DEPRECATED' not in ext.__doc__:
354 383 return ext
355 384
356 385 raise error.UnknownCommand(cmd)
357 386
358 387 def enabled(shortname=True):
359 388 '''return a dict of {name: desc} of extensions'''
360 389 exts = {}
361 390 for ename, ext in extensions():
362 391 doc = (gettext(ext.__doc__) or _('(no help text available)'))
363 392 if shortname:
364 393 ename = ename.split('.')[-1]
365 394 exts[ename] = doc.splitlines()[0].strip()
366 395
367 396 return exts
368 397
369 398 def moduleversion(module):
370 399 '''return version information from given module as a string'''
371 400 if (util.safehasattr(module, 'getversion')
372 401 and callable(module.getversion)):
373 402 version = module.getversion()
374 403 elif util.safehasattr(module, '__version__'):
375 404 version = module.__version__
376 405 else:
377 406 version = ''
378 407 if isinstance(version, (list, tuple)):
379 408 version = '.'.join(str(o) for o in version)
380 409 return version
General Comments 0
You need to be logged in to leave comments. Login now