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