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