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