##// END OF EJS Templates
wrapfunction: deprecates calling `wrappedfunction` with bytes...
marmoute -
r51693:94506fc1 default
parent child Browse files
Show More
@@ -1,987 +1,991 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 if not isinstance(funcname, str):
630 msg = b"pass wrappedfunction target name as `str`, not `bytes`"
631 util.nouideprecwarn(msg, b"6.6", stacklevel=2)
632 funcname = pycompat.sysstr(funcname)
629 self._container = container
633 self._container = container
630 self._funcname = funcname
634 self._funcname = funcname
631 self._wrapper = wrapper
635 self._wrapper = wrapper
632
636
633 def __enter__(self):
637 def __enter__(self):
634 wrapfunction(self._container, self._funcname, self._wrapper)
638 wrapfunction(self._container, self._funcname, self._wrapper)
635
639
636 def __exit__(self, exctype, excvalue, traceback):
640 def __exit__(self, exctype, excvalue, traceback):
637 unwrapfunction(self._container, self._funcname, self._wrapper)
641 unwrapfunction(self._container, self._funcname, self._wrapper)
638
642
639
643
640 def wrapfunction(container, funcname, wrapper):
644 def wrapfunction(container, funcname, wrapper):
641 """Wrap the function named funcname in container
645 """Wrap the function named funcname in container
642
646
643 Replace the funcname member in the given container with the specified
647 Replace the funcname member in the given container with the specified
644 wrapper. The container is typically a module, class, or instance.
648 wrapper. The container is typically a module, class, or instance.
645
649
646 The wrapper will be called like
650 The wrapper will be called like
647
651
648 wrapper(orig, *args, **kwargs)
652 wrapper(orig, *args, **kwargs)
649
653
650 where orig is the original (wrapped) function, and *args, **kwargs
654 where orig is the original (wrapped) function, and *args, **kwargs
651 are the arguments passed to it.
655 are the arguments passed to it.
652
656
653 Wrapping methods of the repository object is not recommended since
657 Wrapping methods of the repository object is not recommended since
654 it conflicts with extensions that extend the repository by
658 it conflicts with extensions that extend the repository by
655 subclassing. All extensions that need to extend methods of
659 subclassing. All extensions that need to extend methods of
656 localrepository should use this subclassing trick: namely,
660 localrepository should use this subclassing trick: namely,
657 reposetup() should look like
661 reposetup() should look like
658
662
659 def reposetup(ui, repo):
663 def reposetup(ui, repo):
660 class myrepo(repo.__class__):
664 class myrepo(repo.__class__):
661 def whatever(self, *args, **kwargs):
665 def whatever(self, *args, **kwargs):
662 [...extension stuff...]
666 [...extension stuff...]
663 super(myrepo, self).whatever(*args, **kwargs)
667 super(myrepo, self).whatever(*args, **kwargs)
664 [...extension stuff...]
668 [...extension stuff...]
665
669
666 repo.__class__ = myrepo
670 repo.__class__ = myrepo
667
671
668 In general, combining wrapfunction() with subclassing does not
672 In general, combining wrapfunction() with subclassing does not
669 work. Since you cannot control what other extensions are loaded by
673 work. Since you cannot control what other extensions are loaded by
670 your end users, you should play nicely with others by using the
674 your end users, you should play nicely with others by using the
671 subclass trick.
675 subclass trick.
672 """
676 """
673 assert callable(wrapper)
677 assert callable(wrapper)
674
678
675 origfn = getattr(container, funcname)
679 origfn = getattr(container, funcname)
676 assert callable(origfn)
680 assert callable(origfn)
677 if inspect.ismodule(container):
681 if inspect.ismodule(container):
678 # origfn is not an instance or class method. "partial" can be used.
682 # origfn is not an instance or class method. "partial" can be used.
679 # "partial" won't insert a frame in traceback.
683 # "partial" won't insert a frame in traceback.
680 wrap = functools.partial(wrapper, origfn)
684 wrap = functools.partial(wrapper, origfn)
681 else:
685 else:
682 # "partial" cannot be safely used. Emulate its effect by using "bind".
686 # "partial" cannot be safely used. Emulate its effect by using "bind".
683 # The downside is one more frame in traceback.
687 # The downside is one more frame in traceback.
684 wrap = bind(wrapper, origfn)
688 wrap = bind(wrapper, origfn)
685 _updatewrapper(wrap, origfn, wrapper)
689 _updatewrapper(wrap, origfn, wrapper)
686 setattr(container, funcname, wrap)
690 setattr(container, funcname, wrap)
687 return origfn
691 return origfn
688
692
689
693
690 def unwrapfunction(container, funcname, wrapper=None):
694 def unwrapfunction(container, funcname, wrapper=None):
691 """undo wrapfunction
695 """undo wrapfunction
692
696
693 If wrappers is None, undo the last wrap. Otherwise removes the wrapper
697 If wrappers is None, undo the last wrap. Otherwise removes the wrapper
694 from the chain of wrappers.
698 from the chain of wrappers.
695
699
696 Return the removed wrapper.
700 Return the removed wrapper.
697 Raise IndexError if wrapper is None and nothing to unwrap; ValueError if
701 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.
702 wrapper is not None but is not found in the wrapper chain.
699 """
703 """
700 chain = getwrapperchain(container, funcname)
704 chain = getwrapperchain(container, funcname)
701 origfn = chain.pop()
705 origfn = chain.pop()
702 if wrapper is None:
706 if wrapper is None:
703 wrapper = chain[0]
707 wrapper = chain[0]
704 chain.remove(wrapper)
708 chain.remove(wrapper)
705 setattr(container, funcname, origfn)
709 setattr(container, funcname, origfn)
706 for w in reversed(chain):
710 for w in reversed(chain):
707 wrapfunction(container, funcname, w)
711 wrapfunction(container, funcname, w)
708 return wrapper
712 return wrapper
709
713
710
714
711 def getwrapperchain(container, funcname):
715 def getwrapperchain(container, funcname):
712 """get a chain of wrappers of a function
716 """get a chain of wrappers of a function
713
717
714 Return a list of functions: [newest wrapper, ..., oldest wrapper, origfunc]
718 Return a list of functions: [newest wrapper, ..., oldest wrapper, origfunc]
715
719
716 The wrapper functions are the ones passed to wrapfunction, whose first
720 The wrapper functions are the ones passed to wrapfunction, whose first
717 argument is origfunc.
721 argument is origfunc.
718 """
722 """
719 result = []
723 result = []
720 fn = getattr(container, funcname)
724 fn = getattr(container, funcname)
721 while fn:
725 while fn:
722 assert callable(fn)
726 assert callable(fn)
723 result.append(getattr(fn, '_unboundwrapper', fn))
727 result.append(getattr(fn, '_unboundwrapper', fn))
724 fn = getattr(fn, '_origfunc', None)
728 fn = getattr(fn, '_origfunc', None)
725 return result
729 return result
726
730
727
731
728 def _disabledpaths():
732 def _disabledpaths():
729 '''find paths of disabled extensions. returns a dict of {name: path}'''
733 '''find paths of disabled extensions. returns a dict of {name: path}'''
730 import hgext
734 import hgext
731
735
732 exts = {}
736 exts = {}
733
737
734 # The hgext might not have a __file__ attribute (e.g. in PyOxidizer) and
738 # 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.
739 # it might not be on a filesystem even if it does.
736 if util.safehasattr(hgext, '__file__'):
740 if util.safehasattr(hgext, '__file__'):
737 extpath = os.path.dirname(
741 extpath = os.path.dirname(
738 util.abspath(pycompat.fsencode(hgext.__file__))
742 util.abspath(pycompat.fsencode(hgext.__file__))
739 )
743 )
740 try:
744 try:
741 files = os.listdir(extpath)
745 files = os.listdir(extpath)
742 except OSError:
746 except OSError:
743 pass
747 pass
744 else:
748 else:
745 for e in files:
749 for e in files:
746 if e.endswith(b'.py'):
750 if e.endswith(b'.py'):
747 name = e.rsplit(b'.', 1)[0]
751 name = e.rsplit(b'.', 1)[0]
748 path = os.path.join(extpath, e)
752 path = os.path.join(extpath, e)
749 else:
753 else:
750 name = e
754 name = e
751 path = os.path.join(extpath, e, b'__init__.py')
755 path = os.path.join(extpath, e, b'__init__.py')
752 if not os.path.exists(path):
756 if not os.path.exists(path):
753 continue
757 continue
754 if name in exts or name in _order or name == b'__init__':
758 if name in exts or name in _order or name == b'__init__':
755 continue
759 continue
756 exts[name] = path
760 exts[name] = path
757
761
758 for name, path in _disabledextensions.items():
762 for name, path in _disabledextensions.items():
759 # If no path was provided for a disabled extension (e.g. "color=!"),
763 # 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.
764 # don't replace the path we already found by the scan above.
761 if path:
765 if path:
762 exts[name] = path
766 exts[name] = path
763 return exts
767 return exts
764
768
765
769
766 def _moduledoc(file):
770 def _moduledoc(file):
767 """return the top-level python documentation for the given file
771 """return the top-level python documentation for the given file
768
772
769 Loosely inspired by pydoc.source_synopsis(), but rewritten to
773 Loosely inspired by pydoc.source_synopsis(), but rewritten to
770 handle triple quotes and to return the whole text instead of just
774 handle triple quotes and to return the whole text instead of just
771 the synopsis"""
775 the synopsis"""
772 result = []
776 result = []
773
777
774 line = file.readline()
778 line = file.readline()
775 while line[:1] == b'#' or not line.strip():
779 while line[:1] == b'#' or not line.strip():
776 line = file.readline()
780 line = file.readline()
777 if not line:
781 if not line:
778 break
782 break
779
783
780 start = line[:3]
784 start = line[:3]
781 if start == b'"""' or start == b"'''":
785 if start == b'"""' or start == b"'''":
782 line = line[3:]
786 line = line[3:]
783 while line:
787 while line:
784 if line.rstrip().endswith(start):
788 if line.rstrip().endswith(start):
785 line = line.split(start)[0]
789 line = line.split(start)[0]
786 if line:
790 if line:
787 result.append(line)
791 result.append(line)
788 break
792 break
789 elif not line:
793 elif not line:
790 return None # unmatched delimiter
794 return None # unmatched delimiter
791 result.append(line)
795 result.append(line)
792 line = file.readline()
796 line = file.readline()
793 else:
797 else:
794 return None
798 return None
795
799
796 return b''.join(result)
800 return b''.join(result)
797
801
798
802
799 def _disabledhelp(path):
803 def _disabledhelp(path):
800 '''retrieve help synopsis of a disabled extension (without importing)'''
804 '''retrieve help synopsis of a disabled extension (without importing)'''
801 try:
805 try:
802 with open(path, b'rb') as src:
806 with open(path, b'rb') as src:
803 doc = _moduledoc(src)
807 doc = _moduledoc(src)
804 except IOError:
808 except IOError:
805 return
809 return
806
810
807 if doc: # extracting localized synopsis
811 if doc: # extracting localized synopsis
808 return gettext(doc)
812 return gettext(doc)
809 else:
813 else:
810 return _(b'(no help text available)')
814 return _(b'(no help text available)')
811
815
812
816
813 def disabled():
817 def disabled():
814 '''find disabled extensions from hgext. returns a dict of {name: desc}'''
818 '''find disabled extensions from hgext. returns a dict of {name: desc}'''
815 try:
819 try:
816 from hgext import __index__ # pytype: disable=import-error
820 from hgext import __index__ # pytype: disable=import-error
817
821
818 return {
822 return {
819 name: gettext(desc)
823 name: gettext(desc)
820 for name, desc in __index__.docs.items()
824 for name, desc in __index__.docs.items()
821 if name not in _order
825 if name not in _order
822 }
826 }
823 except (ImportError, AttributeError):
827 except (ImportError, AttributeError):
824 pass
828 pass
825
829
826 paths = _disabledpaths()
830 paths = _disabledpaths()
827 if not paths:
831 if not paths:
828 return {}
832 return {}
829
833
830 exts = {}
834 exts = {}
831 for name, path in paths.items():
835 for name, path in paths.items():
832 doc = _disabledhelp(path)
836 doc = _disabledhelp(path)
833 if doc and name != b'__index__':
837 if doc and name != b'__index__':
834 exts[name] = stringutil.firstline(doc)
838 exts[name] = stringutil.firstline(doc)
835
839
836 return exts
840 return exts
837
841
838
842
839 def disabled_help(name):
843 def disabled_help(name):
840 """Obtain the full help text for a disabled extension, or None."""
844 """Obtain the full help text for a disabled extension, or None."""
841 paths = _disabledpaths()
845 paths = _disabledpaths()
842 if name in paths:
846 if name in paths:
843 return _disabledhelp(paths[name])
847 return _disabledhelp(paths[name])
844 else:
848 else:
845 try:
849 try:
846 import hgext
850 import hgext
847 from hgext import __index__ # pytype: disable=import-error
851 from hgext import __index__ # pytype: disable=import-error
848
852
849 # The extensions are filesystem based, so either an error occurred
853 # The extensions are filesystem based, so either an error occurred
850 # or all are enabled.
854 # or all are enabled.
851 if util.safehasattr(hgext, '__file__'):
855 if util.safehasattr(hgext, '__file__'):
852 return
856 return
853
857
854 if name in _order: # enabled
858 if name in _order: # enabled
855 return
859 return
856 else:
860 else:
857 return gettext(__index__.docs.get(name))
861 return gettext(__index__.docs.get(name))
858 except (ImportError, AttributeError):
862 except (ImportError, AttributeError):
859 pass
863 pass
860
864
861
865
862 def _walkcommand(node):
866 def _walkcommand(node):
863 """Scan @command() decorators in the tree starting at node"""
867 """Scan @command() decorators in the tree starting at node"""
864 todo = collections.deque([node])
868 todo = collections.deque([node])
865 while todo:
869 while todo:
866 node = todo.popleft()
870 node = todo.popleft()
867 if not isinstance(node, ast.FunctionDef):
871 if not isinstance(node, ast.FunctionDef):
868 todo.extend(ast.iter_child_nodes(node))
872 todo.extend(ast.iter_child_nodes(node))
869 continue
873 continue
870 for d in node.decorator_list:
874 for d in node.decorator_list:
871 if not isinstance(d, ast.Call):
875 if not isinstance(d, ast.Call):
872 continue
876 continue
873 if not isinstance(d.func, ast.Name):
877 if not isinstance(d.func, ast.Name):
874 continue
878 continue
875 if d.func.id != 'command':
879 if d.func.id != 'command':
876 continue
880 continue
877 yield d
881 yield d
878
882
879
883
880 def _disabledcmdtable(path):
884 def _disabledcmdtable(path):
881 """Construct a dummy command table without loading the extension module
885 """Construct a dummy command table without loading the extension module
882
886
883 This may raise IOError or SyntaxError.
887 This may raise IOError or SyntaxError.
884 """
888 """
885 with open(path, b'rb') as src:
889 with open(path, b'rb') as src:
886 root = ast.parse(src.read(), path)
890 root = ast.parse(src.read(), path)
887 cmdtable = {}
891 cmdtable = {}
888 for node in _walkcommand(root):
892 for node in _walkcommand(root):
889 if not node.args:
893 if not node.args:
890 continue
894 continue
891 a = node.args[0]
895 a = node.args[0]
892 if isinstance(a, ast.Str):
896 if isinstance(a, ast.Str):
893 name = pycompat.sysbytes(a.s)
897 name = pycompat.sysbytes(a.s)
894 elif isinstance(a, ast.Bytes):
898 elif isinstance(a, ast.Bytes):
895 name = a.s
899 name = a.s
896 else:
900 else:
897 continue
901 continue
898 cmdtable[name] = (None, [], b'')
902 cmdtable[name] = (None, [], b'')
899 return cmdtable
903 return cmdtable
900
904
901
905
902 def _finddisabledcmd(ui, cmd, name, path, strict):
906 def _finddisabledcmd(ui, cmd, name, path, strict):
903 try:
907 try:
904 cmdtable = _disabledcmdtable(path)
908 cmdtable = _disabledcmdtable(path)
905 except (IOError, SyntaxError):
909 except (IOError, SyntaxError):
906 return
910 return
907 try:
911 try:
908 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
912 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
909 except (error.AmbiguousCommand, error.UnknownCommand):
913 except (error.AmbiguousCommand, error.UnknownCommand):
910 return
914 return
911 for c in aliases:
915 for c in aliases:
912 if c.startswith(cmd):
916 if c.startswith(cmd):
913 cmd = c
917 cmd = c
914 break
918 break
915 else:
919 else:
916 cmd = aliases[0]
920 cmd = aliases[0]
917 doc = _disabledhelp(path)
921 doc = _disabledhelp(path)
918 return (cmd, name, doc)
922 return (cmd, name, doc)
919
923
920
924
921 def disabledcmd(ui, cmd, strict=False):
925 def disabledcmd(ui, cmd, strict=False):
922 """find cmd from disabled extensions without importing.
926 """find cmd from disabled extensions without importing.
923 returns (cmdname, extname, doc)"""
927 returns (cmdname, extname, doc)"""
924
928
925 paths = _disabledpaths()
929 paths = _disabledpaths()
926 if not paths:
930 if not paths:
927 raise error.UnknownCommand(cmd)
931 raise error.UnknownCommand(cmd)
928
932
929 ext = None
933 ext = None
930 # first, search for an extension with the same name as the command
934 # first, search for an extension with the same name as the command
931 path = paths.pop(cmd, None)
935 path = paths.pop(cmd, None)
932 if path:
936 if path:
933 ext = _finddisabledcmd(ui, cmd, cmd, path, strict=strict)
937 ext = _finddisabledcmd(ui, cmd, cmd, path, strict=strict)
934 if not ext:
938 if not ext:
935 # otherwise, interrogate each extension until there's a match
939 # otherwise, interrogate each extension until there's a match
936 for name, path in paths.items():
940 for name, path in paths.items():
937 ext = _finddisabledcmd(ui, cmd, name, path, strict=strict)
941 ext = _finddisabledcmd(ui, cmd, name, path, strict=strict)
938 if ext:
942 if ext:
939 break
943 break
940 if ext:
944 if ext:
941 return ext
945 return ext
942
946
943 raise error.UnknownCommand(cmd)
947 raise error.UnknownCommand(cmd)
944
948
945
949
946 def enabled(shortname=True):
950 def enabled(shortname=True):
947 '''return a dict of {name: desc} of extensions'''
951 '''return a dict of {name: desc} of extensions'''
948 exts = {}
952 exts = {}
949 for ename, ext in extensions():
953 for ename, ext in extensions():
950 doc = gettext(ext.__doc__) or _(b'(no help text available)')
954 doc = gettext(ext.__doc__) or _(b'(no help text available)')
951 assert doc is not None # help pytype
955 assert doc is not None # help pytype
952 if shortname:
956 if shortname:
953 ename = ename.split(b'.')[-1]
957 ename = ename.split(b'.')[-1]
954 exts[ename] = stringutil.firstline(doc).strip()
958 exts[ename] = stringutil.firstline(doc).strip()
955
959
956 return exts
960 return exts
957
961
958
962
959 def notloaded():
963 def notloaded():
960 '''return short names of extensions that failed to load'''
964 '''return short names of extensions that failed to load'''
961 return [name for name, mod in _extensions.items() if mod is None]
965 return [name for name, mod in _extensions.items() if mod is None]
962
966
963
967
964 def moduleversion(module):
968 def moduleversion(module):
965 '''return version information from given module as a string'''
969 '''return version information from given module as a string'''
966 if util.safehasattr(module, b'getversion') and callable(module.getversion):
970 if util.safehasattr(module, b'getversion') and callable(module.getversion):
967 try:
971 try:
968 version = module.getversion()
972 version = module.getversion()
969 except Exception:
973 except Exception:
970 version = b'unknown'
974 version = b'unknown'
971
975
972 elif util.safehasattr(module, b'__version__'):
976 elif util.safehasattr(module, b'__version__'):
973 version = module.__version__
977 version = module.__version__
974 else:
978 else:
975 version = b''
979 version = b''
976 if isinstance(version, (list, tuple)):
980 if isinstance(version, (list, tuple)):
977 version = b'.'.join(pycompat.bytestr(o) for o in version)
981 version = b'.'.join(pycompat.bytestr(o) for o in version)
978 else:
982 else:
979 # version data should be bytes, but not all extensions are ported
983 # version data should be bytes, but not all extensions are ported
980 # to py3.
984 # to py3.
981 version = stringutil.forcebytestr(version)
985 version = stringutil.forcebytestr(version)
982 return version
986 return version
983
987
984
988
985 def ismoduleinternal(module):
989 def ismoduleinternal(module):
986 exttestedwith = getattr(module, 'testedwith', None)
990 exttestedwith = getattr(module, 'testedwith', None)
987 return exttestedwith == b"ships-with-hg-core"
991 return exttestedwith == b"ships-with-hg-core"
General Comments 0
You need to be logged in to leave comments. Login now