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