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