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