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