extensions.py
578 lines
| 18.0 KiB
| text/x-python
|
PythonLexer
/ mercurial / extensions.py
|
r4544 | # extensions.py - extension handling for mercurial | ||
# | ||||
|
r4635 | # Copyright 2005-2007 Matt Mackall <mpm@selenic.com> | ||
|
r4544 | # | ||
|
r8225 | # This software may be used and distributed according to the terms of the | ||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r4544 | |||
|
r25946 | from __future__ import absolute_import | ||
import imp | ||||
|
r31263 | import inspect | ||
|
r25946 | import os | ||
from .i18n import ( | ||||
_, | ||||
gettext, | ||||
) | ||||
from . import ( | ||||
cmdutil, | ||||
|
r31303 | encoding, | ||
|
r25946 | error, | ||
|
r30570 | pycompat, | ||
|
r25946 | util, | ||
) | ||||
|
r4544 | |||
_extensions = {} | ||||
|
r29895 | _disabledextensions = {} | ||
|
r24065 | _aftercallbacks = {} | ||
|
r5192 | _order = [] | ||
|
r32291 | _builtin = {'hbisect', 'bookmarks', 'parentrevspec', 'progress', 'interhg', | ||
'inotify', 'hgcia'} | ||||
|
r5192 | |||
|
r19777 | def extensions(ui=None): | ||
if ui: | ||||
def enabled(name): | ||||
for format in ['%s', 'hgext.%s']: | ||||
conf = ui.config('extensions', format % name) | ||||
if conf is not None and not conf.startswith('!'): | ||||
return True | ||||
else: | ||||
enabled = lambda name: True | ||||
|
r5192 | for name in _order: | ||
module = _extensions[name] | ||||
|
r19777 | if module and enabled(name): | ||
|
r5192 | yield name, module | ||
|
r4544 | |||
def find(name): | ||||
'''return module with given extension name''' | ||||
|
r14415 | mod = None | ||
|
r4544 | try: | ||
|
r27637 | mod = _extensions[name] | ||
|
r4544 | except KeyError: | ||
for k, v in _extensions.iteritems(): | ||||
|
r4560 | if k.endswith('.' + name) or k.endswith('/' + name): | ||
|
r14415 | mod = v | ||
break | ||||
if not mod: | ||||
|
r4544 | raise KeyError(name) | ||
|
r14415 | return mod | ||
|
r4544 | |||
|
r7916 | def loadpath(path, module_name): | ||
module_name = module_name.replace('.', '_') | ||||
|
r20645 | path = util.normpath(util.expandpath(path)) | ||
|
r30575 | module_name = pycompat.fsdecode(module_name) | ||
path = pycompat.fsdecode(path) | ||||
|
r7916 | if os.path.isdir(path): | ||
# module/__init__.py style | ||||
|
r20645 | d, f = os.path.split(path) | ||
|
r7916 | fd, fpath, desc = imp.find_module(f, [d]) | ||
return imp.load_module(module_name, fd, fpath, desc) | ||||
else: | ||||
|
r17217 | try: | ||
return imp.load_source(module_name, path) | ||||
|
r25660 | except IOError as exc: | ||
|
r17217 | if not exc.filename: | ||
exc.filename = path # python does not fill this | ||||
raise | ||||
|
r7916 | |||
|
r28505 | def _importh(name): | ||
"""import and return the <name> module""" | ||||
|
r30570 | mod = __import__(pycompat.sysstr(name)) | ||
|
r28505 | components = name.split('.') | ||
for comp in components[1:]: | ||||
mod = getattr(mod, comp) | ||||
return mod | ||||
|
r30058 | def _importext(name, path=None, reportfunc=None): | ||
if path: | ||||
# the module will be loaded in sys.modules | ||||
# choose an unique name so that it doesn't | ||||
# conflicts with other modules | ||||
mod = loadpath(path, 'hgext.%s' % name) | ||||
else: | ||||
try: | ||||
mod = _importh("hgext.%s" % name) | ||||
except ImportError as err: | ||||
if reportfunc: | ||||
reportfunc(err, "hgext.%s" % name, "hgext3rd.%s" % name) | ||||
try: | ||||
mod = _importh("hgext3rd.%s" % name) | ||||
except ImportError as err: | ||||
if reportfunc: | ||||
reportfunc(err, "hgext3rd.%s" % name, name) | ||||
mod = _importh(name) | ||||
return mod | ||||
|
r31303 | def _forbytes(inst): | ||
"""Portably format an import error into a form suitable for | ||||
%-formatting into bytestrings.""" | ||||
|
r31448 | return encoding.strtolocal(str(inst)) | ||
|
r31303 | |||
|
r28506 | def _reportimporterror(ui, err, failed, next): | ||
|
r30028 | # note: this ui.debug happens before --debug is processed, | ||
# Use --config ui.debug=1 to see them. | ||||
|
r28506 | ui.debug('could not import %s (%s): trying %s\n' | ||
|
r31303 | % (failed, _forbytes(err), next)) | ||
|
r28506 | if ui.debugflag: | ||
ui.traceback() | ||||
|
r32342 | # attributes set by registrar.command | ||
_cmdfuncattrs = ('norepo', 'optionalrepo', 'inferrepo') | ||||
|
r32343 | def _validatecmdtable(ui, cmdtable): | ||
|
r32342 | """Check if extension commands have required attributes""" | ||
for c, e in cmdtable.iteritems(): | ||||
f = e[0] | ||||
|
r32343 | if getattr(f, '_deprecatedregistrar', False): | ||
ui.deprecwarn("cmdutil.command is deprecated, use " | ||||
"registrar.command to register '%s'" % c, '4.6') | ||||
|
r32342 | missing = [a for a in _cmdfuncattrs if not util.safehasattr(f, a)] | ||
if not missing: | ||||
continue | ||||
raise error.ProgrammingError( | ||||
'missing attributes: %s' % ', '.join(missing), | ||||
hint="use @command decorator to register '%s'" % c) | ||||
|
r4544 | def load(ui, name, path): | ||
|
r7011 | if name.startswith('hgext.') or name.startswith('hgext/'): | ||
|
r5031 | shortname = name[6:] | ||
else: | ||||
shortname = name | ||||
|
r27111 | if shortname in _builtin: | ||
|
r13349 | return None | ||
|
r5031 | if shortname in _extensions: | ||
|
r12779 | return _extensions[shortname] | ||
|
r5087 | _extensions[shortname] = None | ||
|
r30058 | mod = _importext(name, path, bind(_reportimporterror, ui)) | ||
|
r27142 | |||
# Before we do anything with the extension, check against minimum stated | ||||
# compatibility. This gives extension authors a mechanism to have their | ||||
# extensions short circuit when loaded with a known incompatible version | ||||
# of Mercurial. | ||||
minver = getattr(mod, 'minimumhgversion', None) | ||||
if minver and util.versiontuple(minver, 2) > util.versiontuple(n=2): | ||||
ui.warn(_('(third party extension %s requires version %s or newer ' | ||||
'of Mercurial; disabling)\n') % (shortname, minver)) | ||||
return | ||||
|
r32343 | _validatecmdtable(ui, getattr(mod, 'cmdtable', {})) | ||
|
r27142 | |||
|
r5031 | _extensions[shortname] = mod | ||
|
r5192 | _order.append(shortname) | ||
|
r24065 | for fn in _aftercallbacks.get(shortname, []): | ||
fn(loaded=True) | ||||
|
r12779 | return mod | ||
|
r4544 | |||
|
r29461 | def _runuisetup(name, ui): | ||
uisetup = getattr(_extensions[name], 'uisetup', None) | ||||
if uisetup: | ||||
uisetup(ui) | ||||
def _runextsetup(name, ui): | ||||
extsetup = getattr(_extensions[name], 'extsetup', None) | ||||
if extsetup: | ||||
try: | ||||
extsetup(ui) | ||||
except TypeError: | ||||
|
r31263 | if inspect.getargspec(extsetup).args: | ||
|
r29461 | raise | ||
extsetup() # old extsetup with no ui argument | ||||
|
r32416 | def loadall(ui, whitelist=None): | ||
|
r4617 | result = ui.configitems("extensions") | ||
|
r32417 | if whitelist is not None: | ||
|
r32416 | result = [(k, v) for (k, v) in result if k in whitelist] | ||
|
r9410 | newindex = len(_order) | ||
|
r7876 | for (name, path) in result: | ||
|
r4617 | if path: | ||
|
r31264 | if path[0:1] == '!': | ||
|
r29895 | _disabledextensions[name] = path[1:] | ||
|
r5469 | continue | ||
|
r4544 | try: | ||
load(ui, name, path) | ||||
|
r25660 | except Exception as inst: | ||
|
r32341 | msg = _forbytes(inst) | ||
|
r6204 | if path: | ||
ui.warn(_("*** failed to import extension %s from %s: %s\n") | ||||
|
r32341 | % (name, path, msg)) | ||
|
r6204 | else: | ||
ui.warn(_("*** failed to import extension %s: %s\n") | ||||
|
r32341 | % (name, msg)) | ||
if isinstance(inst, error.Hint) and inst.hint: | ||||
ui.warn(_("*** (%s)\n") % inst.hint) | ||||
|
r25364 | ui.traceback() | ||
|
r4544 | |||
|
r9410 | for name in _order[newindex:]: | ||
|
r29461 | _runuisetup(name, ui) | ||
|
r9410 | |||
|
r9660 | for name in _order[newindex:]: | ||
|
r29461 | _runextsetup(name, ui) | ||
|
r9660 | |||
|
r24065 | # Call aftercallbacks that were never met. | ||
for shortname in _aftercallbacks: | ||||
if shortname in _extensions: | ||||
continue | ||||
for fn in _aftercallbacks[shortname]: | ||||
fn(loaded=False) | ||||
|
r24950 | # loadall() is called multiple times and lingering _aftercallbacks | ||
# entries could result in double execution. See issue4646. | ||||
_aftercallbacks.clear() | ||||
|
r24065 | def afterloaded(extension, callback): | ||
'''Run the specified function after a named extension is loaded. | ||||
If the named extension is already loaded, the callback will be called | ||||
immediately. | ||||
If the named extension never loads, the callback will be called after | ||||
all extensions have been loaded. | ||||
The callback receives the named argument ``loaded``, which is a boolean | ||||
indicating whether the dependent extension actually loaded. | ||||
''' | ||||
if extension in _extensions: | ||||
|
r24145 | callback(loaded=True) | ||
|
r24065 | else: | ||
_aftercallbacks.setdefault(extension, []).append(callback) | ||||
|
r24734 | def bind(func, *args): | ||
'''Partial function application | ||||
Returns a new function that is the partial application of args and kwargs | ||||
to func. For example, | ||||
f(1, 2, bar=3) === bind(f, 1)(2, bar=3)''' | ||||
assert callable(func) | ||||
def closure(*a, **kw): | ||||
return func(*(args + a), **kw) | ||||
return closure | ||||
|
r29763 | def _updatewrapper(wrap, origfn, unboundwrapper): | ||
'''Copy and add some useful attributes to wrapper''' | ||||
|
r28310 | wrap.__module__ = getattr(origfn, '__module__') | ||
wrap.__doc__ = getattr(origfn, '__doc__') | ||||
|
r28312 | wrap.__dict__.update(getattr(origfn, '__dict__', {})) | ||
|
r29763 | wrap._origfunc = origfn | ||
wrap._unboundwrapper = unboundwrapper | ||||
|
r28310 | |||
|
r24124 | def wrapcommand(table, command, wrapper, synopsis=None, docstring=None): | ||
|
r11519 | '''Wrap the command named `command' in table | ||
Replace command in the command table with wrapper. The wrapped command will | ||||
be inserted into the command table specified by the table argument. | ||||
The wrapper will be called like | ||||
wrapper(orig, *args, **kwargs) | ||||
where orig is the original (wrapped) function, and *args, **kwargs | ||||
are the arguments passed to it. | ||||
|
r24124 | |||
Optionally append to the command synopsis and docstring, used for help. | ||||
For example, if your extension wraps the ``bookmarks`` command to add the | ||||
flags ``--remote`` and ``--all`` you might call this function like so: | ||||
synopsis = ' [-a] [--remote]' | ||||
docstring = """ | ||||
The ``remotenames`` extension adds the ``--remote`` and ``--all`` (``-a``) | ||||
flags to the bookmarks command. Either flag will show the remote bookmarks | ||||
|
r26781 | known to the repository; ``--remote`` will also suppress the output of the | ||
|
r24124 | local bookmarks. | ||
""" | ||||
extensions.wrapcommand(commands.table, 'bookmarks', exbookmarks, | ||||
synopsis, docstring) | ||||
|
r11519 | ''' | ||
|
r21795 | assert callable(wrapper) | ||
|
r7215 | aliases, entry = cmdutil.findcmd(command, table) | ||
for alias, e in table.iteritems(): | ||||
if e is entry: | ||||
key = alias | ||||
break | ||||
origfn = entry[0] | ||||
|
r24734 | wrap = bind(util.checksignature(wrapper), util.checksignature(origfn)) | ||
|
r29763 | _updatewrapper(wrap, origfn, wrapper) | ||
|
r24124 | if docstring is not None: | ||
|
r28310 | wrap.__doc__ += docstring | ||
|
r24124 | |||
|
r7215 | newentry = list(entry) | ||
newentry[0] = wrap | ||||
|
r24124 | if synopsis is not None: | ||
newentry[2] += synopsis | ||||
|
r7215 | table[key] = tuple(newentry) | ||
return entry | ||||
def wrapfunction(container, funcname, wrapper): | ||||
|
r11402 | '''Wrap the function named funcname in container | ||
|
r11520 | Replace the funcname member in the given container with the specified | ||
wrapper. The container is typically a module, class, or instance. | ||||
|
r11402 | |||
The wrapper will be called like | ||||
wrapper(orig, *args, **kwargs) | ||||
where orig is the original (wrapped) function, and *args, **kwargs | ||||
are the arguments passed to it. | ||||
Wrapping methods of the repository object is not recommended since | ||||
it conflicts with extensions that extend the repository by | ||||
subclassing. All extensions that need to extend methods of | ||||
localrepository should use this subclassing trick: namely, | ||||
reposetup() should look like | ||||
def reposetup(ui, repo): | ||||
class myrepo(repo.__class__): | ||||
def whatever(self, *args, **kwargs): | ||||
[...extension stuff...] | ||||
super(myrepo, self).whatever(*args, **kwargs) | ||||
[...extension stuff...] | ||||
repo.__class__ = myrepo | ||||
In general, combining wrapfunction() with subclassing does not | ||||
work. Since you cannot control what other extensions are loaded by | ||||
your end users, you should play nicely with others by using the | ||||
subclass trick. | ||||
''' | ||||
|
r21795 | assert callable(wrapper) | ||
|
r7215 | |||
origfn = getattr(container, funcname) | ||||
|
r21795 | assert callable(origfn) | ||
|
r28311 | wrap = bind(wrapper, origfn) | ||
|
r29763 | _updatewrapper(wrap, origfn, wrapper) | ||
|
r28311 | setattr(container, funcname, wrap) | ||
|
r7215 | return origfn | ||
|
r8871 | |||
|
r29765 | def unwrapfunction(container, funcname, wrapper=None): | ||
'''undo wrapfunction | ||||
If wrappers is None, undo the last wrap. Otherwise removes the wrapper | ||||
from the chain of wrappers. | ||||
Return the removed wrapper. | ||||
Raise IndexError if wrapper is None and nothing to unwrap; ValueError if | ||||
wrapper is not None but is not found in the wrapper chain. | ||||
''' | ||||
chain = getwrapperchain(container, funcname) | ||||
origfn = chain.pop() | ||||
if wrapper is None: | ||||
wrapper = chain[0] | ||||
chain.remove(wrapper) | ||||
setattr(container, funcname, origfn) | ||||
for w in reversed(chain): | ||||
wrapfunction(container, funcname, w) | ||||
return wrapper | ||||
|
r29764 | def getwrapperchain(container, funcname): | ||
'''get a chain of wrappers of a function | ||||
Return a list of functions: [newest wrapper, ..., oldest wrapper, origfunc] | ||||
The wrapper functions are the ones passed to wrapfunction, whose first | ||||
argument is origfunc. | ||||
''' | ||||
result = [] | ||||
fn = getattr(container, funcname) | ||||
while fn: | ||||
assert callable(fn) | ||||
result.append(getattr(fn, '_unboundwrapper', fn)) | ||||
fn = getattr(fn, '_origfunc', None) | ||||
return result | ||||
|
r10364 | def _disabledpaths(strip_init=False): | ||
'''find paths of disabled extensions. returns a dict of {name: path} | ||||
removes /__init__.py from packages if strip_init is True''' | ||||
|
r8872 | import hgext | ||
|
r31074 | extpath = os.path.dirname( | ||
os.path.abspath(pycompat.fsencode(hgext.__file__))) | ||||
|
r8964 | try: # might not be a filesystem path | ||
files = os.listdir(extpath) | ||||
except OSError: | ||||
|
r10363 | return {} | ||
|
r8964 | |||
|
r8871 | exts = {} | ||
|
r8964 | for e in files: | ||
|
r8872 | if e.endswith('.py'): | ||
name = e.rsplit('.', 1)[0] | ||||
path = os.path.join(extpath, e) | ||||
else: | ||||
name = e | ||||
path = os.path.join(extpath, e, '__init__.py') | ||||
|
r8877 | if not os.path.exists(path): | ||
continue | ||||
|
r10364 | if strip_init: | ||
path = os.path.dirname(path) | ||||
|
r10363 | if name in exts or name in _order or name == '__init__': | ||
continue | ||||
exts[name] = path | ||||
|
r29895 | exts.update(_disabledextensions) | ||
|
r10363 | return exts | ||
|
r8872 | |||
|
r14317 | def _moduledoc(file): | ||
'''return the top-level python documentation for the given file | ||||
Loosely inspired by pydoc.source_synopsis(), but rewritten to | ||||
handle triple quotes and to return the whole text instead of just | ||||
the synopsis''' | ||||
result = [] | ||||
line = file.readline() | ||||
while line[:1] == '#' or not line.strip(): | ||||
line = file.readline() | ||||
if not line: | ||||
break | ||||
start = line[:3] | ||||
if start == '"""' or start == "'''": | ||||
line = line[3:] | ||||
while line: | ||||
if line.rstrip().endswith(start): | ||||
line = line.split(start)[0] | ||||
if line: | ||||
result.append(line) | ||||
break | ||||
elif not line: | ||||
return None # unmatched delimiter | ||||
result.append(line) | ||||
line = file.readline() | ||||
else: | ||||
return None | ||||
return ''.join(result) | ||||
|
r10363 | def _disabledhelp(path): | ||
'''retrieve help synopsis of a disabled extension (without importing)''' | ||||
try: | ||||
file = open(path) | ||||
except IOError: | ||||
return | ||||
else: | ||||
|
r14318 | doc = _moduledoc(file) | ||
|
r10363 | file.close() | ||
if doc: # extracting localized synopsis | ||||
|
r30306 | return gettext(doc) | ||
|
r10363 | else: | ||
return _('(no help text available)') | ||||
def disabled(): | ||||
|
r14530 | '''find disabled extensions from hgext. returns a dict of {name: desc}''' | ||
|
r14539 | try: | ||
from hgext import __index__ | ||||
return dict((name, gettext(desc)) | ||||
for name, desc in __index__.docs.iteritems() | ||||
if name not in _order) | ||||
|
r21229 | except (ImportError, AttributeError): | ||
|
r14539 | pass | ||
|
r10363 | paths = _disabledpaths() | ||
if not paths: | ||||
|
r16709 | return {} | ||
|
r10363 | |||
exts = {} | ||||
for name, path in paths.iteritems(): | ||||
doc = _disabledhelp(path) | ||||
|
r14316 | if doc: | ||
|
r30306 | exts[name] = doc.splitlines()[0] | ||
|
r8871 | |||
|
r14316 | return exts | ||
|
r8871 | |||
|
r10364 | def disabledext(name): | ||
'''find a specific disabled extension from hgext. returns desc''' | ||||
|
r14539 | try: | ||
from hgext import __index__ | ||||
if name in _order: # enabled | ||||
return | ||||
else: | ||||
return gettext(__index__.docs.get(name)) | ||||
|
r21229 | except (ImportError, AttributeError): | ||
|
r14539 | pass | ||
|
r10364 | paths = _disabledpaths() | ||
if name in paths: | ||||
return _disabledhelp(paths[name]) | ||||
|
r13191 | def disabledcmd(ui, cmd, strict=False): | ||
|
r10364 | '''import disabled extensions until cmd is found. | ||
|
r16666 | returns (cmdname, extname, module)''' | ||
|
r10364 | |||
paths = _disabledpaths(strip_init=True) | ||||
if not paths: | ||||
raise error.UnknownCommand(cmd) | ||||
def findcmd(cmd, name, path): | ||||
try: | ||||
mod = loadpath(path, 'hgext.%s' % name) | ||||
except Exception: | ||||
return | ||||
try: | ||||
aliases, entry = cmdutil.findcmd(cmd, | ||||
getattr(mod, 'cmdtable', {}), strict) | ||||
except (error.AmbiguousCommand, error.UnknownCommand): | ||||
return | ||||
|
r13191 | except Exception: | ||
ui.warn(_('warning: error finding commands in %s\n') % path) | ||||
ui.traceback() | ||||
return | ||||
|
r10364 | for c in aliases: | ||
if c.startswith(cmd): | ||||
cmd = c | ||||
break | ||||
else: | ||||
cmd = aliases[0] | ||||
return (cmd, name, mod) | ||||
|
r16667 | ext = None | ||
|
r10364 | # first, search for an extension with the same name as the command | ||
path = paths.pop(cmd, None) | ||||
if path: | ||||
ext = findcmd(cmd, cmd, path) | ||||
|
r16667 | if not ext: | ||
# otherwise, interrogate each extension until there's a match | ||||
for name, path in paths.iteritems(): | ||||
ext = findcmd(cmd, name, path) | ||||
if ext: | ||||
break | ||||
if ext and 'DEPRECATED' not in ext.__doc__: | ||||
return ext | ||||
|
r10364 | |||
raise error.UnknownCommand(cmd) | ||||
|
r19769 | def enabled(shortname=True): | ||
|
r14530 | '''return a dict of {name: desc} of extensions''' | ||
|
r8871 | exts = {} | ||
for ename, ext in extensions(): | ||||
doc = (gettext(ext.__doc__) or _('(no help text available)')) | ||||
|
r19769 | if shortname: | ||
ename = ename.split('.')[-1] | ||||
|
r9136 | exts[ename] = doc.splitlines()[0].strip() | ||
|
r8871 | |||
|
r14316 | return exts | ||
|
r21848 | |||
|
r28155 | def notloaded(): | ||
'''return short names of extensions that failed to load''' | ||||
return [name for name, mod in _extensions.iteritems() if mod is None] | ||||
|
r21848 | def moduleversion(module): | ||
'''return version information from given module as a string''' | ||||
if (util.safehasattr(module, 'getversion') | ||||
and callable(module.getversion)): | ||||
version = module.getversion() | ||||
elif util.safehasattr(module, '__version__'): | ||||
version = module.__version__ | ||||
else: | ||||
version = '' | ||||
if isinstance(version, (list, tuple)): | ||||
version = '.'.join(str(o) for o in version) | ||||
return version | ||||
|
r27990 | |||
def ismoduleinternal(module): | ||||
exttestedwith = getattr(module, 'testedwith', None) | ||||
|
r29841 | return exttestedwith == "ships-with-hg-core" | ||