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