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