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