##// END OF EJS Templates
extensions: make `hg nonexistent` not crash with PyOxidizer...
Martin von Zweigbergk -
r45665:5d09a120 default
parent child Browse files
Show More
@@ -1,936 +1,941 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 and util.versiontuple(minver, 2) > util.versiontuple(n=2):
225 if minver and util.versiontuple(minver, 2) > util.versiontuple(n=2):
226 msg = _(
226 msg = _(
227 b'(third party extension %s requires version %s or newer '
227 b'(third party extension %s requires version %s or newer '
228 b'of Mercurial (current: %s); disabling)\n'
228 b'of Mercurial (current: %s); disabling)\n'
229 )
229 )
230 ui.warn(msg % (shortname, minver, util.version()))
230 ui.warn(msg % (shortname, minver, util.version()))
231 return
231 return
232 ui.log(b'extension', b' - validating extension tables: %s\n', shortname)
232 ui.log(b'extension', b' - validating extension tables: %s\n', shortname)
233 _validatetables(ui, mod)
233 _validatetables(ui, mod)
234
234
235 _extensions[shortname] = mod
235 _extensions[shortname] = mod
236 _order.append(shortname)
236 _order.append(shortname)
237 ui.log(
237 ui.log(
238 b'extension', b' - invoking registered callbacks: %s\n', shortname
238 b'extension', b' - invoking registered callbacks: %s\n', shortname
239 )
239 )
240 with util.timedcm('callbacks extension %s', shortname) as stats:
240 with util.timedcm('callbacks extension %s', shortname) as stats:
241 for fn in _aftercallbacks.get(shortname, []):
241 for fn in _aftercallbacks.get(shortname, []):
242 fn(loaded=True)
242 fn(loaded=True)
243 ui.log(b'extension', b' > callbacks completed in %s\n', stats)
243 ui.log(b'extension', b' > callbacks completed in %s\n', stats)
244 return mod
244 return mod
245
245
246
246
247 def _runuisetup(name, ui):
247 def _runuisetup(name, ui):
248 uisetup = getattr(_extensions[name], 'uisetup', None)
248 uisetup = getattr(_extensions[name], 'uisetup', None)
249 if uisetup:
249 if uisetup:
250 try:
250 try:
251 uisetup(ui)
251 uisetup(ui)
252 except Exception as inst:
252 except Exception as inst:
253 ui.traceback(force=True)
253 ui.traceback(force=True)
254 msg = stringutil.forcebytestr(inst)
254 msg = stringutil.forcebytestr(inst)
255 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))
256 return False
256 return False
257 return True
257 return True
258
258
259
259
260 def _runextsetup(name, ui):
260 def _runextsetup(name, ui):
261 extsetup = getattr(_extensions[name], 'extsetup', None)
261 extsetup = getattr(_extensions[name], 'extsetup', None)
262 if extsetup:
262 if extsetup:
263 try:
263 try:
264 extsetup(ui)
264 extsetup(ui)
265 except Exception as inst:
265 except Exception as inst:
266 ui.traceback(force=True)
266 ui.traceback(force=True)
267 msg = stringutil.forcebytestr(inst)
267 msg = stringutil.forcebytestr(inst)
268 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))
269 return False
269 return False
270 return True
270 return True
271
271
272
272
273 def loadall(ui, whitelist=None):
273 def loadall(ui, whitelist=None):
274 loadingtime = collections.defaultdict(int)
274 loadingtime = collections.defaultdict(int)
275 result = ui.configitems(b"extensions")
275 result = ui.configitems(b"extensions")
276 if whitelist is not None:
276 if whitelist is not None:
277 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]
278 newindex = len(_order)
278 newindex = len(_order)
279 ui.log(
279 ui.log(
280 b'extension',
280 b'extension',
281 b'loading %sextensions\n',
281 b'loading %sextensions\n',
282 b'additional ' if newindex else b'',
282 b'additional ' if newindex else b'',
283 )
283 )
284 ui.log(b'extension', b'- processing %d entries\n', len(result))
284 ui.log(b'extension', b'- processing %d entries\n', len(result))
285 with util.timedcm('load all extensions') as stats:
285 with util.timedcm('load all extensions') as stats:
286 for (name, path) in result:
286 for (name, path) in result:
287 if path:
287 if path:
288 if path[0:1] == b'!':
288 if path[0:1] == b'!':
289 if name not in _disabledextensions:
289 if name not in _disabledextensions:
290 ui.log(
290 ui.log(
291 b'extension',
291 b'extension',
292 b' - skipping disabled extension: %s\n',
292 b' - skipping disabled extension: %s\n',
293 name,
293 name,
294 )
294 )
295 _disabledextensions[name] = path[1:]
295 _disabledextensions[name] = path[1:]
296 continue
296 continue
297 try:
297 try:
298 load(ui, name, path, loadingtime)
298 load(ui, name, path, loadingtime)
299 except Exception as inst:
299 except Exception as inst:
300 msg = stringutil.forcebytestr(inst)
300 msg = stringutil.forcebytestr(inst)
301 if path:
301 if path:
302 ui.warn(
302 ui.warn(
303 _(b"*** failed to import extension %s from %s: %s\n")
303 _(b"*** failed to import extension %s from %s: %s\n")
304 % (name, path, msg)
304 % (name, path, msg)
305 )
305 )
306 else:
306 else:
307 ui.warn(
307 ui.warn(
308 _(b"*** failed to import extension %s: %s\n")
308 _(b"*** failed to import extension %s: %s\n")
309 % (name, msg)
309 % (name, msg)
310 )
310 )
311 if isinstance(inst, error.Hint) and inst.hint:
311 if isinstance(inst, error.Hint) and inst.hint:
312 ui.warn(_(b"*** (%s)\n") % inst.hint)
312 ui.warn(_(b"*** (%s)\n") % inst.hint)
313 ui.traceback()
313 ui.traceback()
314
314
315 ui.log(
315 ui.log(
316 b'extension',
316 b'extension',
317 b'> loaded %d extensions, total time %s\n',
317 b'> loaded %d extensions, total time %s\n',
318 len(_order) - newindex,
318 len(_order) - newindex,
319 stats,
319 stats,
320 )
320 )
321 # list of (objname, loadermod, loadername) tuple:
321 # list of (objname, loadermod, loadername) tuple:
322 # - objname is the name of an object in extension module,
322 # - objname is the name of an object in extension module,
323 # from which extra information is loaded
323 # from which extra information is loaded
324 # - loadermod is the module where loader is placed
324 # - loadermod is the module where loader is placed
325 # - loadername is the name of the function,
325 # - loadername is the name of the function,
326 # which takes (ui, extensionname, extraobj) arguments
326 # which takes (ui, extensionname, extraobj) arguments
327 #
327 #
328 # 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
329 earlyextraloaders = [
329 earlyextraloaders = [
330 (b'configtable', configitems, b'loadconfigtable'),
330 (b'configtable', configitems, b'loadconfigtable'),
331 ]
331 ]
332
332
333 ui.log(b'extension', b'- loading configtable attributes\n')
333 ui.log(b'extension', b'- loading configtable attributes\n')
334 _loadextra(ui, newindex, earlyextraloaders)
334 _loadextra(ui, newindex, earlyextraloaders)
335
335
336 broken = set()
336 broken = set()
337 ui.log(b'extension', b'- executing uisetup hooks\n')
337 ui.log(b'extension', b'- executing uisetup hooks\n')
338 with util.timedcm('all uisetup') as alluisetupstats:
338 with util.timedcm('all uisetup') as alluisetupstats:
339 for name in _order[newindex:]:
339 for name in _order[newindex:]:
340 ui.log(b'extension', b' - running uisetup for %s\n', name)
340 ui.log(b'extension', b' - running uisetup for %s\n', name)
341 with util.timedcm('uisetup %s', name) as stats:
341 with util.timedcm('uisetup %s', name) as stats:
342 if not _runuisetup(name, ui):
342 if not _runuisetup(name, ui):
343 ui.log(
343 ui.log(
344 b'extension',
344 b'extension',
345 b' - the %s extension uisetup failed\n',
345 b' - the %s extension uisetup failed\n',
346 name,
346 name,
347 )
347 )
348 broken.add(name)
348 broken.add(name)
349 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)
350 loadingtime[name] += stats.elapsed
350 loadingtime[name] += stats.elapsed
351 ui.log(b'extension', b'> all uisetup took %s\n', alluisetupstats)
351 ui.log(b'extension', b'> all uisetup took %s\n', alluisetupstats)
352
352
353 ui.log(b'extension', b'- executing extsetup hooks\n')
353 ui.log(b'extension', b'- executing extsetup hooks\n')
354 with util.timedcm('all extsetup') as allextetupstats:
354 with util.timedcm('all extsetup') as allextetupstats:
355 for name in _order[newindex:]:
355 for name in _order[newindex:]:
356 if name in broken:
356 if name in broken:
357 continue
357 continue
358 ui.log(b'extension', b' - running extsetup for %s\n', name)
358 ui.log(b'extension', b' - running extsetup for %s\n', name)
359 with util.timedcm('extsetup %s', name) as stats:
359 with util.timedcm('extsetup %s', name) as stats:
360 if not _runextsetup(name, ui):
360 if not _runextsetup(name, ui):
361 ui.log(
361 ui.log(
362 b'extension',
362 b'extension',
363 b' - the %s extension extsetup failed\n',
363 b' - the %s extension extsetup failed\n',
364 name,
364 name,
365 )
365 )
366 broken.add(name)
366 broken.add(name)
367 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)
368 loadingtime[name] += stats.elapsed
368 loadingtime[name] += stats.elapsed
369 ui.log(b'extension', b'> all extsetup took %s\n', allextetupstats)
369 ui.log(b'extension', b'> all extsetup took %s\n', allextetupstats)
370
370
371 for name in broken:
371 for name in broken:
372 ui.log(b'extension', b' - disabling broken %s extension\n', name)
372 ui.log(b'extension', b' - disabling broken %s extension\n', name)
373 _extensions[name] = None
373 _extensions[name] = None
374
374
375 # Call aftercallbacks that were never met.
375 # Call aftercallbacks that were never met.
376 ui.log(b'extension', b'- executing remaining aftercallbacks\n')
376 ui.log(b'extension', b'- executing remaining aftercallbacks\n')
377 with util.timedcm('aftercallbacks') as stats:
377 with util.timedcm('aftercallbacks') as stats:
378 for shortname in _aftercallbacks:
378 for shortname in _aftercallbacks:
379 if shortname in _extensions:
379 if shortname in _extensions:
380 continue
380 continue
381
381
382 for fn in _aftercallbacks[shortname]:
382 for fn in _aftercallbacks[shortname]:
383 ui.log(
383 ui.log(
384 b'extension',
384 b'extension',
385 b' - extension %s not loaded, notify callbacks\n',
385 b' - extension %s not loaded, notify callbacks\n',
386 shortname,
386 shortname,
387 )
387 )
388 fn(loaded=False)
388 fn(loaded=False)
389 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)
390
390
391 # loadall() is called multiple times and lingering _aftercallbacks
391 # loadall() is called multiple times and lingering _aftercallbacks
392 # entries could result in double execution. See issue4646.
392 # entries could result in double execution. See issue4646.
393 _aftercallbacks.clear()
393 _aftercallbacks.clear()
394
394
395 # delay importing avoids cyclic dependency (especially commands)
395 # delay importing avoids cyclic dependency (especially commands)
396 from . import (
396 from . import (
397 color,
397 color,
398 commands,
398 commands,
399 filemerge,
399 filemerge,
400 fileset,
400 fileset,
401 revset,
401 revset,
402 templatefilters,
402 templatefilters,
403 templatefuncs,
403 templatefuncs,
404 templatekw,
404 templatekw,
405 )
405 )
406
406
407 # list of (objname, loadermod, loadername) tuple:
407 # list of (objname, loadermod, loadername) tuple:
408 # - objname is the name of an object in extension module,
408 # - objname is the name of an object in extension module,
409 # from which extra information is loaded
409 # from which extra information is loaded
410 # - loadermod is the module where loader is placed
410 # - loadermod is the module where loader is placed
411 # - loadername is the name of the function,
411 # - loadername is the name of the function,
412 # which takes (ui, extensionname, extraobj) arguments
412 # which takes (ui, extensionname, extraobj) arguments
413 ui.log(b'extension', b'- loading extension registration objects\n')
413 ui.log(b'extension', b'- loading extension registration objects\n')
414 extraloaders = [
414 extraloaders = [
415 (b'cmdtable', commands, b'loadcmdtable'),
415 (b'cmdtable', commands, b'loadcmdtable'),
416 (b'colortable', color, b'loadcolortable'),
416 (b'colortable', color, b'loadcolortable'),
417 (b'filesetpredicate', fileset, b'loadpredicate'),
417 (b'filesetpredicate', fileset, b'loadpredicate'),
418 (b'internalmerge', filemerge, b'loadinternalmerge'),
418 (b'internalmerge', filemerge, b'loadinternalmerge'),
419 (b'revsetpredicate', revset, b'loadpredicate'),
419 (b'revsetpredicate', revset, b'loadpredicate'),
420 (b'templatefilter', templatefilters, b'loadfilter'),
420 (b'templatefilter', templatefilters, b'loadfilter'),
421 (b'templatefunc', templatefuncs, b'loadfunction'),
421 (b'templatefunc', templatefuncs, b'loadfunction'),
422 (b'templatekeyword', templatekw, b'loadkeyword'),
422 (b'templatekeyword', templatekw, b'loadkeyword'),
423 ]
423 ]
424 with util.timedcm('load registration objects') as stats:
424 with util.timedcm('load registration objects') as stats:
425 _loadextra(ui, newindex, extraloaders)
425 _loadextra(ui, newindex, extraloaders)
426 ui.log(
426 ui.log(
427 b'extension',
427 b'extension',
428 b'> extension registration object loading took %s\n',
428 b'> extension registration object loading took %s\n',
429 stats,
429 stats,
430 )
430 )
431
431
432 # Report per extension loading time (except reposetup)
432 # Report per extension loading time (except reposetup)
433 for name in sorted(loadingtime):
433 for name in sorted(loadingtime):
434 ui.log(
434 ui.log(
435 b'extension',
435 b'extension',
436 b'> extension %s take a total of %s to load\n',
436 b'> extension %s take a total of %s to load\n',
437 name,
437 name,
438 util.timecount(loadingtime[name]),
438 util.timecount(loadingtime[name]),
439 )
439 )
440
440
441 ui.log(b'extension', b'extension loading complete\n')
441 ui.log(b'extension', b'extension loading complete\n')
442
442
443
443
444 def _loadextra(ui, newindex, extraloaders):
444 def _loadextra(ui, newindex, extraloaders):
445 for name in _order[newindex:]:
445 for name in _order[newindex:]:
446 module = _extensions[name]
446 module = _extensions[name]
447 if not module:
447 if not module:
448 continue # loading this module failed
448 continue # loading this module failed
449
449
450 for objname, loadermod, loadername in extraloaders:
450 for objname, loadermod, loadername in extraloaders:
451 extraobj = getattr(module, objname, None)
451 extraobj = getattr(module, objname, None)
452 if extraobj is not None:
452 if extraobj is not None:
453 getattr(loadermod, loadername)(ui, name, extraobj)
453 getattr(loadermod, loadername)(ui, name, extraobj)
454
454
455
455
456 def afterloaded(extension, callback):
456 def afterloaded(extension, callback):
457 '''Run the specified function after a named extension is loaded.
457 '''Run the specified function after a named extension is loaded.
458
458
459 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
460 immediately.
460 immediately.
461
461
462 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
463 all extensions have been loaded.
463 all extensions have been loaded.
464
464
465 The callback receives the named argument ``loaded``, which is a boolean
465 The callback receives the named argument ``loaded``, which is a boolean
466 indicating whether the dependent extension actually loaded.
466 indicating whether the dependent extension actually loaded.
467 '''
467 '''
468
468
469 if extension in _extensions:
469 if extension in _extensions:
470 # Report loaded as False if the extension is disabled
470 # Report loaded as False if the extension is disabled
471 loaded = _extensions[extension] is not None
471 loaded = _extensions[extension] is not None
472 callback(loaded=loaded)
472 callback(loaded=loaded)
473 else:
473 else:
474 _aftercallbacks.setdefault(extension, []).append(callback)
474 _aftercallbacks.setdefault(extension, []).append(callback)
475
475
476
476
477 def populateui(ui):
477 def populateui(ui):
478 """Run extension hooks on the given ui to populate additional members,
478 """Run extension hooks on the given ui to populate additional members,
479 extend the class dynamically, etc.
479 extend the class dynamically, etc.
480
480
481 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
482 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
483 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.
484 """
484 """
485 for name, mod in extensions(ui):
485 for name, mod in extensions(ui):
486 hook = getattr(mod, 'uipopulate', None)
486 hook = getattr(mod, 'uipopulate', None)
487 if not hook:
487 if not hook:
488 continue
488 continue
489 try:
489 try:
490 hook(ui)
490 hook(ui)
491 except Exception as inst:
491 except Exception as inst:
492 ui.traceback(force=True)
492 ui.traceback(force=True)
493 ui.warn(
493 ui.warn(
494 _(b'*** failed to populate ui by extension %s: %s\n')
494 _(b'*** failed to populate ui by extension %s: %s\n')
495 % (name, stringutil.forcebytestr(inst))
495 % (name, stringutil.forcebytestr(inst))
496 )
496 )
497
497
498
498
499 def bind(func, *args):
499 def bind(func, *args):
500 '''Partial function application
500 '''Partial function application
501
501
502 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
503 to func. For example,
503 to func. For example,
504
504
505 f(1, 2, bar=3) === bind(f, 1)(2, bar=3)'''
505 f(1, 2, bar=3) === bind(f, 1)(2, bar=3)'''
506 assert callable(func)
506 assert callable(func)
507
507
508 def closure(*a, **kw):
508 def closure(*a, **kw):
509 return func(*(args + a), **kw)
509 return func(*(args + a), **kw)
510
510
511 return closure
511 return closure
512
512
513
513
514 def _updatewrapper(wrap, origfn, unboundwrapper):
514 def _updatewrapper(wrap, origfn, unboundwrapper):
515 '''Copy and add some useful attributes to wrapper'''
515 '''Copy and add some useful attributes to wrapper'''
516 try:
516 try:
517 wrap.__name__ = origfn.__name__
517 wrap.__name__ = origfn.__name__
518 except AttributeError:
518 except AttributeError:
519 pass
519 pass
520 wrap.__module__ = getattr(origfn, '__module__')
520 wrap.__module__ = getattr(origfn, '__module__')
521 wrap.__doc__ = getattr(origfn, '__doc__')
521 wrap.__doc__ = getattr(origfn, '__doc__')
522 wrap.__dict__.update(getattr(origfn, '__dict__', {}))
522 wrap.__dict__.update(getattr(origfn, '__dict__', {}))
523 wrap._origfunc = origfn
523 wrap._origfunc = origfn
524 wrap._unboundwrapper = unboundwrapper
524 wrap._unboundwrapper = unboundwrapper
525
525
526
526
527 def wrapcommand(table, command, wrapper, synopsis=None, docstring=None):
527 def wrapcommand(table, command, wrapper, synopsis=None, docstring=None):
528 '''Wrap the command named `command' in table
528 '''Wrap the command named `command' in table
529
529
530 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
531 be inserted into the command table specified by the table argument.
531 be inserted into the command table specified by the table argument.
532
532
533 The wrapper will be called like
533 The wrapper will be called like
534
534
535 wrapper(orig, *args, **kwargs)
535 wrapper(orig, *args, **kwargs)
536
536
537 where orig is the original (wrapped) function, and *args, **kwargs
537 where orig is the original (wrapped) function, and *args, **kwargs
538 are the arguments passed to it.
538 are the arguments passed to it.
539
539
540 Optionally append to the command synopsis and docstring, used for help.
540 Optionally append to the command synopsis and docstring, used for help.
541 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
542 flags ``--remote`` and ``--all`` you might call this function like so:
542 flags ``--remote`` and ``--all`` you might call this function like so:
543
543
544 synopsis = ' [-a] [--remote]'
544 synopsis = ' [-a] [--remote]'
545 docstring = """
545 docstring = """
546
546
547 The ``remotenames`` extension adds the ``--remote`` and ``--all`` (``-a``)
547 The ``remotenames`` extension adds the ``--remote`` and ``--all`` (``-a``)
548 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
549 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
550 local bookmarks.
550 local bookmarks.
551 """
551 """
552
552
553 extensions.wrapcommand(commands.table, 'bookmarks', exbookmarks,
553 extensions.wrapcommand(commands.table, 'bookmarks', exbookmarks,
554 synopsis, docstring)
554 synopsis, docstring)
555 '''
555 '''
556 assert callable(wrapper)
556 assert callable(wrapper)
557 aliases, entry = cmdutil.findcmd(command, table)
557 aliases, entry = cmdutil.findcmd(command, table)
558 for alias, e in pycompat.iteritems(table):
558 for alias, e in pycompat.iteritems(table):
559 if e is entry:
559 if e is entry:
560 key = alias
560 key = alias
561 break
561 break
562
562
563 origfn = entry[0]
563 origfn = entry[0]
564 wrap = functools.partial(
564 wrap = functools.partial(
565 util.checksignature(wrapper), util.checksignature(origfn)
565 util.checksignature(wrapper), util.checksignature(origfn)
566 )
566 )
567 _updatewrapper(wrap, origfn, wrapper)
567 _updatewrapper(wrap, origfn, wrapper)
568 if docstring is not None:
568 if docstring is not None:
569 wrap.__doc__ += docstring
569 wrap.__doc__ += docstring
570
570
571 newentry = list(entry)
571 newentry = list(entry)
572 newentry[0] = wrap
572 newentry[0] = wrap
573 if synopsis is not None:
573 if synopsis is not None:
574 newentry[2] += synopsis
574 newentry[2] += synopsis
575 table[key] = tuple(newentry)
575 table[key] = tuple(newentry)
576 return entry
576 return entry
577
577
578
578
579 def wrapfilecache(cls, propname, wrapper):
579 def wrapfilecache(cls, propname, wrapper):
580 """Wraps a filecache property.
580 """Wraps a filecache property.
581
581
582 These can't be wrapped using the normal wrapfunction.
582 These can't be wrapped using the normal wrapfunction.
583 """
583 """
584 propname = pycompat.sysstr(propname)
584 propname = pycompat.sysstr(propname)
585 assert callable(wrapper)
585 assert callable(wrapper)
586 for currcls in cls.__mro__:
586 for currcls in cls.__mro__:
587 if propname in currcls.__dict__:
587 if propname in currcls.__dict__:
588 origfn = currcls.__dict__[propname].func
588 origfn = currcls.__dict__[propname].func
589 assert callable(origfn)
589 assert callable(origfn)
590
590
591 def wrap(*args, **kwargs):
591 def wrap(*args, **kwargs):
592 return wrapper(origfn, *args, **kwargs)
592 return wrapper(origfn, *args, **kwargs)
593
593
594 currcls.__dict__[propname].func = wrap
594 currcls.__dict__[propname].func = wrap
595 break
595 break
596
596
597 if currcls is object:
597 if currcls is object:
598 raise AttributeError("type '%s' has no property '%s'" % (cls, propname))
598 raise AttributeError("type '%s' has no property '%s'" % (cls, propname))
599
599
600
600
601 class wrappedfunction(object):
601 class wrappedfunction(object):
602 '''context manager for temporarily wrapping a function'''
602 '''context manager for temporarily wrapping a function'''
603
603
604 def __init__(self, container, funcname, wrapper):
604 def __init__(self, container, funcname, wrapper):
605 assert callable(wrapper)
605 assert callable(wrapper)
606 self._container = container
606 self._container = container
607 self._funcname = funcname
607 self._funcname = funcname
608 self._wrapper = wrapper
608 self._wrapper = wrapper
609
609
610 def __enter__(self):
610 def __enter__(self):
611 wrapfunction(self._container, self._funcname, self._wrapper)
611 wrapfunction(self._container, self._funcname, self._wrapper)
612
612
613 def __exit__(self, exctype, excvalue, traceback):
613 def __exit__(self, exctype, excvalue, traceback):
614 unwrapfunction(self._container, self._funcname, self._wrapper)
614 unwrapfunction(self._container, self._funcname, self._wrapper)
615
615
616
616
617 def wrapfunction(container, funcname, wrapper):
617 def wrapfunction(container, funcname, wrapper):
618 '''Wrap the function named funcname in container
618 '''Wrap the function named funcname in container
619
619
620 Replace the funcname member in the given container with the specified
620 Replace the funcname member in the given container with the specified
621 wrapper. The container is typically a module, class, or instance.
621 wrapper. The container is typically a module, class, or instance.
622
622
623 The wrapper will be called like
623 The wrapper will be called like
624
624
625 wrapper(orig, *args, **kwargs)
625 wrapper(orig, *args, **kwargs)
626
626
627 where orig is the original (wrapped) function, and *args, **kwargs
627 where orig is the original (wrapped) function, and *args, **kwargs
628 are the arguments passed to it.
628 are the arguments passed to it.
629
629
630 Wrapping methods of the repository object is not recommended since
630 Wrapping methods of the repository object is not recommended since
631 it conflicts with extensions that extend the repository by
631 it conflicts with extensions that extend the repository by
632 subclassing. All extensions that need to extend methods of
632 subclassing. All extensions that need to extend methods of
633 localrepository should use this subclassing trick: namely,
633 localrepository should use this subclassing trick: namely,
634 reposetup() should look like
634 reposetup() should look like
635
635
636 def reposetup(ui, repo):
636 def reposetup(ui, repo):
637 class myrepo(repo.__class__):
637 class myrepo(repo.__class__):
638 def whatever(self, *args, **kwargs):
638 def whatever(self, *args, **kwargs):
639 [...extension stuff...]
639 [...extension stuff...]
640 super(myrepo, self).whatever(*args, **kwargs)
640 super(myrepo, self).whatever(*args, **kwargs)
641 [...extension stuff...]
641 [...extension stuff...]
642
642
643 repo.__class__ = myrepo
643 repo.__class__ = myrepo
644
644
645 In general, combining wrapfunction() with subclassing does not
645 In general, combining wrapfunction() with subclassing does not
646 work. Since you cannot control what other extensions are loaded by
646 work. Since you cannot control what other extensions are loaded by
647 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
648 subclass trick.
648 subclass trick.
649 '''
649 '''
650 assert callable(wrapper)
650 assert callable(wrapper)
651
651
652 origfn = getattr(container, funcname)
652 origfn = getattr(container, funcname)
653 assert callable(origfn)
653 assert callable(origfn)
654 if inspect.ismodule(container):
654 if inspect.ismodule(container):
655 # 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.
656 # "partial" won't insert a frame in traceback.
656 # "partial" won't insert a frame in traceback.
657 wrap = functools.partial(wrapper, origfn)
657 wrap = functools.partial(wrapper, origfn)
658 else:
658 else:
659 # "partial" cannot be safely used. Emulate its effect by using "bind".
659 # "partial" cannot be safely used. Emulate its effect by using "bind".
660 # The downside is one more frame in traceback.
660 # The downside is one more frame in traceback.
661 wrap = bind(wrapper, origfn)
661 wrap = bind(wrapper, origfn)
662 _updatewrapper(wrap, origfn, wrapper)
662 _updatewrapper(wrap, origfn, wrapper)
663 setattr(container, funcname, wrap)
663 setattr(container, funcname, wrap)
664 return origfn
664 return origfn
665
665
666
666
667 def unwrapfunction(container, funcname, wrapper=None):
667 def unwrapfunction(container, funcname, wrapper=None):
668 '''undo wrapfunction
668 '''undo wrapfunction
669
669
670 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
671 from the chain of wrappers.
671 from the chain of wrappers.
672
672
673 Return the removed wrapper.
673 Return the removed wrapper.
674 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
675 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.
676 '''
676 '''
677 chain = getwrapperchain(container, funcname)
677 chain = getwrapperchain(container, funcname)
678 origfn = chain.pop()
678 origfn = chain.pop()
679 if wrapper is None:
679 if wrapper is None:
680 wrapper = chain[0]
680 wrapper = chain[0]
681 chain.remove(wrapper)
681 chain.remove(wrapper)
682 setattr(container, funcname, origfn)
682 setattr(container, funcname, origfn)
683 for w in reversed(chain):
683 for w in reversed(chain):
684 wrapfunction(container, funcname, w)
684 wrapfunction(container, funcname, w)
685 return wrapper
685 return wrapper
686
686
687
687
688 def getwrapperchain(container, funcname):
688 def getwrapperchain(container, funcname):
689 '''get a chain of wrappers of a function
689 '''get a chain of wrappers of a function
690
690
691 Return a list of functions: [newest wrapper, ..., oldest wrapper, origfunc]
691 Return a list of functions: [newest wrapper, ..., oldest wrapper, origfunc]
692
692
693 The wrapper functions are the ones passed to wrapfunction, whose first
693 The wrapper functions are the ones passed to wrapfunction, whose first
694 argument is origfunc.
694 argument is origfunc.
695 '''
695 '''
696 result = []
696 result = []
697 fn = getattr(container, funcname)
697 fn = getattr(container, funcname)
698 while fn:
698 while fn:
699 assert callable(fn)
699 assert callable(fn)
700 result.append(getattr(fn, '_unboundwrapper', fn))
700 result.append(getattr(fn, '_unboundwrapper', fn))
701 fn = getattr(fn, '_origfunc', None)
701 fn = getattr(fn, '_origfunc', None)
702 return result
702 return result
703
703
704
704
705 def _disabledpaths():
705 def _disabledpaths():
706 '''find paths of disabled extensions. returns a dict of {name: path}'''
706 '''find paths of disabled extensions. returns a dict of {name: path}'''
707 import hgext
707 import hgext
708
708
709 extpath = os.path.dirname(
709 # The hgext might not have a __file__ attribute (e.g. in PyOxidizer) and
710 os.path.abspath(pycompat.fsencode(hgext.__file__))
710 # it might not be on a filesystem even if it does.
711 )
711 if util.safehasattr(hgext, '__file__'):
712 try: # might not be a filesystem path
712 extpath = os.path.dirname(
713 files = os.listdir(extpath)
713 os.path.abspath(pycompat.fsencode(hgext.__file__))
714 except OSError:
714 )
715 try:
716 files = os.listdir(extpath)
717 except OSError:
718 return {}
719 else:
715 return {}
720 return {}
716
721
717 exts = {}
722 exts = {}
718 for e in files:
723 for e in files:
719 if e.endswith(b'.py'):
724 if e.endswith(b'.py'):
720 name = e.rsplit(b'.', 1)[0]
725 name = e.rsplit(b'.', 1)[0]
721 path = os.path.join(extpath, e)
726 path = os.path.join(extpath, e)
722 else:
727 else:
723 name = e
728 name = e
724 path = os.path.join(extpath, e, b'__init__.py')
729 path = os.path.join(extpath, e, b'__init__.py')
725 if not os.path.exists(path):
730 if not os.path.exists(path):
726 continue
731 continue
727 if name in exts or name in _order or name == b'__init__':
732 if name in exts or name in _order or name == b'__init__':
728 continue
733 continue
729 exts[name] = path
734 exts[name] = path
730 for name, path in pycompat.iteritems(_disabledextensions):
735 for name, path in pycompat.iteritems(_disabledextensions):
731 # If no path was provided for a disabled extension (e.g. "color=!"),
736 # If no path was provided for a disabled extension (e.g. "color=!"),
732 # don't replace the path we already found by the scan above.
737 # don't replace the path we already found by the scan above.
733 if path:
738 if path:
734 exts[name] = path
739 exts[name] = path
735 return exts
740 return exts
736
741
737
742
738 def _moduledoc(file):
743 def _moduledoc(file):
739 '''return the top-level python documentation for the given file
744 '''return the top-level python documentation for the given file
740
745
741 Loosely inspired by pydoc.source_synopsis(), but rewritten to
746 Loosely inspired by pydoc.source_synopsis(), but rewritten to
742 handle triple quotes and to return the whole text instead of just
747 handle triple quotes and to return the whole text instead of just
743 the synopsis'''
748 the synopsis'''
744 result = []
749 result = []
745
750
746 line = file.readline()
751 line = file.readline()
747 while line[:1] == b'#' or not line.strip():
752 while line[:1] == b'#' or not line.strip():
748 line = file.readline()
753 line = file.readline()
749 if not line:
754 if not line:
750 break
755 break
751
756
752 start = line[:3]
757 start = line[:3]
753 if start == b'"""' or start == b"'''":
758 if start == b'"""' or start == b"'''":
754 line = line[3:]
759 line = line[3:]
755 while line:
760 while line:
756 if line.rstrip().endswith(start):
761 if line.rstrip().endswith(start):
757 line = line.split(start)[0]
762 line = line.split(start)[0]
758 if line:
763 if line:
759 result.append(line)
764 result.append(line)
760 break
765 break
761 elif not line:
766 elif not line:
762 return None # unmatched delimiter
767 return None # unmatched delimiter
763 result.append(line)
768 result.append(line)
764 line = file.readline()
769 line = file.readline()
765 else:
770 else:
766 return None
771 return None
767
772
768 return b''.join(result)
773 return b''.join(result)
769
774
770
775
771 def _disabledhelp(path):
776 def _disabledhelp(path):
772 '''retrieve help synopsis of a disabled extension (without importing)'''
777 '''retrieve help synopsis of a disabled extension (without importing)'''
773 try:
778 try:
774 with open(path, b'rb') as src:
779 with open(path, b'rb') as src:
775 doc = _moduledoc(src)
780 doc = _moduledoc(src)
776 except IOError:
781 except IOError:
777 return
782 return
778
783
779 if doc: # extracting localized synopsis
784 if doc: # extracting localized synopsis
780 return gettext(doc)
785 return gettext(doc)
781 else:
786 else:
782 return _(b'(no help text available)')
787 return _(b'(no help text available)')
783
788
784
789
785 def disabled():
790 def disabled():
786 '''find disabled extensions from hgext. returns a dict of {name: desc}'''
791 '''find disabled extensions from hgext. returns a dict of {name: desc}'''
787 try:
792 try:
788 from hgext import __index__ # pytype: disable=import-error
793 from hgext import __index__ # pytype: disable=import-error
789
794
790 return {
795 return {
791 name: gettext(desc)
796 name: gettext(desc)
792 for name, desc in pycompat.iteritems(__index__.docs)
797 for name, desc in pycompat.iteritems(__index__.docs)
793 if name not in _order
798 if name not in _order
794 }
799 }
795 except (ImportError, AttributeError):
800 except (ImportError, AttributeError):
796 pass
801 pass
797
802
798 paths = _disabledpaths()
803 paths = _disabledpaths()
799 if not paths:
804 if not paths:
800 return {}
805 return {}
801
806
802 exts = {}
807 exts = {}
803 for name, path in pycompat.iteritems(paths):
808 for name, path in pycompat.iteritems(paths):
804 doc = _disabledhelp(path)
809 doc = _disabledhelp(path)
805 if doc:
810 if doc:
806 exts[name] = doc.splitlines()[0]
811 exts[name] = doc.splitlines()[0]
807
812
808 return exts
813 return exts
809
814
810
815
811 def disabled_help(name):
816 def disabled_help(name):
812 """Obtain the full help text for a disabled extension, or None."""
817 """Obtain the full help text for a disabled extension, or None."""
813 paths = _disabledpaths()
818 paths = _disabledpaths()
814 if name in paths:
819 if name in paths:
815 return _disabledhelp(paths[name])
820 return _disabledhelp(paths[name])
816
821
817
822
818 def _walkcommand(node):
823 def _walkcommand(node):
819 """Scan @command() decorators in the tree starting at node"""
824 """Scan @command() decorators in the tree starting at node"""
820 todo = collections.deque([node])
825 todo = collections.deque([node])
821 while todo:
826 while todo:
822 node = todo.popleft()
827 node = todo.popleft()
823 if not isinstance(node, ast.FunctionDef):
828 if not isinstance(node, ast.FunctionDef):
824 todo.extend(ast.iter_child_nodes(node))
829 todo.extend(ast.iter_child_nodes(node))
825 continue
830 continue
826 for d in node.decorator_list:
831 for d in node.decorator_list:
827 if not isinstance(d, ast.Call):
832 if not isinstance(d, ast.Call):
828 continue
833 continue
829 if not isinstance(d.func, ast.Name):
834 if not isinstance(d.func, ast.Name):
830 continue
835 continue
831 if d.func.id != 'command':
836 if d.func.id != 'command':
832 continue
837 continue
833 yield d
838 yield d
834
839
835
840
836 def _disabledcmdtable(path):
841 def _disabledcmdtable(path):
837 """Construct a dummy command table without loading the extension module
842 """Construct a dummy command table without loading the extension module
838
843
839 This may raise IOError or SyntaxError.
844 This may raise IOError or SyntaxError.
840 """
845 """
841 with open(path, b'rb') as src:
846 with open(path, b'rb') as src:
842 root = ast.parse(src.read(), path)
847 root = ast.parse(src.read(), path)
843 cmdtable = {}
848 cmdtable = {}
844 for node in _walkcommand(root):
849 for node in _walkcommand(root):
845 if not node.args:
850 if not node.args:
846 continue
851 continue
847 a = node.args[0]
852 a = node.args[0]
848 if isinstance(a, ast.Str):
853 if isinstance(a, ast.Str):
849 name = pycompat.sysbytes(a.s)
854 name = pycompat.sysbytes(a.s)
850 elif pycompat.ispy3 and isinstance(a, ast.Bytes):
855 elif pycompat.ispy3 and isinstance(a, ast.Bytes):
851 name = a.s
856 name = a.s
852 else:
857 else:
853 continue
858 continue
854 cmdtable[name] = (None, [], b'')
859 cmdtable[name] = (None, [], b'')
855 return cmdtable
860 return cmdtable
856
861
857
862
858 def _finddisabledcmd(ui, cmd, name, path, strict):
863 def _finddisabledcmd(ui, cmd, name, path, strict):
859 try:
864 try:
860 cmdtable = _disabledcmdtable(path)
865 cmdtable = _disabledcmdtable(path)
861 except (IOError, SyntaxError):
866 except (IOError, SyntaxError):
862 return
867 return
863 try:
868 try:
864 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
869 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
865 except (error.AmbiguousCommand, error.UnknownCommand):
870 except (error.AmbiguousCommand, error.UnknownCommand):
866 return
871 return
867 for c in aliases:
872 for c in aliases:
868 if c.startswith(cmd):
873 if c.startswith(cmd):
869 cmd = c
874 cmd = c
870 break
875 break
871 else:
876 else:
872 cmd = aliases[0]
877 cmd = aliases[0]
873 doc = _disabledhelp(path)
878 doc = _disabledhelp(path)
874 return (cmd, name, doc)
879 return (cmd, name, doc)
875
880
876
881
877 def disabledcmd(ui, cmd, strict=False):
882 def disabledcmd(ui, cmd, strict=False):
878 '''find cmd from disabled extensions without importing.
883 '''find cmd from disabled extensions without importing.
879 returns (cmdname, extname, doc)'''
884 returns (cmdname, extname, doc)'''
880
885
881 paths = _disabledpaths()
886 paths = _disabledpaths()
882 if not paths:
887 if not paths:
883 raise error.UnknownCommand(cmd)
888 raise error.UnknownCommand(cmd)
884
889
885 ext = None
890 ext = None
886 # first, search for an extension with the same name as the command
891 # first, search for an extension with the same name as the command
887 path = paths.pop(cmd, None)
892 path = paths.pop(cmd, None)
888 if path:
893 if path:
889 ext = _finddisabledcmd(ui, cmd, cmd, path, strict=strict)
894 ext = _finddisabledcmd(ui, cmd, cmd, path, strict=strict)
890 if not ext:
895 if not ext:
891 # otherwise, interrogate each extension until there's a match
896 # otherwise, interrogate each extension until there's a match
892 for name, path in pycompat.iteritems(paths):
897 for name, path in pycompat.iteritems(paths):
893 ext = _finddisabledcmd(ui, cmd, name, path, strict=strict)
898 ext = _finddisabledcmd(ui, cmd, name, path, strict=strict)
894 if ext:
899 if ext:
895 break
900 break
896 if ext:
901 if ext:
897 return ext
902 return ext
898
903
899 raise error.UnknownCommand(cmd)
904 raise error.UnknownCommand(cmd)
900
905
901
906
902 def enabled(shortname=True):
907 def enabled(shortname=True):
903 '''return a dict of {name: desc} of extensions'''
908 '''return a dict of {name: desc} of extensions'''
904 exts = {}
909 exts = {}
905 for ename, ext in extensions():
910 for ename, ext in extensions():
906 doc = gettext(ext.__doc__) or _(b'(no help text available)')
911 doc = gettext(ext.__doc__) or _(b'(no help text available)')
907 if shortname:
912 if shortname:
908 ename = ename.split(b'.')[-1]
913 ename = ename.split(b'.')[-1]
909 exts[ename] = doc.splitlines()[0].strip()
914 exts[ename] = doc.splitlines()[0].strip()
910
915
911 return exts
916 return exts
912
917
913
918
914 def notloaded():
919 def notloaded():
915 '''return short names of extensions that failed to load'''
920 '''return short names of extensions that failed to load'''
916 return [
921 return [
917 name for name, mod in pycompat.iteritems(_extensions) if mod is None
922 name for name, mod in pycompat.iteritems(_extensions) if mod is None
918 ]
923 ]
919
924
920
925
921 def moduleversion(module):
926 def moduleversion(module):
922 '''return version information from given module as a string'''
927 '''return version information from given module as a string'''
923 if util.safehasattr(module, b'getversion') and callable(module.getversion):
928 if util.safehasattr(module, b'getversion') and callable(module.getversion):
924 version = module.getversion()
929 version = module.getversion()
925 elif util.safehasattr(module, b'__version__'):
930 elif util.safehasattr(module, b'__version__'):
926 version = module.__version__
931 version = module.__version__
927 else:
932 else:
928 version = b''
933 version = b''
929 if isinstance(version, (list, tuple)):
934 if isinstance(version, (list, tuple)):
930 version = b'.'.join(pycompat.bytestr(o) for o in version)
935 version = b'.'.join(pycompat.bytestr(o) for o in version)
931 return version
936 return version
932
937
933
938
934 def ismoduleinternal(module):
939 def ismoduleinternal(module):
935 exttestedwith = getattr(module, 'testedwith', None)
940 exttestedwith = getattr(module, 'testedwith', None)
936 return exttestedwith == b"ships-with-hg-core"
941 return exttestedwith == b"ships-with-hg-core"
General Comments 0
You need to be logged in to leave comments. Login now