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