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