##// END OF EJS Templates
extensions: trace the total time of running all extsetup callbacks...
Boris Feld -
r39545:3a86f7eb default
parent child Browse files
Show More
@@ -1,810 +1,812 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 ast
11 11 import collections
12 12 import functools
13 13 import imp
14 14 import inspect
15 15 import os
16 16
17 17 from .i18n import (
18 18 _,
19 19 gettext,
20 20 )
21 21
22 22 from . import (
23 23 cmdutil,
24 24 configitems,
25 25 error,
26 26 pycompat,
27 27 util,
28 28 )
29 29
30 30 from .utils import (
31 31 stringutil,
32 32 )
33 33
34 34 _extensions = {}
35 35 _disabledextensions = {}
36 36 _aftercallbacks = {}
37 37 _order = []
38 38 _builtin = {
39 39 'hbisect',
40 40 'bookmarks',
41 41 'color',
42 42 'parentrevspec',
43 43 'progress',
44 44 'interhg',
45 45 'inotify',
46 46 'hgcia'
47 47 }
48 48
49 49 def extensions(ui=None):
50 50 if ui:
51 51 def enabled(name):
52 52 for format in ['%s', 'hgext.%s']:
53 53 conf = ui.config('extensions', format % name)
54 54 if conf is not None and not conf.startswith('!'):
55 55 return True
56 56 else:
57 57 enabled = lambda name: True
58 58 for name in _order:
59 59 module = _extensions[name]
60 60 if module and enabled(name):
61 61 yield name, module
62 62
63 63 def find(name):
64 64 '''return module with given extension name'''
65 65 mod = None
66 66 try:
67 67 mod = _extensions[name]
68 68 except KeyError:
69 69 for k, v in _extensions.iteritems():
70 70 if k.endswith('.' + name) or k.endswith('/' + name):
71 71 mod = v
72 72 break
73 73 if not mod:
74 74 raise KeyError(name)
75 75 return mod
76 76
77 77 def loadpath(path, module_name):
78 78 module_name = module_name.replace('.', '_')
79 79 path = util.normpath(util.expandpath(path))
80 80 module_name = pycompat.fsdecode(module_name)
81 81 path = pycompat.fsdecode(path)
82 82 if os.path.isdir(path):
83 83 # module/__init__.py style
84 84 d, f = os.path.split(path)
85 85 fd, fpath, desc = imp.find_module(f, [d])
86 86 return imp.load_module(module_name, fd, fpath, desc)
87 87 else:
88 88 try:
89 89 return imp.load_source(module_name, path)
90 90 except IOError as exc:
91 91 if not exc.filename:
92 92 exc.filename = path # python does not fill this
93 93 raise
94 94
95 95 def _importh(name):
96 96 """import and return the <name> module"""
97 97 mod = __import__(pycompat.sysstr(name))
98 98 components = name.split('.')
99 99 for comp in components[1:]:
100 100 mod = getattr(mod, comp)
101 101 return mod
102 102
103 103 def _importext(name, path=None, reportfunc=None):
104 104 if path:
105 105 # the module will be loaded in sys.modules
106 106 # choose an unique name so that it doesn't
107 107 # conflicts with other modules
108 108 mod = loadpath(path, 'hgext.%s' % name)
109 109 else:
110 110 try:
111 111 mod = _importh("hgext.%s" % name)
112 112 except ImportError as err:
113 113 if reportfunc:
114 114 reportfunc(err, "hgext.%s" % name, "hgext3rd.%s" % name)
115 115 try:
116 116 mod = _importh("hgext3rd.%s" % name)
117 117 except ImportError as err:
118 118 if reportfunc:
119 119 reportfunc(err, "hgext3rd.%s" % name, name)
120 120 mod = _importh(name)
121 121 return mod
122 122
123 123 def _reportimporterror(ui, err, failed, next):
124 124 # note: this ui.debug happens before --debug is processed,
125 125 # Use --config ui.debug=1 to see them.
126 126 if ui.configbool('devel', 'debug.extensions'):
127 127 ui.debug('debug.extensions: - could not import %s (%s): trying %s\n'
128 128 % (failed, stringutil.forcebytestr(err), next))
129 129 if ui.debugflag:
130 130 ui.traceback()
131 131
132 132 def _rejectunicode(name, xs):
133 133 if isinstance(xs, (list, set, tuple)):
134 134 for x in xs:
135 135 _rejectunicode(name, x)
136 136 elif isinstance(xs, dict):
137 137 for k, v in xs.items():
138 138 _rejectunicode(name, k)
139 139 _rejectunicode(b'%s.%s' % (name, stringutil.forcebytestr(k)), v)
140 140 elif isinstance(xs, type(u'')):
141 141 raise error.ProgrammingError(b"unicode %r found in %s" % (xs, name),
142 142 hint="use b'' to make it byte string")
143 143
144 144 # attributes set by registrar.command
145 145 _cmdfuncattrs = ('norepo', 'optionalrepo', 'inferrepo')
146 146
147 147 def _validatecmdtable(ui, cmdtable):
148 148 """Check if extension commands have required attributes"""
149 149 for c, e in cmdtable.iteritems():
150 150 f = e[0]
151 151 missing = [a for a in _cmdfuncattrs if not util.safehasattr(f, a)]
152 152 if not missing:
153 153 continue
154 154 raise error.ProgrammingError(
155 155 'missing attributes: %s' % ', '.join(missing),
156 156 hint="use @command decorator to register '%s'" % c)
157 157
158 158 def _validatetables(ui, mod):
159 159 """Sanity check for loadable tables provided by extension module"""
160 160 for t in ['cmdtable', 'colortable', 'configtable']:
161 161 _rejectunicode(t, getattr(mod, t, {}))
162 162 for t in ['filesetpredicate', 'internalmerge', 'revsetpredicate',
163 163 'templatefilter', 'templatefunc', 'templatekeyword']:
164 164 o = getattr(mod, t, None)
165 165 if o:
166 166 _rejectunicode(t, o._table)
167 167 _validatecmdtable(ui, getattr(mod, 'cmdtable', {}))
168 168
169 169 def load(ui, name, path, log=lambda *a: None):
170 170 if name.startswith('hgext.') or name.startswith('hgext/'):
171 171 shortname = name[6:]
172 172 else:
173 173 shortname = name
174 174 if shortname in _builtin:
175 175 return None
176 176 if shortname in _extensions:
177 177 return _extensions[shortname]
178 178 log(' - loading extension: %r\n', shortname)
179 179 _extensions[shortname] = None
180 180 with util.timedcm('load extension %r', shortname) as stats:
181 181 mod = _importext(name, path, bind(_reportimporterror, ui))
182 182 log(' > %r extension loaded in %s\n', shortname, stats)
183 183
184 184 # Before we do anything with the extension, check against minimum stated
185 185 # compatibility. This gives extension authors a mechanism to have their
186 186 # extensions short circuit when loaded with a known incompatible version
187 187 # of Mercurial.
188 188 minver = getattr(mod, 'minimumhgversion', None)
189 189 if minver and util.versiontuple(minver, 2) > util.versiontuple(n=2):
190 190 ui.warn(_('(third party extension %s requires version %s or newer '
191 191 'of Mercurial; disabling)\n') % (shortname, minver))
192 192 return
193 193 log(' - validating extension tables: %r\n', shortname)
194 194 _validatetables(ui, mod)
195 195
196 196 _extensions[shortname] = mod
197 197 _order.append(shortname)
198 198 log(' - invoking registered callbacks: %r\n', shortname)
199 199 with util.timedcm('callbacks extension %r', shortname) as stats:
200 200 for fn in _aftercallbacks.get(shortname, []):
201 201 fn(loaded=True)
202 202 log(' > callbacks completed in %s\n', stats)
203 203 return mod
204 204
205 205 def _runuisetup(name, ui):
206 206 uisetup = getattr(_extensions[name], 'uisetup', None)
207 207 if uisetup:
208 208 try:
209 209 uisetup(ui)
210 210 except Exception as inst:
211 211 ui.traceback(force=True)
212 212 msg = stringutil.forcebytestr(inst)
213 213 ui.warn(_("*** failed to set up extension %s: %s\n") % (name, msg))
214 214 return False
215 215 return True
216 216
217 217 def _runextsetup(name, ui):
218 218 extsetup = getattr(_extensions[name], 'extsetup', None)
219 219 if extsetup:
220 220 try:
221 221 try:
222 222 extsetup(ui)
223 223 except TypeError:
224 224 if pycompat.getargspec(extsetup).args:
225 225 raise
226 226 extsetup() # old extsetup with no ui argument
227 227 except Exception as inst:
228 228 ui.traceback(force=True)
229 229 msg = stringutil.forcebytestr(inst)
230 230 ui.warn(_("*** failed to set up extension %s: %s\n") % (name, msg))
231 231 return False
232 232 return True
233 233
234 234 def loadall(ui, whitelist=None):
235 235 if ui.configbool('devel', 'debug.extensions'):
236 236 log = lambda msg, *values: ui.debug('debug.extensions: ',
237 237 msg % values, label='debug.extensions')
238 238 else:
239 239 log = lambda *a, **kw: None
240 240 result = ui.configitems("extensions")
241 241 if whitelist is not None:
242 242 result = [(k, v) for (k, v) in result if k in whitelist]
243 243 newindex = len(_order)
244 244 log('loading %sextensions\n', 'additional ' if newindex else '')
245 245 log('- processing %d entries\n', len(result))
246 246 with util.timedcm('load all extensions') as stats:
247 247 for (name, path) in result:
248 248 if path:
249 249 if path[0:1] == '!':
250 250 if name not in _disabledextensions:
251 251 log(' - skipping disabled extension: %r\n', name)
252 252 _disabledextensions[name] = path[1:]
253 253 continue
254 254 try:
255 255 load(ui, name, path, log)
256 256 except Exception as inst:
257 257 msg = stringutil.forcebytestr(inst)
258 258 if path:
259 259 ui.warn(_("*** failed to import extension %s from %s: %s\n")
260 260 % (name, path, msg))
261 261 else:
262 262 ui.warn(_("*** failed to import extension %s: %s\n")
263 263 % (name, msg))
264 264 if isinstance(inst, error.Hint) and inst.hint:
265 265 ui.warn(_("*** (%s)\n") % inst.hint)
266 266 ui.traceback()
267 267
268 268 log('> loaded %d extensions, total time %s\n',
269 269 len(_order) - newindex, stats)
270 270 # list of (objname, loadermod, loadername) tuple:
271 271 # - objname is the name of an object in extension module,
272 272 # from which extra information is loaded
273 273 # - loadermod is the module where loader is placed
274 274 # - loadername is the name of the function,
275 275 # which takes (ui, extensionname, extraobj) arguments
276 276 #
277 277 # This one is for the list of item that must be run before running any setup
278 278 earlyextraloaders = [
279 279 ('configtable', configitems, 'loadconfigtable'),
280 280 ]
281 281
282 282 log('- loading configtable attributes\n')
283 283 _loadextra(ui, newindex, earlyextraloaders)
284 284
285 285 broken = set()
286 286 log('- executing uisetup hooks\n')
287 287 with util.timedcm('all uisetup') as alluisetupstats:
288 288 for name in _order[newindex:]:
289 289 log(' - running uisetup for %r\n', name)
290 290 with util.timedcm('uisetup %r', name) as stats:
291 291 if not _runuisetup(name, ui):
292 292 log(' - the %r extension uisetup failed\n', name)
293 293 broken.add(name)
294 294 log(' > uisetup for %r took %s\n', name, stats)
295 295 log('> all uisetup took %s\n', alluisetupstats)
296 296
297 297 log('- executing extsetup hooks\n')
298 for name in _order[newindex:]:
299 if name in broken:
300 continue
301 log(' - running extsetup for %r\n', name)
302 with util.timedcm('extsetup %r', name) as stats:
303 if not _runextsetup(name, ui):
304 log(' - the %r extension extsetup failed\n', name)
305 broken.add(name)
306 log(' > extsetup for %r took %s\n', name, stats)
298 with util.timedcm('all extsetup') as allextetupstats:
299 for name in _order[newindex:]:
300 if name in broken:
301 continue
302 log(' - running extsetup for %r\n', name)
303 with util.timedcm('extsetup %r', name) as stats:
304 if not _runextsetup(name, ui):
305 log(' - the %r extension extsetup failed\n', name)
306 broken.add(name)
307 log(' > extsetup for %r took %s\n', name, stats)
308 log('> all extsetup took %s\n', allextetupstats)
307 309
308 310 for name in broken:
309 311 log(' - disabling broken %r extension\n', name)
310 312 _extensions[name] = None
311 313
312 314 # Call aftercallbacks that were never met.
313 315 log('- executing remaining aftercallbacks\n')
314 316 with util.timedcm('aftercallbacks') as stats:
315 317 for shortname in _aftercallbacks:
316 318 if shortname in _extensions:
317 319 continue
318 320
319 321 for fn in _aftercallbacks[shortname]:
320 322 log(' - extension %r not loaded, notify callbacks\n',
321 323 shortname)
322 324 fn(loaded=False)
323 325 log('> remaining aftercallbacks completed in %s\n', stats)
324 326
325 327 # loadall() is called multiple times and lingering _aftercallbacks
326 328 # entries could result in double execution. See issue4646.
327 329 _aftercallbacks.clear()
328 330
329 331 # delay importing avoids cyclic dependency (especially commands)
330 332 from . import (
331 333 color,
332 334 commands,
333 335 filemerge,
334 336 fileset,
335 337 revset,
336 338 templatefilters,
337 339 templatefuncs,
338 340 templatekw,
339 341 )
340 342
341 343 # list of (objname, loadermod, loadername) tuple:
342 344 # - objname is the name of an object in extension module,
343 345 # from which extra information is loaded
344 346 # - loadermod is the module where loader is placed
345 347 # - loadername is the name of the function,
346 348 # which takes (ui, extensionname, extraobj) arguments
347 349 log('- loading extension registration objects\n')
348 350 extraloaders = [
349 351 ('cmdtable', commands, 'loadcmdtable'),
350 352 ('colortable', color, 'loadcolortable'),
351 353 ('filesetpredicate', fileset, 'loadpredicate'),
352 354 ('internalmerge', filemerge, 'loadinternalmerge'),
353 355 ('revsetpredicate', revset, 'loadpredicate'),
354 356 ('templatefilter', templatefilters, 'loadfilter'),
355 357 ('templatefunc', templatefuncs, 'loadfunction'),
356 358 ('templatekeyword', templatekw, 'loadkeyword'),
357 359 ]
358 360 with util.timedcm('load registration objects') as stats:
359 361 _loadextra(ui, newindex, extraloaders)
360 362 log('> extension registration object loading took %s\n', stats)
361 363 log('extension loading complete\n')
362 364
363 365 def _loadextra(ui, newindex, extraloaders):
364 366 for name in _order[newindex:]:
365 367 module = _extensions[name]
366 368 if not module:
367 369 continue # loading this module failed
368 370
369 371 for objname, loadermod, loadername in extraloaders:
370 372 extraobj = getattr(module, objname, None)
371 373 if extraobj is not None:
372 374 getattr(loadermod, loadername)(ui, name, extraobj)
373 375
374 376 def afterloaded(extension, callback):
375 377 '''Run the specified function after a named extension is loaded.
376 378
377 379 If the named extension is already loaded, the callback will be called
378 380 immediately.
379 381
380 382 If the named extension never loads, the callback will be called after
381 383 all extensions have been loaded.
382 384
383 385 The callback receives the named argument ``loaded``, which is a boolean
384 386 indicating whether the dependent extension actually loaded.
385 387 '''
386 388
387 389 if extension in _extensions:
388 390 # Report loaded as False if the extension is disabled
389 391 loaded = (_extensions[extension] is not None)
390 392 callback(loaded=loaded)
391 393 else:
392 394 _aftercallbacks.setdefault(extension, []).append(callback)
393 395
394 396 def bind(func, *args):
395 397 '''Partial function application
396 398
397 399 Returns a new function that is the partial application of args and kwargs
398 400 to func. For example,
399 401
400 402 f(1, 2, bar=3) === bind(f, 1)(2, bar=3)'''
401 403 assert callable(func)
402 404 def closure(*a, **kw):
403 405 return func(*(args + a), **kw)
404 406 return closure
405 407
406 408 def _updatewrapper(wrap, origfn, unboundwrapper):
407 409 '''Copy and add some useful attributes to wrapper'''
408 410 try:
409 411 wrap.__name__ = origfn.__name__
410 412 except AttributeError:
411 413 pass
412 414 wrap.__module__ = getattr(origfn, '__module__')
413 415 wrap.__doc__ = getattr(origfn, '__doc__')
414 416 wrap.__dict__.update(getattr(origfn, '__dict__', {}))
415 417 wrap._origfunc = origfn
416 418 wrap._unboundwrapper = unboundwrapper
417 419
418 420 def wrapcommand(table, command, wrapper, synopsis=None, docstring=None):
419 421 '''Wrap the command named `command' in table
420 422
421 423 Replace command in the command table with wrapper. The wrapped command will
422 424 be inserted into the command table specified by the table argument.
423 425
424 426 The wrapper will be called like
425 427
426 428 wrapper(orig, *args, **kwargs)
427 429
428 430 where orig is the original (wrapped) function, and *args, **kwargs
429 431 are the arguments passed to it.
430 432
431 433 Optionally append to the command synopsis and docstring, used for help.
432 434 For example, if your extension wraps the ``bookmarks`` command to add the
433 435 flags ``--remote`` and ``--all`` you might call this function like so:
434 436
435 437 synopsis = ' [-a] [--remote]'
436 438 docstring = """
437 439
438 440 The ``remotenames`` extension adds the ``--remote`` and ``--all`` (``-a``)
439 441 flags to the bookmarks command. Either flag will show the remote bookmarks
440 442 known to the repository; ``--remote`` will also suppress the output of the
441 443 local bookmarks.
442 444 """
443 445
444 446 extensions.wrapcommand(commands.table, 'bookmarks', exbookmarks,
445 447 synopsis, docstring)
446 448 '''
447 449 assert callable(wrapper)
448 450 aliases, entry = cmdutil.findcmd(command, table)
449 451 for alias, e in table.iteritems():
450 452 if e is entry:
451 453 key = alias
452 454 break
453 455
454 456 origfn = entry[0]
455 457 wrap = functools.partial(util.checksignature(wrapper),
456 458 util.checksignature(origfn))
457 459 _updatewrapper(wrap, origfn, wrapper)
458 460 if docstring is not None:
459 461 wrap.__doc__ += docstring
460 462
461 463 newentry = list(entry)
462 464 newentry[0] = wrap
463 465 if synopsis is not None:
464 466 newentry[2] += synopsis
465 467 table[key] = tuple(newentry)
466 468 return entry
467 469
468 470 def wrapfilecache(cls, propname, wrapper):
469 471 """Wraps a filecache property.
470 472
471 473 These can't be wrapped using the normal wrapfunction.
472 474 """
473 475 propname = pycompat.sysstr(propname)
474 476 assert callable(wrapper)
475 477 for currcls in cls.__mro__:
476 478 if propname in currcls.__dict__:
477 479 origfn = currcls.__dict__[propname].func
478 480 assert callable(origfn)
479 481 def wrap(*args, **kwargs):
480 482 return wrapper(origfn, *args, **kwargs)
481 483 currcls.__dict__[propname].func = wrap
482 484 break
483 485
484 486 if currcls is object:
485 487 raise AttributeError(r"type '%s' has no property '%s'" % (
486 488 cls, propname))
487 489
488 490 class wrappedfunction(object):
489 491 '''context manager for temporarily wrapping a function'''
490 492
491 493 def __init__(self, container, funcname, wrapper):
492 494 assert callable(wrapper)
493 495 self._container = container
494 496 self._funcname = funcname
495 497 self._wrapper = wrapper
496 498
497 499 def __enter__(self):
498 500 wrapfunction(self._container, self._funcname, self._wrapper)
499 501
500 502 def __exit__(self, exctype, excvalue, traceback):
501 503 unwrapfunction(self._container, self._funcname, self._wrapper)
502 504
503 505 def wrapfunction(container, funcname, wrapper):
504 506 '''Wrap the function named funcname in container
505 507
506 508 Replace the funcname member in the given container with the specified
507 509 wrapper. The container is typically a module, class, or instance.
508 510
509 511 The wrapper will be called like
510 512
511 513 wrapper(orig, *args, **kwargs)
512 514
513 515 where orig is the original (wrapped) function, and *args, **kwargs
514 516 are the arguments passed to it.
515 517
516 518 Wrapping methods of the repository object is not recommended since
517 519 it conflicts with extensions that extend the repository by
518 520 subclassing. All extensions that need to extend methods of
519 521 localrepository should use this subclassing trick: namely,
520 522 reposetup() should look like
521 523
522 524 def reposetup(ui, repo):
523 525 class myrepo(repo.__class__):
524 526 def whatever(self, *args, **kwargs):
525 527 [...extension stuff...]
526 528 super(myrepo, self).whatever(*args, **kwargs)
527 529 [...extension stuff...]
528 530
529 531 repo.__class__ = myrepo
530 532
531 533 In general, combining wrapfunction() with subclassing does not
532 534 work. Since you cannot control what other extensions are loaded by
533 535 your end users, you should play nicely with others by using the
534 536 subclass trick.
535 537 '''
536 538 assert callable(wrapper)
537 539
538 540 origfn = getattr(container, funcname)
539 541 assert callable(origfn)
540 542 if inspect.ismodule(container):
541 543 # origfn is not an instance or class method. "partial" can be used.
542 544 # "partial" won't insert a frame in traceback.
543 545 wrap = functools.partial(wrapper, origfn)
544 546 else:
545 547 # "partial" cannot be safely used. Emulate its effect by using "bind".
546 548 # The downside is one more frame in traceback.
547 549 wrap = bind(wrapper, origfn)
548 550 _updatewrapper(wrap, origfn, wrapper)
549 551 setattr(container, funcname, wrap)
550 552 return origfn
551 553
552 554 def unwrapfunction(container, funcname, wrapper=None):
553 555 '''undo wrapfunction
554 556
555 557 If wrappers is None, undo the last wrap. Otherwise removes the wrapper
556 558 from the chain of wrappers.
557 559
558 560 Return the removed wrapper.
559 561 Raise IndexError if wrapper is None and nothing to unwrap; ValueError if
560 562 wrapper is not None but is not found in the wrapper chain.
561 563 '''
562 564 chain = getwrapperchain(container, funcname)
563 565 origfn = chain.pop()
564 566 if wrapper is None:
565 567 wrapper = chain[0]
566 568 chain.remove(wrapper)
567 569 setattr(container, funcname, origfn)
568 570 for w in reversed(chain):
569 571 wrapfunction(container, funcname, w)
570 572 return wrapper
571 573
572 574 def getwrapperchain(container, funcname):
573 575 '''get a chain of wrappers of a function
574 576
575 577 Return a list of functions: [newest wrapper, ..., oldest wrapper, origfunc]
576 578
577 579 The wrapper functions are the ones passed to wrapfunction, whose first
578 580 argument is origfunc.
579 581 '''
580 582 result = []
581 583 fn = getattr(container, funcname)
582 584 while fn:
583 585 assert callable(fn)
584 586 result.append(getattr(fn, '_unboundwrapper', fn))
585 587 fn = getattr(fn, '_origfunc', None)
586 588 return result
587 589
588 590 def _disabledpaths():
589 591 '''find paths of disabled extensions. returns a dict of {name: path}'''
590 592 import hgext
591 593 extpath = os.path.dirname(
592 594 os.path.abspath(pycompat.fsencode(hgext.__file__)))
593 595 try: # might not be a filesystem path
594 596 files = os.listdir(extpath)
595 597 except OSError:
596 598 return {}
597 599
598 600 exts = {}
599 601 for e in files:
600 602 if e.endswith('.py'):
601 603 name = e.rsplit('.', 1)[0]
602 604 path = os.path.join(extpath, e)
603 605 else:
604 606 name = e
605 607 path = os.path.join(extpath, e, '__init__.py')
606 608 if not os.path.exists(path):
607 609 continue
608 610 if name in exts or name in _order or name == '__init__':
609 611 continue
610 612 exts[name] = path
611 613 for name, path in _disabledextensions.iteritems():
612 614 # If no path was provided for a disabled extension (e.g. "color=!"),
613 615 # don't replace the path we already found by the scan above.
614 616 if path:
615 617 exts[name] = path
616 618 return exts
617 619
618 620 def _moduledoc(file):
619 621 '''return the top-level python documentation for the given file
620 622
621 623 Loosely inspired by pydoc.source_synopsis(), but rewritten to
622 624 handle triple quotes and to return the whole text instead of just
623 625 the synopsis'''
624 626 result = []
625 627
626 628 line = file.readline()
627 629 while line[:1] == '#' or not line.strip():
628 630 line = file.readline()
629 631 if not line:
630 632 break
631 633
632 634 start = line[:3]
633 635 if start == '"""' or start == "'''":
634 636 line = line[3:]
635 637 while line:
636 638 if line.rstrip().endswith(start):
637 639 line = line.split(start)[0]
638 640 if line:
639 641 result.append(line)
640 642 break
641 643 elif not line:
642 644 return None # unmatched delimiter
643 645 result.append(line)
644 646 line = file.readline()
645 647 else:
646 648 return None
647 649
648 650 return ''.join(result)
649 651
650 652 def _disabledhelp(path):
651 653 '''retrieve help synopsis of a disabled extension (without importing)'''
652 654 try:
653 655 with open(path, 'rb') as src:
654 656 doc = _moduledoc(src)
655 657 except IOError:
656 658 return
657 659
658 660 if doc: # extracting localized synopsis
659 661 return gettext(doc)
660 662 else:
661 663 return _('(no help text available)')
662 664
663 665 def disabled():
664 666 '''find disabled extensions from hgext. returns a dict of {name: desc}'''
665 667 try:
666 668 from hgext import __index__
667 669 return dict((name, gettext(desc))
668 670 for name, desc in __index__.docs.iteritems()
669 671 if name not in _order)
670 672 except (ImportError, AttributeError):
671 673 pass
672 674
673 675 paths = _disabledpaths()
674 676 if not paths:
675 677 return {}
676 678
677 679 exts = {}
678 680 for name, path in paths.iteritems():
679 681 doc = _disabledhelp(path)
680 682 if doc:
681 683 exts[name] = doc.splitlines()[0]
682 684
683 685 return exts
684 686
685 687 def disabledext(name):
686 688 '''find a specific disabled extension from hgext. returns desc'''
687 689 try:
688 690 from hgext import __index__
689 691 if name in _order: # enabled
690 692 return
691 693 else:
692 694 return gettext(__index__.docs.get(name))
693 695 except (ImportError, AttributeError):
694 696 pass
695 697
696 698 paths = _disabledpaths()
697 699 if name in paths:
698 700 return _disabledhelp(paths[name])
699 701
700 702 def _walkcommand(node):
701 703 """Scan @command() decorators in the tree starting at node"""
702 704 todo = collections.deque([node])
703 705 while todo:
704 706 node = todo.popleft()
705 707 if not isinstance(node, ast.FunctionDef):
706 708 todo.extend(ast.iter_child_nodes(node))
707 709 continue
708 710 for d in node.decorator_list:
709 711 if not isinstance(d, ast.Call):
710 712 continue
711 713 if not isinstance(d.func, ast.Name):
712 714 continue
713 715 if d.func.id != r'command':
714 716 continue
715 717 yield d
716 718
717 719 def _disabledcmdtable(path):
718 720 """Construct a dummy command table without loading the extension module
719 721
720 722 This may raise IOError or SyntaxError.
721 723 """
722 724 with open(path, 'rb') as src:
723 725 root = ast.parse(src.read(), path)
724 726 cmdtable = {}
725 727 for node in _walkcommand(root):
726 728 if not node.args:
727 729 continue
728 730 a = node.args[0]
729 731 if isinstance(a, ast.Str):
730 732 name = pycompat.sysbytes(a.s)
731 733 elif pycompat.ispy3 and isinstance(a, ast.Bytes):
732 734 name = a.s
733 735 else:
734 736 continue
735 737 cmdtable[name] = (None, [], b'')
736 738 return cmdtable
737 739
738 740 def _finddisabledcmd(ui, cmd, name, path, strict):
739 741 try:
740 742 cmdtable = _disabledcmdtable(path)
741 743 except (IOError, SyntaxError):
742 744 return
743 745 try:
744 746 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
745 747 except (error.AmbiguousCommand, error.UnknownCommand):
746 748 return
747 749 for c in aliases:
748 750 if c.startswith(cmd):
749 751 cmd = c
750 752 break
751 753 else:
752 754 cmd = aliases[0]
753 755 doc = _disabledhelp(path)
754 756 return (cmd, name, doc)
755 757
756 758 def disabledcmd(ui, cmd, strict=False):
757 759 '''find cmd from disabled extensions without importing.
758 760 returns (cmdname, extname, doc)'''
759 761
760 762 paths = _disabledpaths()
761 763 if not paths:
762 764 raise error.UnknownCommand(cmd)
763 765
764 766 ext = None
765 767 # first, search for an extension with the same name as the command
766 768 path = paths.pop(cmd, None)
767 769 if path:
768 770 ext = _finddisabledcmd(ui, cmd, cmd, path, strict=strict)
769 771 if not ext:
770 772 # otherwise, interrogate each extension until there's a match
771 773 for name, path in paths.iteritems():
772 774 ext = _finddisabledcmd(ui, cmd, name, path, strict=strict)
773 775 if ext:
774 776 break
775 777 if ext:
776 778 return ext
777 779
778 780 raise error.UnknownCommand(cmd)
779 781
780 782 def enabled(shortname=True):
781 783 '''return a dict of {name: desc} of extensions'''
782 784 exts = {}
783 785 for ename, ext in extensions():
784 786 doc = (gettext(ext.__doc__) or _('(no help text available)'))
785 787 if shortname:
786 788 ename = ename.split('.')[-1]
787 789 exts[ename] = doc.splitlines()[0].strip()
788 790
789 791 return exts
790 792
791 793 def notloaded():
792 794 '''return short names of extensions that failed to load'''
793 795 return [name for name, mod in _extensions.iteritems() if mod is None]
794 796
795 797 def moduleversion(module):
796 798 '''return version information from given module as a string'''
797 799 if (util.safehasattr(module, 'getversion')
798 800 and callable(module.getversion)):
799 801 version = module.getversion()
800 802 elif util.safehasattr(module, '__version__'):
801 803 version = module.__version__
802 804 else:
803 805 version = ''
804 806 if isinstance(version, (list, tuple)):
805 807 version = '.'.join(pycompat.bytestr(o) for o in version)
806 808 return version
807 809
808 810 def ismoduleinternal(module):
809 811 exttestedwith = getattr(module, 'testedwith', None)
810 812 return exttestedwith == "ships-with-hg-core"
@@ -1,134 +1,135 b''
1 1 ensure that failing ui.atexit handlers report sensibly
2 2
3 3 $ cat > $TESTTMP/bailatexit.py <<EOF
4 4 > from mercurial import util
5 5 > def bail():
6 6 > raise RuntimeError('ui.atexit handler exception')
7 7 >
8 8 > def extsetup(ui):
9 9 > ui.atexit(bail)
10 10 > EOF
11 11 $ hg -q --config extensions.bailatexit=$TESTTMP/bailatexit.py \
12 12 > help help
13 13 hg help [-ecks] [TOPIC]
14 14
15 15 show help for a given topic or a help overview
16 16 error in exit handlers:
17 17 Traceback (most recent call last):
18 18 File "*/mercurial/dispatch.py", line *, in _runexithandlers (glob)
19 19 func(*args, **kwargs)
20 20 File "$TESTTMP/bailatexit.py", line *, in bail (glob)
21 21 raise RuntimeError('ui.atexit handler exception')
22 22 RuntimeError: ui.atexit handler exception
23 23 [255]
24 24
25 25 $ rm $TESTTMP/bailatexit.py
26 26
27 27 another bad extension
28 28
29 29 $ echo 'raise Exception("bit bucket overflow")' > badext.py
30 30 $ abspathexc=`pwd`/badext.py
31 31
32 32 $ cat >baddocext.py <<EOF
33 33 > """
34 34 > baddocext is bad
35 35 > """
36 36 > EOF
37 37 $ abspathdoc=`pwd`/baddocext.py
38 38
39 39 $ cat <<EOF >> $HGRCPATH
40 40 > [extensions]
41 41 > gpg =
42 42 > hgext.gpg =
43 43 > badext = $abspathexc
44 44 > baddocext = $abspathdoc
45 45 > badext2 =
46 46 > EOF
47 47
48 48 $ hg -q help help 2>&1 |grep extension
49 49 *** failed to import extension badext from $TESTTMP/badext.py: bit bucket overflow
50 50 *** failed to import extension badext2: No module named badext2
51 51
52 52 show traceback
53 53
54 54 $ hg -q help help --traceback 2>&1 | egrep ' extension|^Exception|Traceback|ImportError'
55 55 *** failed to import extension badext from $TESTTMP/badext.py: bit bucket overflow
56 56 Traceback (most recent call last):
57 57 Exception: bit bucket overflow
58 58 *** failed to import extension badext2: No module named badext2
59 59 Traceback (most recent call last):
60 60 ImportError: No module named badext2
61 61
62 62 names of extensions failed to load can be accessed via extensions.notloaded()
63 63
64 64 $ cat <<EOF > showbadexts.py
65 65 > from mercurial import commands, extensions, registrar
66 66 > cmdtable = {}
67 67 > command = registrar.command(cmdtable)
68 68 > @command(b'showbadexts', norepo=True)
69 69 > def showbadexts(ui, *pats, **opts):
70 70 > ui.write('BADEXTS: %s\n' % ' '.join(sorted(extensions.notloaded())))
71 71 > EOF
72 72 $ hg --config extensions.badexts=showbadexts.py showbadexts 2>&1 | grep '^BADEXTS'
73 73 BADEXTS: badext badext2
74 74
75 75 #if no-extraextensions
76 76 show traceback for ImportError of hgext.name if devel.debug.extensions is set
77 77
78 78 $ (hg help help --traceback --debug --config devel.debug.extensions=yes 2>&1) \
79 79 > | grep -v '^ ' \
80 80 > | egrep 'extension..[^p]|^Exception|Traceback|ImportError|not import'
81 81 debug.extensions: loading extensions
82 82 debug.extensions: - processing 5 entries
83 83 debug.extensions: - loading extension: 'gpg'
84 84 debug.extensions: > 'gpg' extension loaded in * (glob)
85 85 debug.extensions: - validating extension tables: 'gpg'
86 86 debug.extensions: - invoking registered callbacks: 'gpg'
87 87 debug.extensions: > callbacks completed in * (glob)
88 88 debug.extensions: - loading extension: 'badext'
89 89 *** failed to import extension badext from $TESTTMP/badext.py: bit bucket overflow
90 90 Traceback (most recent call last):
91 91 Exception: bit bucket overflow
92 92 debug.extensions: - loading extension: 'baddocext'
93 93 debug.extensions: > 'baddocext' extension loaded in * (glob)
94 94 debug.extensions: - validating extension tables: 'baddocext'
95 95 debug.extensions: - invoking registered callbacks: 'baddocext'
96 96 debug.extensions: > callbacks completed in * (glob)
97 97 debug.extensions: - loading extension: 'badext2'
98 98 debug.extensions: - could not import hgext.badext2 (No module named badext2): trying hgext3rd.badext2
99 99 Traceback (most recent call last):
100 100 ImportError: No module named *badext2 (glob)
101 101 debug.extensions: - could not import hgext3rd.badext2 (No module named badext2): trying badext2
102 102 Traceback (most recent call last):
103 103 ImportError: No module named *badext2 (glob)
104 104 *** failed to import extension badext2: No module named badext2
105 105 Traceback (most recent call last):
106 106 ImportError: No module named badext2
107 107 debug.extensions: > loaded 2 extensions, total time * (glob)
108 108 debug.extensions: - loading configtable attributes
109 109 debug.extensions: - executing uisetup hooks
110 110 debug.extensions: - running uisetup for 'gpg'
111 111 debug.extensions: > uisetup for 'gpg' took * (glob)
112 112 debug.extensions: - running uisetup for 'baddocext'
113 113 debug.extensions: > uisetup for 'baddocext' took * (glob)
114 114 debug.extensions: > all uisetup took * (glob)
115 115 debug.extensions: - executing extsetup hooks
116 116 debug.extensions: - running extsetup for 'gpg'
117 117 debug.extensions: > extsetup for 'gpg' took * (glob)
118 118 debug.extensions: - running extsetup for 'baddocext'
119 119 debug.extensions: > extsetup for 'baddocext' took * (glob)
120 debug.extensions: > all extsetup took * (glob)
120 121 debug.extensions: - executing remaining aftercallbacks
121 122 debug.extensions: > remaining aftercallbacks completed in * (glob)
122 123 debug.extensions: - loading extension registration objects
123 124 debug.extensions: > extension registration object loading took * (glob)
124 125 debug.extensions: extension loading complete
125 126 #endif
126 127
127 128 confirm that there's no crash when an extension's documentation is bad
128 129
129 130 $ hg help --keyword baddocext
130 131 *** failed to import extension badext from $TESTTMP/badext.py: bit bucket overflow
131 132 *** failed to import extension badext2: No module named badext2
132 133 Topics:
133 134
134 135 extensions Using Additional Features
@@ -1,92 +1,94 b''
1 1 Test basic extension support
2 2
3 3 $ cat > foobar.py <<EOF
4 4 > import os
5 5 > from mercurial import commands, registrar
6 6 > cmdtable = {}
7 7 > command = registrar.command(cmdtable)
8 8 > configtable = {}
9 9 > configitem = registrar.configitem(configtable)
10 10 > configitem(b'tests', b'foo', default=b"Foo")
11 11 > def uisetup(ui):
12 12 > ui.debug(b"uisetup called [debug]\\n")
13 13 > ui.write(b"uisetup called\\n")
14 14 > ui.status(b"uisetup called [status]\\n")
15 15 > ui.flush()
16 16 > def reposetup(ui, repo):
17 17 > ui.write(b"reposetup called for %s\\n" % os.path.basename(repo.root))
18 18 > ui.write(b"ui %s= repo.ui\\n" % (ui == repo.ui and b"=" or b"!"))
19 19 > ui.flush()
20 20 > @command(b'foo', [], b'hg foo')
21 21 > def foo(ui, *args, **kwargs):
22 22 > foo = ui.config(b'tests', b'foo')
23 23 > ui.write(foo)
24 24 > ui.write(b"\\n")
25 25 > @command(b'bar', [], b'hg bar', norepo=True)
26 26 > def bar(ui, *args, **kwargs):
27 27 > ui.write(b"Bar\\n")
28 28 > EOF
29 29 $ abspath=`pwd`/foobar.py
30 30
31 31 $ mkdir barfoo
32 32 $ cp foobar.py barfoo/__init__.py
33 33 $ barfoopath=`pwd`/barfoo
34 34
35 35 $ hg init a
36 36 $ cd a
37 37 $ echo foo > file
38 38 $ hg add file
39 39 $ hg commit -m 'add file'
40 40
41 41 $ echo '[extensions]' >> $HGRCPATH
42 42 $ echo "foobar = $abspath" >> $HGRCPATH
43 43
44 44 Test extension setup timings
45 45
46 46 $ hg foo --traceback --config devel.debug.extensions=yes --debug 2>&1
47 47 debug.extensions: loading extensions
48 48 debug.extensions: - processing 1 entries
49 49 debug.extensions: - loading extension: 'foobar'
50 50 debug.extensions: > 'foobar' extension loaded in * (glob)
51 51 debug.extensions: - validating extension tables: 'foobar'
52 52 debug.extensions: - invoking registered callbacks: 'foobar'
53 53 debug.extensions: > callbacks completed in * (glob)
54 54 debug.extensions: > loaded 1 extensions, total time * (glob)
55 55 debug.extensions: - loading configtable attributes
56 56 debug.extensions: - executing uisetup hooks
57 57 debug.extensions: - running uisetup for 'foobar'
58 58 uisetup called [debug]
59 59 uisetup called
60 60 uisetup called [status]
61 61 debug.extensions: > uisetup for 'foobar' took * (glob)
62 62 debug.extensions: > all uisetup took * (glob)
63 63 debug.extensions: - executing extsetup hooks
64 64 debug.extensions: - running extsetup for 'foobar'
65 65 debug.extensions: > extsetup for 'foobar' took * (glob)
66 debug.extensions: > all extsetup took * (glob)
66 67 debug.extensions: - executing remaining aftercallbacks
67 68 debug.extensions: > remaining aftercallbacks completed in * (glob)
68 69 debug.extensions: - loading extension registration objects
69 70 debug.extensions: > extension registration object loading took * (glob)
70 71 debug.extensions: extension loading complete
71 72 debug.extensions: loading additional extensions
72 73 debug.extensions: - processing 1 entries
73 74 debug.extensions: > loaded 0 extensions, total time * (glob)
74 75 debug.extensions: - loading configtable attributes
75 76 debug.extensions: - executing uisetup hooks
76 77 debug.extensions: > all uisetup took * (glob)
77 78 debug.extensions: - executing extsetup hooks
79 debug.extensions: > all extsetup took * (glob)
78 80 debug.extensions: - executing remaining aftercallbacks
79 81 debug.extensions: > remaining aftercallbacks completed in * (glob)
80 82 debug.extensions: - loading extension registration objects
81 83 debug.extensions: > extension registration object loading took * (glob)
82 84 debug.extensions: extension loading complete
83 85 debug.extensions: - executing reposetup hooks
84 86 debug.extensions: - running reposetup for foobar
85 87 reposetup called for a
86 88 ui == repo.ui
87 89 debug.extensions: > reposetup for 'foobar' took * (glob)
88 90 Foo
89 91
90 92 $ cd ..
91 93
92 94 $ echo 'foobar = !' >> $HGRCPATH
General Comments 0
You need to be logged in to leave comments. Login now