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