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