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