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