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