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