##// END OF EJS Templates
extensions: indicate loaded for an immediately called afterload callback...
Matt Harbison -
r24145:28af978c default
parent child Browse files
Show More
@@ -1,431 +1,431 b''
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 def afterloaded(extension, callback):
137 def afterloaded(extension, callback):
138 '''Run the specified function after a named extension is loaded.
138 '''Run the specified function after a named extension is loaded.
139
139
140 If the named extension is already loaded, the callback will be called
140 If the named extension is already loaded, the callback will be called
141 immediately.
141 immediately.
142
142
143 If the named extension never loads, the callback will be called after
143 If the named extension never loads, the callback will be called after
144 all extensions have been loaded.
144 all extensions have been loaded.
145
145
146 The callback receives the named argument ``loaded``, which is a boolean
146 The callback receives the named argument ``loaded``, which is a boolean
147 indicating whether the dependent extension actually loaded.
147 indicating whether the dependent extension actually loaded.
148 '''
148 '''
149
149
150 if extension in _extensions:
150 if extension in _extensions:
151 callback(loaded=False)
151 callback(loaded=True)
152 else:
152 else:
153 _aftercallbacks.setdefault(extension, []).append(callback)
153 _aftercallbacks.setdefault(extension, []).append(callback)
154
154
155 def wrapcommand(table, command, wrapper, synopsis=None, docstring=None):
155 def wrapcommand(table, command, wrapper, synopsis=None, docstring=None):
156 '''Wrap the command named `command' in table
156 '''Wrap the command named `command' in table
157
157
158 Replace command in the command table with wrapper. The wrapped command will
158 Replace command in the command table with wrapper. The wrapped command will
159 be inserted into the command table specified by the table argument.
159 be inserted into the command table specified by the table argument.
160
160
161 The wrapper will be called like
161 The wrapper will be called like
162
162
163 wrapper(orig, *args, **kwargs)
163 wrapper(orig, *args, **kwargs)
164
164
165 where orig is the original (wrapped) function, and *args, **kwargs
165 where orig is the original (wrapped) function, and *args, **kwargs
166 are the arguments passed to it.
166 are the arguments passed to it.
167
167
168 Optionally append to the command synopsis and docstring, used for help.
168 Optionally append to the command synopsis and docstring, used for help.
169 For example, if your extension wraps the ``bookmarks`` command to add the
169 For example, if your extension wraps the ``bookmarks`` command to add the
170 flags ``--remote`` and ``--all`` you might call this function like so:
170 flags ``--remote`` and ``--all`` you might call this function like so:
171
171
172 synopsis = ' [-a] [--remote]'
172 synopsis = ' [-a] [--remote]'
173 docstring = """
173 docstring = """
174
174
175 The ``remotenames`` extension adds the ``--remote`` and ``--all`` (``-a``)
175 The ``remotenames`` extension adds the ``--remote`` and ``--all`` (``-a``)
176 flags to the bookmarks command. Either flag will show the remote bookmarks
176 flags to the bookmarks command. Either flag will show the remote bookmarks
177 known to the repository; ``--remote`` will also supress the output of the
177 known to the repository; ``--remote`` will also supress the output of the
178 local bookmarks.
178 local bookmarks.
179 """
179 """
180
180
181 extensions.wrapcommand(commands.table, 'bookmarks', exbookmarks,
181 extensions.wrapcommand(commands.table, 'bookmarks', exbookmarks,
182 synopsis, docstring)
182 synopsis, docstring)
183 '''
183 '''
184 assert callable(wrapper)
184 assert callable(wrapper)
185 aliases, entry = cmdutil.findcmd(command, table)
185 aliases, entry = cmdutil.findcmd(command, table)
186 for alias, e in table.iteritems():
186 for alias, e in table.iteritems():
187 if e is entry:
187 if e is entry:
188 key = alias
188 key = alias
189 break
189 break
190
190
191 origfn = entry[0]
191 origfn = entry[0]
192 def wrap(*args, **kwargs):
192 def wrap(*args, **kwargs):
193 return util.checksignature(wrapper)(
193 return util.checksignature(wrapper)(
194 util.checksignature(origfn), *args, **kwargs)
194 util.checksignature(origfn), *args, **kwargs)
195
195
196 wrap.__module__ = getattr(origfn, '__module__')
196 wrap.__module__ = getattr(origfn, '__module__')
197
197
198 doc = getattr(origfn, '__doc__')
198 doc = getattr(origfn, '__doc__')
199 if docstring is not None:
199 if docstring is not None:
200 doc += docstring
200 doc += docstring
201 wrap.__doc__ = doc
201 wrap.__doc__ = doc
202
202
203 newentry = list(entry)
203 newentry = list(entry)
204 newentry[0] = wrap
204 newentry[0] = wrap
205 if synopsis is not None:
205 if synopsis is not None:
206 newentry[2] += synopsis
206 newentry[2] += synopsis
207 table[key] = tuple(newentry)
207 table[key] = tuple(newentry)
208 return entry
208 return entry
209
209
210 def wrapfunction(container, funcname, wrapper):
210 def wrapfunction(container, funcname, wrapper):
211 '''Wrap the function named funcname in container
211 '''Wrap the function named funcname in container
212
212
213 Replace the funcname member in the given container with the specified
213 Replace the funcname member in the given container with the specified
214 wrapper. The container is typically a module, class, or instance.
214 wrapper. The container is typically a module, class, or instance.
215
215
216 The wrapper will be called like
216 The wrapper will be called like
217
217
218 wrapper(orig, *args, **kwargs)
218 wrapper(orig, *args, **kwargs)
219
219
220 where orig is the original (wrapped) function, and *args, **kwargs
220 where orig is the original (wrapped) function, and *args, **kwargs
221 are the arguments passed to it.
221 are the arguments passed to it.
222
222
223 Wrapping methods of the repository object is not recommended since
223 Wrapping methods of the repository object is not recommended since
224 it conflicts with extensions that extend the repository by
224 it conflicts with extensions that extend the repository by
225 subclassing. All extensions that need to extend methods of
225 subclassing. All extensions that need to extend methods of
226 localrepository should use this subclassing trick: namely,
226 localrepository should use this subclassing trick: namely,
227 reposetup() should look like
227 reposetup() should look like
228
228
229 def reposetup(ui, repo):
229 def reposetup(ui, repo):
230 class myrepo(repo.__class__):
230 class myrepo(repo.__class__):
231 def whatever(self, *args, **kwargs):
231 def whatever(self, *args, **kwargs):
232 [...extension stuff...]
232 [...extension stuff...]
233 super(myrepo, self).whatever(*args, **kwargs)
233 super(myrepo, self).whatever(*args, **kwargs)
234 [...extension stuff...]
234 [...extension stuff...]
235
235
236 repo.__class__ = myrepo
236 repo.__class__ = myrepo
237
237
238 In general, combining wrapfunction() with subclassing does not
238 In general, combining wrapfunction() with subclassing does not
239 work. Since you cannot control what other extensions are loaded by
239 work. Since you cannot control what other extensions are loaded by
240 your end users, you should play nicely with others by using the
240 your end users, you should play nicely with others by using the
241 subclass trick.
241 subclass trick.
242 '''
242 '''
243 assert callable(wrapper)
243 assert callable(wrapper)
244 def wrap(*args, **kwargs):
244 def wrap(*args, **kwargs):
245 return wrapper(origfn, *args, **kwargs)
245 return wrapper(origfn, *args, **kwargs)
246
246
247 origfn = getattr(container, funcname)
247 origfn = getattr(container, funcname)
248 assert callable(origfn)
248 assert callable(origfn)
249 setattr(container, funcname, wrap)
249 setattr(container, funcname, wrap)
250 return origfn
250 return origfn
251
251
252 def _disabledpaths(strip_init=False):
252 def _disabledpaths(strip_init=False):
253 '''find paths of disabled extensions. returns a dict of {name: path}
253 '''find paths of disabled extensions. returns a dict of {name: path}
254 removes /__init__.py from packages if strip_init is True'''
254 removes /__init__.py from packages if strip_init is True'''
255 import hgext
255 import hgext
256 extpath = os.path.dirname(os.path.abspath(hgext.__file__))
256 extpath = os.path.dirname(os.path.abspath(hgext.__file__))
257 try: # might not be a filesystem path
257 try: # might not be a filesystem path
258 files = os.listdir(extpath)
258 files = os.listdir(extpath)
259 except OSError:
259 except OSError:
260 return {}
260 return {}
261
261
262 exts = {}
262 exts = {}
263 for e in files:
263 for e in files:
264 if e.endswith('.py'):
264 if e.endswith('.py'):
265 name = e.rsplit('.', 1)[0]
265 name = e.rsplit('.', 1)[0]
266 path = os.path.join(extpath, e)
266 path = os.path.join(extpath, e)
267 else:
267 else:
268 name = e
268 name = e
269 path = os.path.join(extpath, e, '__init__.py')
269 path = os.path.join(extpath, e, '__init__.py')
270 if not os.path.exists(path):
270 if not os.path.exists(path):
271 continue
271 continue
272 if strip_init:
272 if strip_init:
273 path = os.path.dirname(path)
273 path = os.path.dirname(path)
274 if name in exts or name in _order or name == '__init__':
274 if name in exts or name in _order or name == '__init__':
275 continue
275 continue
276 exts[name] = path
276 exts[name] = path
277 return exts
277 return exts
278
278
279 def _moduledoc(file):
279 def _moduledoc(file):
280 '''return the top-level python documentation for the given file
280 '''return the top-level python documentation for the given file
281
281
282 Loosely inspired by pydoc.source_synopsis(), but rewritten to
282 Loosely inspired by pydoc.source_synopsis(), but rewritten to
283 handle triple quotes and to return the whole text instead of just
283 handle triple quotes and to return the whole text instead of just
284 the synopsis'''
284 the synopsis'''
285 result = []
285 result = []
286
286
287 line = file.readline()
287 line = file.readline()
288 while line[:1] == '#' or not line.strip():
288 while line[:1] == '#' or not line.strip():
289 line = file.readline()
289 line = file.readline()
290 if not line:
290 if not line:
291 break
291 break
292
292
293 start = line[:3]
293 start = line[:3]
294 if start == '"""' or start == "'''":
294 if start == '"""' or start == "'''":
295 line = line[3:]
295 line = line[3:]
296 while line:
296 while line:
297 if line.rstrip().endswith(start):
297 if line.rstrip().endswith(start):
298 line = line.split(start)[0]
298 line = line.split(start)[0]
299 if line:
299 if line:
300 result.append(line)
300 result.append(line)
301 break
301 break
302 elif not line:
302 elif not line:
303 return None # unmatched delimiter
303 return None # unmatched delimiter
304 result.append(line)
304 result.append(line)
305 line = file.readline()
305 line = file.readline()
306 else:
306 else:
307 return None
307 return None
308
308
309 return ''.join(result)
309 return ''.join(result)
310
310
311 def _disabledhelp(path):
311 def _disabledhelp(path):
312 '''retrieve help synopsis of a disabled extension (without importing)'''
312 '''retrieve help synopsis of a disabled extension (without importing)'''
313 try:
313 try:
314 file = open(path)
314 file = open(path)
315 except IOError:
315 except IOError:
316 return
316 return
317 else:
317 else:
318 doc = _moduledoc(file)
318 doc = _moduledoc(file)
319 file.close()
319 file.close()
320
320
321 if doc: # extracting localized synopsis
321 if doc: # extracting localized synopsis
322 return gettext(doc).splitlines()[0]
322 return gettext(doc).splitlines()[0]
323 else:
323 else:
324 return _('(no help text available)')
324 return _('(no help text available)')
325
325
326 def disabled():
326 def disabled():
327 '''find disabled extensions from hgext. returns a dict of {name: desc}'''
327 '''find disabled extensions from hgext. returns a dict of {name: desc}'''
328 try:
328 try:
329 from hgext import __index__
329 from hgext import __index__
330 return dict((name, gettext(desc))
330 return dict((name, gettext(desc))
331 for name, desc in __index__.docs.iteritems()
331 for name, desc in __index__.docs.iteritems()
332 if name not in _order)
332 if name not in _order)
333 except (ImportError, AttributeError):
333 except (ImportError, AttributeError):
334 pass
334 pass
335
335
336 paths = _disabledpaths()
336 paths = _disabledpaths()
337 if not paths:
337 if not paths:
338 return {}
338 return {}
339
339
340 exts = {}
340 exts = {}
341 for name, path in paths.iteritems():
341 for name, path in paths.iteritems():
342 doc = _disabledhelp(path)
342 doc = _disabledhelp(path)
343 if doc:
343 if doc:
344 exts[name] = doc
344 exts[name] = doc
345
345
346 return exts
346 return exts
347
347
348 def disabledext(name):
348 def disabledext(name):
349 '''find a specific disabled extension from hgext. returns desc'''
349 '''find a specific disabled extension from hgext. returns desc'''
350 try:
350 try:
351 from hgext import __index__
351 from hgext import __index__
352 if name in _order: # enabled
352 if name in _order: # enabled
353 return
353 return
354 else:
354 else:
355 return gettext(__index__.docs.get(name))
355 return gettext(__index__.docs.get(name))
356 except (ImportError, AttributeError):
356 except (ImportError, AttributeError):
357 pass
357 pass
358
358
359 paths = _disabledpaths()
359 paths = _disabledpaths()
360 if name in paths:
360 if name in paths:
361 return _disabledhelp(paths[name])
361 return _disabledhelp(paths[name])
362
362
363 def disabledcmd(ui, cmd, strict=False):
363 def disabledcmd(ui, cmd, strict=False):
364 '''import disabled extensions until cmd is found.
364 '''import disabled extensions until cmd is found.
365 returns (cmdname, extname, module)'''
365 returns (cmdname, extname, module)'''
366
366
367 paths = _disabledpaths(strip_init=True)
367 paths = _disabledpaths(strip_init=True)
368 if not paths:
368 if not paths:
369 raise error.UnknownCommand(cmd)
369 raise error.UnknownCommand(cmd)
370
370
371 def findcmd(cmd, name, path):
371 def findcmd(cmd, name, path):
372 try:
372 try:
373 mod = loadpath(path, 'hgext.%s' % name)
373 mod = loadpath(path, 'hgext.%s' % name)
374 except Exception:
374 except Exception:
375 return
375 return
376 try:
376 try:
377 aliases, entry = cmdutil.findcmd(cmd,
377 aliases, entry = cmdutil.findcmd(cmd,
378 getattr(mod, 'cmdtable', {}), strict)
378 getattr(mod, 'cmdtable', {}), strict)
379 except (error.AmbiguousCommand, error.UnknownCommand):
379 except (error.AmbiguousCommand, error.UnknownCommand):
380 return
380 return
381 except Exception:
381 except Exception:
382 ui.warn(_('warning: error finding commands in %s\n') % path)
382 ui.warn(_('warning: error finding commands in %s\n') % path)
383 ui.traceback()
383 ui.traceback()
384 return
384 return
385 for c in aliases:
385 for c in aliases:
386 if c.startswith(cmd):
386 if c.startswith(cmd):
387 cmd = c
387 cmd = c
388 break
388 break
389 else:
389 else:
390 cmd = aliases[0]
390 cmd = aliases[0]
391 return (cmd, name, mod)
391 return (cmd, name, mod)
392
392
393 ext = None
393 ext = None
394 # first, search for an extension with the same name as the command
394 # first, search for an extension with the same name as the command
395 path = paths.pop(cmd, None)
395 path = paths.pop(cmd, None)
396 if path:
396 if path:
397 ext = findcmd(cmd, cmd, path)
397 ext = findcmd(cmd, cmd, path)
398 if not ext:
398 if not ext:
399 # otherwise, interrogate each extension until there's a match
399 # otherwise, interrogate each extension until there's a match
400 for name, path in paths.iteritems():
400 for name, path in paths.iteritems():
401 ext = findcmd(cmd, name, path)
401 ext = findcmd(cmd, name, path)
402 if ext:
402 if ext:
403 break
403 break
404 if ext and 'DEPRECATED' not in ext.__doc__:
404 if ext and 'DEPRECATED' not in ext.__doc__:
405 return ext
405 return ext
406
406
407 raise error.UnknownCommand(cmd)
407 raise error.UnknownCommand(cmd)
408
408
409 def enabled(shortname=True):
409 def enabled(shortname=True):
410 '''return a dict of {name: desc} of extensions'''
410 '''return a dict of {name: desc} of extensions'''
411 exts = {}
411 exts = {}
412 for ename, ext in extensions():
412 for ename, ext in extensions():
413 doc = (gettext(ext.__doc__) or _('(no help text available)'))
413 doc = (gettext(ext.__doc__) or _('(no help text available)'))
414 if shortname:
414 if shortname:
415 ename = ename.split('.')[-1]
415 ename = ename.split('.')[-1]
416 exts[ename] = doc.splitlines()[0].strip()
416 exts[ename] = doc.splitlines()[0].strip()
417
417
418 return exts
418 return exts
419
419
420 def moduleversion(module):
420 def moduleversion(module):
421 '''return version information from given module as a string'''
421 '''return version information from given module as a string'''
422 if (util.safehasattr(module, 'getversion')
422 if (util.safehasattr(module, 'getversion')
423 and callable(module.getversion)):
423 and callable(module.getversion)):
424 version = module.getversion()
424 version = module.getversion()
425 elif util.safehasattr(module, '__version__'):
425 elif util.safehasattr(module, '__version__'):
426 version = module.__version__
426 version = module.__version__
427 else:
427 else:
428 version = ''
428 version = ''
429 if isinstance(version, (list, tuple)):
429 if isinstance(version, (list, tuple)):
430 version = '.'.join(str(o) for o in version)
430 version = '.'.join(str(o) for o in version)
431 return version
431 return version
General Comments 0
You need to be logged in to leave comments. Login now