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