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