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