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