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