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