##// END OF EJS Templates
extensions: clear aftercallbacks after execution (issue4646)...
Gregory Szorc -
r24950:e6e7d1cc stable
parent child Browse files
Show More
@@ -1,439 +1,443
1 # extensions.py - extension handling for mercurial
1 # extensions.py - extension handling for mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@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 import imp, os
8 import imp, os
9 import util, cmdutil, error
9 import util, cmdutil, error
10 from i18n import _, gettext
10 from i18n import _, gettext
11
11
12 _extensions = {}
12 _extensions = {}
13 _aftercallbacks = {}
13 _aftercallbacks = {}
14 _order = []
14 _order = []
15 _ignore = ['hbisect', 'bookmarks', 'parentrevspec', 'interhg', 'inotify']
15 _ignore = ['hbisect', 'bookmarks', 'parentrevspec', 'interhg', 'inotify']
16
16
17 def extensions(ui=None):
17 def extensions(ui=None):
18 if ui:
18 if ui:
19 def enabled(name):
19 def enabled(name):
20 for format in ['%s', 'hgext.%s']:
20 for format in ['%s', 'hgext.%s']:
21 conf = ui.config('extensions', format % name)
21 conf = ui.config('extensions', format % name)
22 if conf is not None and not conf.startswith('!'):
22 if conf is not None and not conf.startswith('!'):
23 return True
23 return True
24 else:
24 else:
25 enabled = lambda name: True
25 enabled = lambda name: True
26 for name in _order:
26 for name in _order:
27 module = _extensions[name]
27 module = _extensions[name]
28 if module and enabled(name):
28 if module and enabled(name):
29 yield name, module
29 yield name, module
30
30
31 def find(name):
31 def find(name):
32 '''return module with given extension name'''
32 '''return module with given extension name'''
33 mod = None
33 mod = None
34 try:
34 try:
35 mod = _extensions[name]
35 mod = _extensions[name]
36 except KeyError:
36 except KeyError:
37 for k, v in _extensions.iteritems():
37 for k, v in _extensions.iteritems():
38 if k.endswith('.' + name) or k.endswith('/' + name):
38 if k.endswith('.' + name) or k.endswith('/' + name):
39 mod = v
39 mod = v
40 break
40 break
41 if not mod:
41 if not mod:
42 raise KeyError(name)
42 raise KeyError(name)
43 return mod
43 return mod
44
44
45 def loadpath(path, module_name):
45 def loadpath(path, module_name):
46 module_name = module_name.replace('.', '_')
46 module_name = module_name.replace('.', '_')
47 path = util.normpath(util.expandpath(path))
47 path = util.normpath(util.expandpath(path))
48 if os.path.isdir(path):
48 if os.path.isdir(path):
49 # module/__init__.py style
49 # module/__init__.py style
50 d, f = os.path.split(path)
50 d, f = os.path.split(path)
51 fd, fpath, desc = imp.find_module(f, [d])
51 fd, fpath, desc = imp.find_module(f, [d])
52 return imp.load_module(module_name, fd, fpath, desc)
52 return imp.load_module(module_name, fd, fpath, desc)
53 else:
53 else:
54 try:
54 try:
55 return imp.load_source(module_name, path)
55 return imp.load_source(module_name, path)
56 except IOError, exc:
56 except IOError, exc:
57 if not exc.filename:
57 if not exc.filename:
58 exc.filename = path # python does not fill this
58 exc.filename = path # python does not fill this
59 raise
59 raise
60
60
61 def load(ui, name, path):
61 def load(ui, name, path):
62 if name.startswith('hgext.') or name.startswith('hgext/'):
62 if name.startswith('hgext.') or name.startswith('hgext/'):
63 shortname = name[6:]
63 shortname = name[6:]
64 else:
64 else:
65 shortname = name
65 shortname = name
66 if shortname in _ignore:
66 if shortname in _ignore:
67 return None
67 return None
68 if shortname in _extensions:
68 if shortname in _extensions:
69 return _extensions[shortname]
69 return _extensions[shortname]
70 _extensions[shortname] = None
70 _extensions[shortname] = None
71 if path:
71 if path:
72 # the module will be loaded in sys.modules
72 # the module will be loaded in sys.modules
73 # choose an unique name so that it doesn't
73 # choose an unique name so that it doesn't
74 # conflicts with other modules
74 # conflicts with other modules
75 mod = loadpath(path, 'hgext.%s' % name)
75 mod = loadpath(path, 'hgext.%s' % name)
76 else:
76 else:
77 def importh(name):
77 def importh(name):
78 mod = __import__(name)
78 mod = __import__(name)
79 components = name.split('.')
79 components = name.split('.')
80 for comp in components[1:]:
80 for comp in components[1:]:
81 mod = getattr(mod, comp)
81 mod = getattr(mod, comp)
82 return mod
82 return mod
83 try:
83 try:
84 mod = importh("hgext.%s" % name)
84 mod = importh("hgext.%s" % name)
85 except ImportError, err:
85 except ImportError, err:
86 ui.debug('could not import hgext.%s (%s): trying %s\n'
86 ui.debug('could not import hgext.%s (%s): trying %s\n'
87 % (name, err, name))
87 % (name, err, name))
88 mod = importh(name)
88 mod = importh(name)
89 _extensions[shortname] = mod
89 _extensions[shortname] = mod
90 _order.append(shortname)
90 _order.append(shortname)
91 for fn in _aftercallbacks.get(shortname, []):
91 for fn in _aftercallbacks.get(shortname, []):
92 fn(loaded=True)
92 fn(loaded=True)
93 return mod
93 return mod
94
94
95 def loadall(ui):
95 def loadall(ui):
96 result = ui.configitems("extensions")
96 result = ui.configitems("extensions")
97 newindex = len(_order)
97 newindex = len(_order)
98 for (name, path) in result:
98 for (name, path) in result:
99 if path:
99 if path:
100 if path[0] == '!':
100 if path[0] == '!':
101 continue
101 continue
102 try:
102 try:
103 load(ui, name, path)
103 load(ui, name, path)
104 except KeyboardInterrupt:
104 except KeyboardInterrupt:
105 raise
105 raise
106 except Exception, inst:
106 except Exception, inst:
107 if path:
107 if path:
108 ui.warn(_("*** failed to import extension %s from %s: %s\n")
108 ui.warn(_("*** failed to import extension %s from %s: %s\n")
109 % (name, path, inst))
109 % (name, path, inst))
110 else:
110 else:
111 ui.warn(_("*** failed to import extension %s: %s\n")
111 ui.warn(_("*** failed to import extension %s: %s\n")
112 % (name, inst))
112 % (name, inst))
113
113
114 for name in _order[newindex:]:
114 for name in _order[newindex:]:
115 uisetup = getattr(_extensions[name], 'uisetup', None)
115 uisetup = getattr(_extensions[name], 'uisetup', None)
116 if uisetup:
116 if uisetup:
117 uisetup(ui)
117 uisetup(ui)
118
118
119 for name in _order[newindex:]:
119 for name in _order[newindex:]:
120 extsetup = getattr(_extensions[name], 'extsetup', None)
120 extsetup = getattr(_extensions[name], 'extsetup', None)
121 if extsetup:
121 if extsetup:
122 try:
122 try:
123 extsetup(ui)
123 extsetup(ui)
124 except TypeError:
124 except TypeError:
125 if extsetup.func_code.co_argcount != 0:
125 if extsetup.func_code.co_argcount != 0:
126 raise
126 raise
127 extsetup() # old extsetup with no ui argument
127 extsetup() # old extsetup with no ui argument
128
128
129 # Call aftercallbacks that were never met.
129 # Call aftercallbacks that were never met.
130 for shortname in _aftercallbacks:
130 for shortname in _aftercallbacks:
131 if shortname in _extensions:
131 if shortname in _extensions:
132 continue
132 continue
133
133
134 for fn in _aftercallbacks[shortname]:
134 for fn in _aftercallbacks[shortname]:
135 fn(loaded=False)
135 fn(loaded=False)
136
136
137 # loadall() is called multiple times and lingering _aftercallbacks
138 # entries could result in double execution. See issue4646.
139 _aftercallbacks.clear()
140
137 def afterloaded(extension, callback):
141 def afterloaded(extension, callback):
138 '''Run the specified function after a named extension is loaded.
142 '''Run the specified function after a named extension is loaded.
139
143
140 If the named extension is already loaded, the callback will be called
144 If the named extension is already loaded, the callback will be called
141 immediately.
145 immediately.
142
146
143 If the named extension never loads, the callback will be called after
147 If the named extension never loads, the callback will be called after
144 all extensions have been loaded.
148 all extensions have been loaded.
145
149
146 The callback receives the named argument ``loaded``, which is a boolean
150 The callback receives the named argument ``loaded``, which is a boolean
147 indicating whether the dependent extension actually loaded.
151 indicating whether the dependent extension actually loaded.
148 '''
152 '''
149
153
150 if extension in _extensions:
154 if extension in _extensions:
151 callback(loaded=True)
155 callback(loaded=True)
152 else:
156 else:
153 _aftercallbacks.setdefault(extension, []).append(callback)
157 _aftercallbacks.setdefault(extension, []).append(callback)
154
158
155 def bind(func, *args):
159 def bind(func, *args):
156 '''Partial function application
160 '''Partial function application
157
161
158 Returns a new function that is the partial application of args and kwargs
162 Returns a new function that is the partial application of args and kwargs
159 to func. For example,
163 to func. For example,
160
164
161 f(1, 2, bar=3) === bind(f, 1)(2, bar=3)'''
165 f(1, 2, bar=3) === bind(f, 1)(2, bar=3)'''
162 assert callable(func)
166 assert callable(func)
163 def closure(*a, **kw):
167 def closure(*a, **kw):
164 return func(*(args + a), **kw)
168 return func(*(args + a), **kw)
165 return closure
169 return closure
166
170
167 def wrapcommand(table, command, wrapper, synopsis=None, docstring=None):
171 def wrapcommand(table, command, wrapper, synopsis=None, docstring=None):
168 '''Wrap the command named `command' in table
172 '''Wrap the command named `command' in table
169
173
170 Replace command in the command table with wrapper. The wrapped command will
174 Replace command in the command table with wrapper. The wrapped command will
171 be inserted into the command table specified by the table argument.
175 be inserted into the command table specified by the table argument.
172
176
173 The wrapper will be called like
177 The wrapper will be called like
174
178
175 wrapper(orig, *args, **kwargs)
179 wrapper(orig, *args, **kwargs)
176
180
177 where orig is the original (wrapped) function, and *args, **kwargs
181 where orig is the original (wrapped) function, and *args, **kwargs
178 are the arguments passed to it.
182 are the arguments passed to it.
179
183
180 Optionally append to the command synopsis and docstring, used for help.
184 Optionally append to the command synopsis and docstring, used for help.
181 For example, if your extension wraps the ``bookmarks`` command to add the
185 For example, if your extension wraps the ``bookmarks`` command to add the
182 flags ``--remote`` and ``--all`` you might call this function like so:
186 flags ``--remote`` and ``--all`` you might call this function like so:
183
187
184 synopsis = ' [-a] [--remote]'
188 synopsis = ' [-a] [--remote]'
185 docstring = """
189 docstring = """
186
190
187 The ``remotenames`` extension adds the ``--remote`` and ``--all`` (``-a``)
191 The ``remotenames`` extension adds the ``--remote`` and ``--all`` (``-a``)
188 flags to the bookmarks command. Either flag will show the remote bookmarks
192 flags to the bookmarks command. Either flag will show the remote bookmarks
189 known to the repository; ``--remote`` will also supress the output of the
193 known to the repository; ``--remote`` will also supress the output of the
190 local bookmarks.
194 local bookmarks.
191 """
195 """
192
196
193 extensions.wrapcommand(commands.table, 'bookmarks', exbookmarks,
197 extensions.wrapcommand(commands.table, 'bookmarks', exbookmarks,
194 synopsis, docstring)
198 synopsis, docstring)
195 '''
199 '''
196 assert callable(wrapper)
200 assert callable(wrapper)
197 aliases, entry = cmdutil.findcmd(command, table)
201 aliases, entry = cmdutil.findcmd(command, table)
198 for alias, e in table.iteritems():
202 for alias, e in table.iteritems():
199 if e is entry:
203 if e is entry:
200 key = alias
204 key = alias
201 break
205 break
202
206
203 origfn = entry[0]
207 origfn = entry[0]
204 wrap = bind(util.checksignature(wrapper), util.checksignature(origfn))
208 wrap = bind(util.checksignature(wrapper), util.checksignature(origfn))
205
209
206 wrap.__module__ = getattr(origfn, '__module__')
210 wrap.__module__ = getattr(origfn, '__module__')
207
211
208 doc = getattr(origfn, '__doc__')
212 doc = getattr(origfn, '__doc__')
209 if docstring is not None:
213 if docstring is not None:
210 doc += docstring
214 doc += docstring
211 wrap.__doc__ = doc
215 wrap.__doc__ = doc
212
216
213 newentry = list(entry)
217 newentry = list(entry)
214 newentry[0] = wrap
218 newentry[0] = wrap
215 if synopsis is not None:
219 if synopsis is not None:
216 newentry[2] += synopsis
220 newentry[2] += synopsis
217 table[key] = tuple(newentry)
221 table[key] = tuple(newentry)
218 return entry
222 return entry
219
223
220 def wrapfunction(container, funcname, wrapper):
224 def wrapfunction(container, funcname, wrapper):
221 '''Wrap the function named funcname in container
225 '''Wrap the function named funcname in container
222
226
223 Replace the funcname member in the given container with the specified
227 Replace the funcname member in the given container with the specified
224 wrapper. The container is typically a module, class, or instance.
228 wrapper. The container is typically a module, class, or instance.
225
229
226 The wrapper will be called like
230 The wrapper will be called like
227
231
228 wrapper(orig, *args, **kwargs)
232 wrapper(orig, *args, **kwargs)
229
233
230 where orig is the original (wrapped) function, and *args, **kwargs
234 where orig is the original (wrapped) function, and *args, **kwargs
231 are the arguments passed to it.
235 are the arguments passed to it.
232
236
233 Wrapping methods of the repository object is not recommended since
237 Wrapping methods of the repository object is not recommended since
234 it conflicts with extensions that extend the repository by
238 it conflicts with extensions that extend the repository by
235 subclassing. All extensions that need to extend methods of
239 subclassing. All extensions that need to extend methods of
236 localrepository should use this subclassing trick: namely,
240 localrepository should use this subclassing trick: namely,
237 reposetup() should look like
241 reposetup() should look like
238
242
239 def reposetup(ui, repo):
243 def reposetup(ui, repo):
240 class myrepo(repo.__class__):
244 class myrepo(repo.__class__):
241 def whatever(self, *args, **kwargs):
245 def whatever(self, *args, **kwargs):
242 [...extension stuff...]
246 [...extension stuff...]
243 super(myrepo, self).whatever(*args, **kwargs)
247 super(myrepo, self).whatever(*args, **kwargs)
244 [...extension stuff...]
248 [...extension stuff...]
245
249
246 repo.__class__ = myrepo
250 repo.__class__ = myrepo
247
251
248 In general, combining wrapfunction() with subclassing does not
252 In general, combining wrapfunction() with subclassing does not
249 work. Since you cannot control what other extensions are loaded by
253 work. Since you cannot control what other extensions are loaded by
250 your end users, you should play nicely with others by using the
254 your end users, you should play nicely with others by using the
251 subclass trick.
255 subclass trick.
252 '''
256 '''
253 assert callable(wrapper)
257 assert callable(wrapper)
254
258
255 origfn = getattr(container, funcname)
259 origfn = getattr(container, funcname)
256 assert callable(origfn)
260 assert callable(origfn)
257 setattr(container, funcname, bind(wrapper, origfn))
261 setattr(container, funcname, bind(wrapper, origfn))
258 return origfn
262 return origfn
259
263
260 def _disabledpaths(strip_init=False):
264 def _disabledpaths(strip_init=False):
261 '''find paths of disabled extensions. returns a dict of {name: path}
265 '''find paths of disabled extensions. returns a dict of {name: path}
262 removes /__init__.py from packages if strip_init is True'''
266 removes /__init__.py from packages if strip_init is True'''
263 import hgext
267 import hgext
264 extpath = os.path.dirname(os.path.abspath(hgext.__file__))
268 extpath = os.path.dirname(os.path.abspath(hgext.__file__))
265 try: # might not be a filesystem path
269 try: # might not be a filesystem path
266 files = os.listdir(extpath)
270 files = os.listdir(extpath)
267 except OSError:
271 except OSError:
268 return {}
272 return {}
269
273
270 exts = {}
274 exts = {}
271 for e in files:
275 for e in files:
272 if e.endswith('.py'):
276 if e.endswith('.py'):
273 name = e.rsplit('.', 1)[0]
277 name = e.rsplit('.', 1)[0]
274 path = os.path.join(extpath, e)
278 path = os.path.join(extpath, e)
275 else:
279 else:
276 name = e
280 name = e
277 path = os.path.join(extpath, e, '__init__.py')
281 path = os.path.join(extpath, e, '__init__.py')
278 if not os.path.exists(path):
282 if not os.path.exists(path):
279 continue
283 continue
280 if strip_init:
284 if strip_init:
281 path = os.path.dirname(path)
285 path = os.path.dirname(path)
282 if name in exts or name in _order or name == '__init__':
286 if name in exts or name in _order or name == '__init__':
283 continue
287 continue
284 exts[name] = path
288 exts[name] = path
285 return exts
289 return exts
286
290
287 def _moduledoc(file):
291 def _moduledoc(file):
288 '''return the top-level python documentation for the given file
292 '''return the top-level python documentation for the given file
289
293
290 Loosely inspired by pydoc.source_synopsis(), but rewritten to
294 Loosely inspired by pydoc.source_synopsis(), but rewritten to
291 handle triple quotes and to return the whole text instead of just
295 handle triple quotes and to return the whole text instead of just
292 the synopsis'''
296 the synopsis'''
293 result = []
297 result = []
294
298
295 line = file.readline()
299 line = file.readline()
296 while line[:1] == '#' or not line.strip():
300 while line[:1] == '#' or not line.strip():
297 line = file.readline()
301 line = file.readline()
298 if not line:
302 if not line:
299 break
303 break
300
304
301 start = line[:3]
305 start = line[:3]
302 if start == '"""' or start == "'''":
306 if start == '"""' or start == "'''":
303 line = line[3:]
307 line = line[3:]
304 while line:
308 while line:
305 if line.rstrip().endswith(start):
309 if line.rstrip().endswith(start):
306 line = line.split(start)[0]
310 line = line.split(start)[0]
307 if line:
311 if line:
308 result.append(line)
312 result.append(line)
309 break
313 break
310 elif not line:
314 elif not line:
311 return None # unmatched delimiter
315 return None # unmatched delimiter
312 result.append(line)
316 result.append(line)
313 line = file.readline()
317 line = file.readline()
314 else:
318 else:
315 return None
319 return None
316
320
317 return ''.join(result)
321 return ''.join(result)
318
322
319 def _disabledhelp(path):
323 def _disabledhelp(path):
320 '''retrieve help synopsis of a disabled extension (without importing)'''
324 '''retrieve help synopsis of a disabled extension (without importing)'''
321 try:
325 try:
322 file = open(path)
326 file = open(path)
323 except IOError:
327 except IOError:
324 return
328 return
325 else:
329 else:
326 doc = _moduledoc(file)
330 doc = _moduledoc(file)
327 file.close()
331 file.close()
328
332
329 if doc: # extracting localized synopsis
333 if doc: # extracting localized synopsis
330 return gettext(doc).splitlines()[0]
334 return gettext(doc).splitlines()[0]
331 else:
335 else:
332 return _('(no help text available)')
336 return _('(no help text available)')
333
337
334 def disabled():
338 def disabled():
335 '''find disabled extensions from hgext. returns a dict of {name: desc}'''
339 '''find disabled extensions from hgext. returns a dict of {name: desc}'''
336 try:
340 try:
337 from hgext import __index__
341 from hgext import __index__
338 return dict((name, gettext(desc))
342 return dict((name, gettext(desc))
339 for name, desc in __index__.docs.iteritems()
343 for name, desc in __index__.docs.iteritems()
340 if name not in _order)
344 if name not in _order)
341 except (ImportError, AttributeError):
345 except (ImportError, AttributeError):
342 pass
346 pass
343
347
344 paths = _disabledpaths()
348 paths = _disabledpaths()
345 if not paths:
349 if not paths:
346 return {}
350 return {}
347
351
348 exts = {}
352 exts = {}
349 for name, path in paths.iteritems():
353 for name, path in paths.iteritems():
350 doc = _disabledhelp(path)
354 doc = _disabledhelp(path)
351 if doc:
355 if doc:
352 exts[name] = doc
356 exts[name] = doc
353
357
354 return exts
358 return exts
355
359
356 def disabledext(name):
360 def disabledext(name):
357 '''find a specific disabled extension from hgext. returns desc'''
361 '''find a specific disabled extension from hgext. returns desc'''
358 try:
362 try:
359 from hgext import __index__
363 from hgext import __index__
360 if name in _order: # enabled
364 if name in _order: # enabled
361 return
365 return
362 else:
366 else:
363 return gettext(__index__.docs.get(name))
367 return gettext(__index__.docs.get(name))
364 except (ImportError, AttributeError):
368 except (ImportError, AttributeError):
365 pass
369 pass
366
370
367 paths = _disabledpaths()
371 paths = _disabledpaths()
368 if name in paths:
372 if name in paths:
369 return _disabledhelp(paths[name])
373 return _disabledhelp(paths[name])
370
374
371 def disabledcmd(ui, cmd, strict=False):
375 def disabledcmd(ui, cmd, strict=False):
372 '''import disabled extensions until cmd is found.
376 '''import disabled extensions until cmd is found.
373 returns (cmdname, extname, module)'''
377 returns (cmdname, extname, module)'''
374
378
375 paths = _disabledpaths(strip_init=True)
379 paths = _disabledpaths(strip_init=True)
376 if not paths:
380 if not paths:
377 raise error.UnknownCommand(cmd)
381 raise error.UnknownCommand(cmd)
378
382
379 def findcmd(cmd, name, path):
383 def findcmd(cmd, name, path):
380 try:
384 try:
381 mod = loadpath(path, 'hgext.%s' % name)
385 mod = loadpath(path, 'hgext.%s' % name)
382 except Exception:
386 except Exception:
383 return
387 return
384 try:
388 try:
385 aliases, entry = cmdutil.findcmd(cmd,
389 aliases, entry = cmdutil.findcmd(cmd,
386 getattr(mod, 'cmdtable', {}), strict)
390 getattr(mod, 'cmdtable', {}), strict)
387 except (error.AmbiguousCommand, error.UnknownCommand):
391 except (error.AmbiguousCommand, error.UnknownCommand):
388 return
392 return
389 except Exception:
393 except Exception:
390 ui.warn(_('warning: error finding commands in %s\n') % path)
394 ui.warn(_('warning: error finding commands in %s\n') % path)
391 ui.traceback()
395 ui.traceback()
392 return
396 return
393 for c in aliases:
397 for c in aliases:
394 if c.startswith(cmd):
398 if c.startswith(cmd):
395 cmd = c
399 cmd = c
396 break
400 break
397 else:
401 else:
398 cmd = aliases[0]
402 cmd = aliases[0]
399 return (cmd, name, mod)
403 return (cmd, name, mod)
400
404
401 ext = None
405 ext = None
402 # first, search for an extension with the same name as the command
406 # first, search for an extension with the same name as the command
403 path = paths.pop(cmd, None)
407 path = paths.pop(cmd, None)
404 if path:
408 if path:
405 ext = findcmd(cmd, cmd, path)
409 ext = findcmd(cmd, cmd, path)
406 if not ext:
410 if not ext:
407 # otherwise, interrogate each extension until there's a match
411 # otherwise, interrogate each extension until there's a match
408 for name, path in paths.iteritems():
412 for name, path in paths.iteritems():
409 ext = findcmd(cmd, name, path)
413 ext = findcmd(cmd, name, path)
410 if ext:
414 if ext:
411 break
415 break
412 if ext and 'DEPRECATED' not in ext.__doc__:
416 if ext and 'DEPRECATED' not in ext.__doc__:
413 return ext
417 return ext
414
418
415 raise error.UnknownCommand(cmd)
419 raise error.UnknownCommand(cmd)
416
420
417 def enabled(shortname=True):
421 def enabled(shortname=True):
418 '''return a dict of {name: desc} of extensions'''
422 '''return a dict of {name: desc} of extensions'''
419 exts = {}
423 exts = {}
420 for ename, ext in extensions():
424 for ename, ext in extensions():
421 doc = (gettext(ext.__doc__) or _('(no help text available)'))
425 doc = (gettext(ext.__doc__) or _('(no help text available)'))
422 if shortname:
426 if shortname:
423 ename = ename.split('.')[-1]
427 ename = ename.split('.')[-1]
424 exts[ename] = doc.splitlines()[0].strip()
428 exts[ename] = doc.splitlines()[0].strip()
425
429
426 return exts
430 return exts
427
431
428 def moduleversion(module):
432 def moduleversion(module):
429 '''return version information from given module as a string'''
433 '''return version information from given module as a string'''
430 if (util.safehasattr(module, 'getversion')
434 if (util.safehasattr(module, 'getversion')
431 and callable(module.getversion)):
435 and callable(module.getversion)):
432 version = module.getversion()
436 version = module.getversion()
433 elif util.safehasattr(module, '__version__'):
437 elif util.safehasattr(module, '__version__'):
434 version = module.__version__
438 version = module.__version__
435 else:
439 else:
436 version = ''
440 version = ''
437 if isinstance(version, (list, tuple)):
441 if isinstance(version, (list, tuple)):
438 version = '.'.join(str(o) for o in version)
442 version = '.'.join(str(o) for o in version)
439 return version
443 return version
General Comments 0
You need to be logged in to leave comments. Login now