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