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