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