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