##// 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
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():
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 import hgext
545 import hgext
546 extpath = os.path.dirname(
546 extpath = os.path.dirname(
547 os.path.abspath(pycompat.fsencode(hgext.__file__)))
547 os.path.abspath(pycompat.fsencode(hgext.__file__)))
548 try: # might not be a filesystem path
548 try: # might not be a filesystem path
549 files = os.listdir(extpath)
549 files = os.listdir(extpath)
550 except OSError:
550 except OSError:
551 return {}
551 return {}
552
552
553 exts = {}
553 exts = {}
554 for e in files:
554 for e in files:
555 if e.endswith('.py'):
555 if e.endswith('.py'):
556 name = e.rsplit('.', 1)[0]
556 name = e.rsplit('.', 1)[0]
557 path = os.path.join(extpath, e)
557 path = os.path.join(extpath, e)
558 else:
558 else:
559 name = e
559 name = e
560 path = os.path.join(extpath, e, '__init__.py')
560 path = os.path.join(extpath, e, '__init__.py')
561 if not os.path.exists(path):
561 if not os.path.exists(path):
562 continue
562 continue
563 if name in exts or name in _order or name == '__init__':
563 if name in exts or name in _order or name == '__init__':
564 continue
564 continue
565 exts[name] = path
565 exts[name] = path
566 for name, path in _disabledextensions.iteritems():
566 for name, path in _disabledextensions.iteritems():
567 # 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=!"),
568 # 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.
569 if path:
569 if path:
570 exts[name] = path
570 exts[name] = path
571 return exts
571 return exts
572
572
573 def _moduledoc(file):
573 def _moduledoc(file):
574 '''return the top-level python documentation for the given file
574 '''return the top-level python documentation for the given file
575
575
576 Loosely inspired by pydoc.source_synopsis(), but rewritten to
576 Loosely inspired by pydoc.source_synopsis(), but rewritten to
577 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
578 the synopsis'''
578 the synopsis'''
579 result = []
579 result = []
580
580
581 line = file.readline()
581 line = file.readline()
582 while line[:1] == '#' or not line.strip():
582 while line[:1] == '#' or not line.strip():
583 line = file.readline()
583 line = file.readline()
584 if not line:
584 if not line:
585 break
585 break
586
586
587 start = line[:3]
587 start = line[:3]
588 if start == '"""' or start == "'''":
588 if start == '"""' or start == "'''":
589 line = line[3:]
589 line = line[3:]
590 while line:
590 while line:
591 if line.rstrip().endswith(start):
591 if line.rstrip().endswith(start):
592 line = line.split(start)[0]
592 line = line.split(start)[0]
593 if line:
593 if line:
594 result.append(line)
594 result.append(line)
595 break
595 break
596 elif not line:
596 elif not line:
597 return None # unmatched delimiter
597 return None # unmatched delimiter
598 result.append(line)
598 result.append(line)
599 line = file.readline()
599 line = file.readline()
600 else:
600 else:
601 return None
601 return None
602
602
603 return ''.join(result)
603 return ''.join(result)
604
604
605 def _disabledhelp(path):
605 def _disabledhelp(path):
606 '''retrieve help synopsis of a disabled extension (without importing)'''
606 '''retrieve help synopsis of a disabled extension (without importing)'''
607 try:
607 try:
608 file = open(path, 'rb')
608 with open(path, 'rb') as src:
609 doc = _moduledoc(src)
609 except IOError:
610 except IOError:
610 return
611 return
611 else:
612 doc = _moduledoc(file)
613 file.close()
614
612
615 if doc: # extracting localized synopsis
613 if doc: # extracting localized synopsis
616 return gettext(doc)
614 return gettext(doc)
617 else:
615 else:
618 return _('(no help text available)')
616 return _('(no help text available)')
619
617
620 def disabled():
618 def disabled():
621 '''find disabled extensions from hgext. returns a dict of {name: desc}'''
619 '''find disabled extensions from hgext. returns a dict of {name: desc}'''
622 try:
620 try:
623 from hgext import __index__
621 from hgext import __index__
624 return dict((name, gettext(desc))
622 return dict((name, gettext(desc))
625 for name, desc in __index__.docs.iteritems()
623 for name, desc in __index__.docs.iteritems()
626 if name not in _order)
624 if name not in _order)
627 except (ImportError, AttributeError):
625 except (ImportError, AttributeError):
628 pass
626 pass
629
627
630 paths = _disabledpaths()
628 paths = _disabledpaths()
631 if not paths:
629 if not paths:
632 return {}
630 return {}
633
631
634 exts = {}
632 exts = {}
635 for name, path in paths.iteritems():
633 for name, path in paths.iteritems():
636 doc = _disabledhelp(path)
634 doc = _disabledhelp(path)
637 if doc:
635 if doc:
638 exts[name] = doc.splitlines()[0]
636 exts[name] = doc.splitlines()[0]
639
637
640 return exts
638 return exts
641
639
642 def disabledext(name):
640 def disabledext(name):
643 '''find a specific disabled extension from hgext. returns desc'''
641 '''find a specific disabled extension from hgext. returns desc'''
644 try:
642 try:
645 from hgext import __index__
643 from hgext import __index__
646 if name in _order: # enabled
644 if name in _order: # enabled
647 return
645 return
648 else:
646 else:
649 return gettext(__index__.docs.get(name))
647 return gettext(__index__.docs.get(name))
650 except (ImportError, AttributeError):
648 except (ImportError, AttributeError):
651 pass
649 pass
652
650
653 paths = _disabledpaths()
651 paths = _disabledpaths()
654 if name in paths:
652 if name in paths:
655 return _disabledhelp(paths[name])
653 return _disabledhelp(paths[name])
656
654
657 def _walkcommand(node):
655 def _walkcommand(node):
658 """Scan @command() decorators in the tree starting at node"""
656 """Scan @command() decorators in the tree starting at node"""
659 todo = collections.deque([node])
657 todo = collections.deque([node])
660 while todo:
658 while todo:
661 node = todo.popleft()
659 node = todo.popleft()
662 if not isinstance(node, ast.FunctionDef):
660 if not isinstance(node, ast.FunctionDef):
663 todo.extend(ast.iter_child_nodes(node))
661 todo.extend(ast.iter_child_nodes(node))
664 continue
662 continue
665 for d in node.decorator_list:
663 for d in node.decorator_list:
666 if not isinstance(d, ast.Call):
664 if not isinstance(d, ast.Call):
667 continue
665 continue
668 if not isinstance(d.func, ast.Name):
666 if not isinstance(d.func, ast.Name):
669 continue
667 continue
670 if d.func.id != r'command':
668 if d.func.id != r'command':
671 continue
669 continue
672 yield d
670 yield d
673
671
674 def _disabledcmdtable(path):
672 def _disabledcmdtable(path):
675 """Construct a dummy command table without loading the extension module
673 """Construct a dummy command table without loading the extension module
676
674
677 This may raise IOError or SyntaxError.
675 This may raise IOError or SyntaxError.
678 """
676 """
679 with open(path, 'rb') as src:
677 with open(path, 'rb') as src:
680 root = ast.parse(src.read(), path)
678 root = ast.parse(src.read(), path)
681 cmdtable = {}
679 cmdtable = {}
682 for node in _walkcommand(root):
680 for node in _walkcommand(root):
683 if not node.args:
681 if not node.args:
684 continue
682 continue
685 a = node.args[0]
683 a = node.args[0]
686 if isinstance(a, ast.Str):
684 if isinstance(a, ast.Str):
687 name = pycompat.sysbytes(a.s)
685 name = pycompat.sysbytes(a.s)
688 elif pycompat.ispy3 and isinstance(a, ast.Bytes):
686 elif pycompat.ispy3 and isinstance(a, ast.Bytes):
689 name = a.s
687 name = a.s
690 else:
688 else:
691 continue
689 continue
692 cmdtable[name] = (None, [], b'')
690 cmdtable[name] = (None, [], b'')
693 return cmdtable
691 return cmdtable
694
692
695 def _finddisabledcmd(ui, cmd, name, path, strict):
693 def _finddisabledcmd(ui, cmd, name, path, strict):
696 try:
694 try:
697 cmdtable = _disabledcmdtable(path)
695 cmdtable = _disabledcmdtable(path)
698 except (IOError, SyntaxError):
696 except (IOError, SyntaxError):
699 return
697 return
700 try:
698 try:
701 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
699 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
702 except (error.AmbiguousCommand, error.UnknownCommand):
700 except (error.AmbiguousCommand, error.UnknownCommand):
703 return
701 return
704 for c in aliases:
702 for c in aliases:
705 if c.startswith(cmd):
703 if c.startswith(cmd):
706 cmd = c
704 cmd = c
707 break
705 break
708 else:
706 else:
709 cmd = aliases[0]
707 cmd = aliases[0]
710 doc = _disabledhelp(path)
708 doc = _disabledhelp(path)
711 return (cmd, name, doc)
709 return (cmd, name, doc)
712
710
713 def disabledcmd(ui, cmd, strict=False):
711 def disabledcmd(ui, cmd, strict=False):
714 '''find cmd from disabled extensions without importing.
712 '''find cmd from disabled extensions without importing.
715 returns (cmdname, extname, doc)'''
713 returns (cmdname, extname, doc)'''
716
714
717 paths = _disabledpaths()
715 paths = _disabledpaths()
718 if not paths:
716 if not paths:
719 raise error.UnknownCommand(cmd)
717 raise error.UnknownCommand(cmd)
720
718
721 ext = None
719 ext = None
722 # first, search for an extension with the same name as the command
720 # first, search for an extension with the same name as the command
723 path = paths.pop(cmd, None)
721 path = paths.pop(cmd, None)
724 if path:
722 if path:
725 ext = _finddisabledcmd(ui, cmd, cmd, path, strict=strict)
723 ext = _finddisabledcmd(ui, cmd, cmd, path, strict=strict)
726 if not ext:
724 if not ext:
727 # otherwise, interrogate each extension until there's a match
725 # otherwise, interrogate each extension until there's a match
728 for name, path in paths.iteritems():
726 for name, path in paths.iteritems():
729 ext = _finddisabledcmd(ui, cmd, name, path, strict=strict)
727 ext = _finddisabledcmd(ui, cmd, name, path, strict=strict)
730 if ext:
728 if ext:
731 break
729 break
732 if ext:
730 if ext:
733 return ext
731 return ext
734
732
735 raise error.UnknownCommand(cmd)
733 raise error.UnknownCommand(cmd)
736
734
737 def enabled(shortname=True):
735 def enabled(shortname=True):
738 '''return a dict of {name: desc} of extensions'''
736 '''return a dict of {name: desc} of extensions'''
739 exts = {}
737 exts = {}
740 for ename, ext in extensions():
738 for ename, ext in extensions():
741 doc = (gettext(ext.__doc__) or _('(no help text available)'))
739 doc = (gettext(ext.__doc__) or _('(no help text available)'))
742 if shortname:
740 if shortname:
743 ename = ename.split('.')[-1]
741 ename = ename.split('.')[-1]
744 exts[ename] = doc.splitlines()[0].strip()
742 exts[ename] = doc.splitlines()[0].strip()
745
743
746 return exts
744 return exts
747
745
748 def notloaded():
746 def notloaded():
749 '''return short names of extensions that failed to load'''
747 '''return short names of extensions that failed to load'''
750 return [name for name, mod in _extensions.iteritems() if mod is None]
748 return [name for name, mod in _extensions.iteritems() if mod is None]
751
749
752 def moduleversion(module):
750 def moduleversion(module):
753 '''return version information from given module as a string'''
751 '''return version information from given module as a string'''
754 if (util.safehasattr(module, 'getversion')
752 if (util.safehasattr(module, 'getversion')
755 and callable(module.getversion)):
753 and callable(module.getversion)):
756 version = module.getversion()
754 version = module.getversion()
757 elif util.safehasattr(module, '__version__'):
755 elif util.safehasattr(module, '__version__'):
758 version = module.__version__
756 version = module.__version__
759 else:
757 else:
760 version = ''
758 version = ''
761 if isinstance(version, (list, tuple)):
759 if isinstance(version, (list, tuple)):
762 version = '.'.join(pycompat.bytestr(o) for o in version)
760 version = '.'.join(pycompat.bytestr(o) for o in version)
763 return version
761 return version
764
762
765 def ismoduleinternal(module):
763 def ismoduleinternal(module):
766 exttestedwith = getattr(module, 'testedwith', None)
764 exttestedwith = getattr(module, 'testedwith', None)
767 return exttestedwith == "ships-with-hg-core"
765 return exttestedwith == "ships-with-hg-core"
General Comments 0
You need to be logged in to leave comments. Login now