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