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