##// END OF EJS Templates
wrapfunction: deprecates calling `wrappedfunction` with bytes...
marmoute -
r51693:94506fc1 default
parent child Browse files
Show More
@@ -1,987 +1,991 b''
1 1 # extensions.py - extension handling for mercurial
2 2 #
3 3 # Copyright 2005-2007 Olivia Mackall <olivia@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
9 9 import ast
10 10 import collections
11 11 import functools
12 12 import imp
13 13 import inspect
14 14 import os
15 15
16 16 from .i18n import (
17 17 _,
18 18 gettext,
19 19 )
20 20 from .pycompat import (
21 21 getattr,
22 22 open,
23 23 setattr,
24 24 )
25 25
26 26 from . import (
27 27 cmdutil,
28 28 configitems,
29 29 error,
30 30 pycompat,
31 31 util,
32 32 )
33 33
34 34 from .utils import stringutil
35 35
36 36 _extensions = {}
37 37 _disabledextensions = {}
38 38 _aftercallbacks = {}
39 39 _order = []
40 40 _builtin = {
41 41 b'hbisect',
42 42 b'bookmarks',
43 43 b'color',
44 44 b'parentrevspec',
45 45 b'progress',
46 46 b'interhg',
47 47 b'inotify',
48 48 b'hgcia',
49 49 b'shelve',
50 50 }
51 51
52 52
53 53 def extensions(ui=None):
54 54 if ui:
55 55
56 56 def enabled(name):
57 57 for format in [b'%s', b'hgext.%s']:
58 58 conf = ui.config(b'extensions', format % name)
59 59 if conf is not None and not conf.startswith(b'!'):
60 60 return True
61 61
62 62 else:
63 63 enabled = lambda name: True
64 64 for name in _order:
65 65 module = _extensions[name]
66 66 if module and enabled(name):
67 67 yield name, module
68 68
69 69
70 70 def find(name):
71 71 '''return module with given extension name'''
72 72 mod = None
73 73 try:
74 74 mod = _extensions[name]
75 75 except KeyError:
76 76 for k, v in _extensions.items():
77 77 if k.endswith(b'.' + name) or k.endswith(b'/' + name):
78 78 mod = v
79 79 break
80 80 if not mod:
81 81 raise KeyError(name)
82 82 return mod
83 83
84 84
85 85 def loadpath(path, module_name):
86 86 module_name = module_name.replace(b'.', b'_')
87 87 path = util.normpath(util.expandpath(path))
88 88 module_name = pycompat.fsdecode(module_name)
89 89 path = pycompat.fsdecode(path)
90 90 if os.path.isdir(path):
91 91 # module/__init__.py style
92 92 d, f = os.path.split(path)
93 93 fd, fpath, desc = imp.find_module(f, [d])
94 94 # When https://github.com/python/typeshed/issues/3466 is fixed
95 95 # and in a pytype release we can drop this disable.
96 96 return imp.load_module(
97 97 module_name, fd, fpath, desc # pytype: disable=wrong-arg-types
98 98 )
99 99 else:
100 100 try:
101 101 return imp.load_source(module_name, path)
102 102 except IOError as exc:
103 103 if not exc.filename:
104 104 exc.filename = path # python does not fill this
105 105 raise
106 106
107 107
108 108 def _importh(name):
109 109 """import and return the <name> module"""
110 110 mod = __import__(pycompat.sysstr(name))
111 111 components = name.split(b'.')
112 112 for comp in components[1:]:
113 113 mod = getattr(mod, comp)
114 114 return mod
115 115
116 116
117 117 def _importext(name, path=None, reportfunc=None):
118 118 if path:
119 119 # the module will be loaded in sys.modules
120 120 # choose an unique name so that it doesn't
121 121 # conflicts with other modules
122 122 mod = loadpath(path, b'hgext.%s' % name)
123 123 else:
124 124 try:
125 125 mod = _importh(b"hgext.%s" % name)
126 126 except ImportError as err:
127 127 if reportfunc:
128 128 reportfunc(err, b"hgext.%s" % name, b"hgext3rd.%s" % name)
129 129 try:
130 130 mod = _importh(b"hgext3rd.%s" % name)
131 131 except ImportError as err:
132 132 if reportfunc:
133 133 reportfunc(err, b"hgext3rd.%s" % name, name)
134 134 mod = _importh(name)
135 135 return mod
136 136
137 137
138 138 def _reportimporterror(ui, err, failed, next):
139 139 # note: this ui.log happens before --debug is processed,
140 140 # Use --config ui.debug=1 to see them.
141 141 ui.log(
142 142 b'extension',
143 143 b' - could not import %s (%s): trying %s\n',
144 144 failed,
145 145 stringutil.forcebytestr(err),
146 146 next,
147 147 )
148 148 if ui.debugflag and ui.configbool(b'devel', b'debug.extensions'):
149 149 ui.traceback()
150 150
151 151
152 152 def _rejectunicode(name, xs):
153 153 if isinstance(xs, (list, set, tuple)):
154 154 for x in xs:
155 155 _rejectunicode(name, x)
156 156 elif isinstance(xs, dict):
157 157 for k, v in xs.items():
158 158 _rejectunicode(name, k)
159 159 _rejectunicode(b'%s.%s' % (name, stringutil.forcebytestr(k)), v)
160 160 elif isinstance(xs, type(u'')):
161 161 raise error.ProgrammingError(
162 162 b"unicode %r found in %s" % (xs, name),
163 163 hint=b"use b'' to make it byte string",
164 164 )
165 165
166 166
167 167 # attributes set by registrar.command
168 168 _cmdfuncattrs = (b'norepo', b'optionalrepo', b'inferrepo')
169 169
170 170
171 171 def _validatecmdtable(ui, cmdtable):
172 172 """Check if extension commands have required attributes"""
173 173 for c, e in cmdtable.items():
174 174 f = e[0]
175 175 missing = [a for a in _cmdfuncattrs if not util.safehasattr(f, a)]
176 176 if not missing:
177 177 continue
178 178 raise error.ProgrammingError(
179 179 b'missing attributes: %s' % b', '.join(missing),
180 180 hint=b"use @command decorator to register '%s'" % c,
181 181 )
182 182
183 183
184 184 def _validatetables(ui, mod):
185 185 """Sanity check for loadable tables provided by extension module"""
186 186 for t in [b'cmdtable', b'colortable', b'configtable']:
187 187 _rejectunicode(t, getattr(mod, t, {}))
188 188 for t in [
189 189 b'filesetpredicate',
190 190 b'internalmerge',
191 191 b'revsetpredicate',
192 192 b'templatefilter',
193 193 b'templatefunc',
194 194 b'templatekeyword',
195 195 ]:
196 196 o = getattr(mod, t, None)
197 197 if o:
198 198 _rejectunicode(t, o._table)
199 199 _validatecmdtable(ui, getattr(mod, 'cmdtable', {}))
200 200
201 201
202 202 def load(ui, name, path, loadingtime=None):
203 203 if name.startswith(b'hgext.') or name.startswith(b'hgext/'):
204 204 shortname = name[6:]
205 205 else:
206 206 shortname = name
207 207 if shortname in _builtin:
208 208 return None
209 209 if shortname in _extensions:
210 210 return _extensions[shortname]
211 211 ui.log(b'extension', b' - loading extension: %s\n', shortname)
212 212 _extensions[shortname] = None
213 213 with util.timedcm('load extension %s', shortname) as stats:
214 214 mod = _importext(name, path, bind(_reportimporterror, ui))
215 215 ui.log(b'extension', b' > %s extension loaded in %s\n', shortname, stats)
216 216 if loadingtime is not None:
217 217 loadingtime[shortname] += stats.elapsed
218 218
219 219 # Before we do anything with the extension, check against minimum stated
220 220 # compatibility. This gives extension authors a mechanism to have their
221 221 # extensions short circuit when loaded with a known incompatible version
222 222 # of Mercurial.
223 223 minver = getattr(mod, 'minimumhgversion', None)
224 224 if minver:
225 225 curver = util.versiontuple(n=2)
226 226 extmin = util.versiontuple(stringutil.forcebytestr(minver), 2)
227 227
228 228 if None in extmin:
229 229 extmin = (extmin[0] or 0, extmin[1] or 0)
230 230
231 231 if None in curver or extmin > curver:
232 232 msg = _(
233 233 b'(third party extension %s requires version %s or newer '
234 234 b'of Mercurial (current: %s); disabling)\n'
235 235 )
236 236 ui.warn(msg % (shortname, minver, util.version()))
237 237 return
238 238 ui.log(b'extension', b' - validating extension tables: %s\n', shortname)
239 239 _validatetables(ui, mod)
240 240
241 241 _extensions[shortname] = mod
242 242 _order.append(shortname)
243 243 ui.log(
244 244 b'extension', b' - invoking registered callbacks: %s\n', shortname
245 245 )
246 246 with util.timedcm('callbacks extension %s', shortname) as stats:
247 247 for fn in _aftercallbacks.get(shortname, []):
248 248 fn(loaded=True)
249 249 ui.log(b'extension', b' > callbacks completed in %s\n', stats)
250 250 return mod
251 251
252 252
253 253 def _runuisetup(name, ui):
254 254 uisetup = getattr(_extensions[name], 'uisetup', None)
255 255 if uisetup:
256 256 try:
257 257 uisetup(ui)
258 258 except Exception as inst:
259 259 ui.traceback(force=True)
260 260 msg = stringutil.forcebytestr(inst)
261 261 ui.warn(_(b"*** failed to set up extension %s: %s\n") % (name, msg))
262 262 return False
263 263 return True
264 264
265 265
266 266 def _runextsetup(name, ui):
267 267 extsetup = getattr(_extensions[name], 'extsetup', None)
268 268 if extsetup:
269 269 try:
270 270 extsetup(ui)
271 271 except Exception as inst:
272 272 ui.traceback(force=True)
273 273 msg = stringutil.forcebytestr(inst)
274 274 ui.warn(_(b"*** failed to set up extension %s: %s\n") % (name, msg))
275 275 return False
276 276 return True
277 277
278 278
279 279 def loadall(ui, whitelist=None):
280 280 loadingtime = collections.defaultdict(int)
281 281 result = ui.configitems(b"extensions")
282 282 if whitelist is not None:
283 283 result = [(k, v) for (k, v) in result if k in whitelist]
284 284 result = [(k, v) for (k, v) in result if b':' not in k]
285 285 newindex = len(_order)
286 286 ui.log(
287 287 b'extension',
288 288 b'loading %sextensions\n',
289 289 b'additional ' if newindex else b'',
290 290 )
291 291 ui.log(b'extension', b'- processing %d entries\n', len(result))
292 292 with util.timedcm('load all extensions') as stats:
293 293 default_sub_options = ui.configsuboptions(b"extensions", b"*")[1]
294 294
295 295 for (name, path) in result:
296 296 if path:
297 297 if path[0:1] == b'!':
298 298 if name not in _disabledextensions:
299 299 ui.log(
300 300 b'extension',
301 301 b' - skipping disabled extension: %s\n',
302 302 name,
303 303 )
304 304 _disabledextensions[name] = path[1:]
305 305 continue
306 306 try:
307 307 load(ui, name, path, loadingtime)
308 308 except Exception as inst:
309 309 msg = stringutil.forcebytestr(inst)
310 310 if path:
311 311 error_msg = _(
312 312 b'failed to import extension "%s" from %s: %s'
313 313 )
314 314 error_msg %= (name, path, msg)
315 315 else:
316 316 error_msg = _(b'failed to import extension "%s": %s')
317 317 error_msg %= (name, msg)
318 318
319 319 options = default_sub_options.copy()
320 320 ext_options = ui.configsuboptions(b"extensions", name)[1]
321 321 options.update(ext_options)
322 322 if stringutil.parsebool(options.get(b"required", b'no')):
323 323 hint = None
324 324 if isinstance(inst, error.Hint) and inst.hint:
325 325 hint = inst.hint
326 326 if hint is None:
327 327 hint = _(
328 328 b"loading of this extension was required, "
329 329 b"see `hg help config.extensions` for details"
330 330 )
331 331 raise error.Abort(error_msg, hint=hint)
332 332 else:
333 333 ui.warn((b"*** %s\n") % error_msg)
334 334 if isinstance(inst, error.Hint) and inst.hint:
335 335 ui.warn(_(b"*** (%s)\n") % inst.hint)
336 336 ui.traceback()
337 337
338 338 ui.log(
339 339 b'extension',
340 340 b'> loaded %d extensions, total time %s\n',
341 341 len(_order) - newindex,
342 342 stats,
343 343 )
344 344 # list of (objname, loadermod, loadername) tuple:
345 345 # - objname is the name of an object in extension module,
346 346 # from which extra information is loaded
347 347 # - loadermod is the module where loader is placed
348 348 # - loadername is the name of the function,
349 349 # which takes (ui, extensionname, extraobj) arguments
350 350 #
351 351 # This one is for the list of item that must be run before running any setup
352 352 earlyextraloaders = [
353 353 (b'configtable', configitems, b'loadconfigtable'),
354 354 ]
355 355
356 356 ui.log(b'extension', b'- loading configtable attributes\n')
357 357 _loadextra(ui, newindex, earlyextraloaders)
358 358
359 359 broken = set()
360 360 ui.log(b'extension', b'- executing uisetup hooks\n')
361 361 with util.timedcm('all uisetup') as alluisetupstats:
362 362 for name in _order[newindex:]:
363 363 ui.log(b'extension', b' - running uisetup for %s\n', name)
364 364 with util.timedcm('uisetup %s', name) as stats:
365 365 if not _runuisetup(name, ui):
366 366 ui.log(
367 367 b'extension',
368 368 b' - the %s extension uisetup failed\n',
369 369 name,
370 370 )
371 371 broken.add(name)
372 372 ui.log(b'extension', b' > uisetup for %s took %s\n', name, stats)
373 373 loadingtime[name] += stats.elapsed
374 374 ui.log(b'extension', b'> all uisetup took %s\n', alluisetupstats)
375 375
376 376 ui.log(b'extension', b'- executing extsetup hooks\n')
377 377 with util.timedcm('all extsetup') as allextetupstats:
378 378 for name in _order[newindex:]:
379 379 if name in broken:
380 380 continue
381 381 ui.log(b'extension', b' - running extsetup for %s\n', name)
382 382 with util.timedcm('extsetup %s', name) as stats:
383 383 if not _runextsetup(name, ui):
384 384 ui.log(
385 385 b'extension',
386 386 b' - the %s extension extsetup failed\n',
387 387 name,
388 388 )
389 389 broken.add(name)
390 390 ui.log(b'extension', b' > extsetup for %s took %s\n', name, stats)
391 391 loadingtime[name] += stats.elapsed
392 392 ui.log(b'extension', b'> all extsetup took %s\n', allextetupstats)
393 393
394 394 for name in broken:
395 395 ui.log(b'extension', b' - disabling broken %s extension\n', name)
396 396 _extensions[name] = None
397 397
398 398 # Call aftercallbacks that were never met.
399 399 ui.log(b'extension', b'- executing remaining aftercallbacks\n')
400 400 with util.timedcm('aftercallbacks') as stats:
401 401 for shortname in _aftercallbacks:
402 402 if shortname in _extensions:
403 403 continue
404 404
405 405 for fn in _aftercallbacks[shortname]:
406 406 ui.log(
407 407 b'extension',
408 408 b' - extension %s not loaded, notify callbacks\n',
409 409 shortname,
410 410 )
411 411 fn(loaded=False)
412 412 ui.log(b'extension', b'> remaining aftercallbacks completed in %s\n', stats)
413 413
414 414 # loadall() is called multiple times and lingering _aftercallbacks
415 415 # entries could result in double execution. See issue4646.
416 416 _aftercallbacks.clear()
417 417
418 418 # delay importing avoids cyclic dependency (especially commands)
419 419 from . import (
420 420 color,
421 421 commands,
422 422 filemerge,
423 423 fileset,
424 424 revset,
425 425 templatefilters,
426 426 templatefuncs,
427 427 templatekw,
428 428 )
429 429
430 430 # list of (objname, loadermod, loadername) tuple:
431 431 # - objname is the name of an object in extension module,
432 432 # from which extra information is loaded
433 433 # - loadermod is the module where loader is placed
434 434 # - loadername is the name of the function,
435 435 # which takes (ui, extensionname, extraobj) arguments
436 436 ui.log(b'extension', b'- loading extension registration objects\n')
437 437 extraloaders = [
438 438 (b'cmdtable', commands, b'loadcmdtable'),
439 439 (b'colortable', color, b'loadcolortable'),
440 440 (b'filesetpredicate', fileset, b'loadpredicate'),
441 441 (b'internalmerge', filemerge, b'loadinternalmerge'),
442 442 (b'revsetpredicate', revset, b'loadpredicate'),
443 443 (b'templatefilter', templatefilters, b'loadfilter'),
444 444 (b'templatefunc', templatefuncs, b'loadfunction'),
445 445 (b'templatekeyword', templatekw, b'loadkeyword'),
446 446 ]
447 447 with util.timedcm('load registration objects') as stats:
448 448 _loadextra(ui, newindex, extraloaders)
449 449 ui.log(
450 450 b'extension',
451 451 b'> extension registration object loading took %s\n',
452 452 stats,
453 453 )
454 454
455 455 # Report per extension loading time (except reposetup)
456 456 for name in sorted(loadingtime):
457 457 ui.log(
458 458 b'extension',
459 459 b'> extension %s take a total of %s to load\n',
460 460 name,
461 461 util.timecount(loadingtime[name]),
462 462 )
463 463
464 464 ui.log(b'extension', b'extension loading complete\n')
465 465
466 466
467 467 def _loadextra(ui, newindex, extraloaders):
468 468 for name in _order[newindex:]:
469 469 module = _extensions[name]
470 470 if not module:
471 471 continue # loading this module failed
472 472
473 473 for objname, loadermod, loadername in extraloaders:
474 474 extraobj = getattr(module, objname, None)
475 475 if extraobj is not None:
476 476 getattr(loadermod, loadername)(ui, name, extraobj)
477 477
478 478
479 479 def afterloaded(extension, callback):
480 480 """Run the specified function after a named extension is loaded.
481 481
482 482 If the named extension is already loaded, the callback will be called
483 483 immediately.
484 484
485 485 If the named extension never loads, the callback will be called after
486 486 all extensions have been loaded.
487 487
488 488 The callback receives the named argument ``loaded``, which is a boolean
489 489 indicating whether the dependent extension actually loaded.
490 490 """
491 491
492 492 if extension in _extensions:
493 493 # Report loaded as False if the extension is disabled
494 494 loaded = _extensions[extension] is not None
495 495 callback(loaded=loaded)
496 496 else:
497 497 _aftercallbacks.setdefault(extension, []).append(callback)
498 498
499 499
500 500 def populateui(ui):
501 501 """Run extension hooks on the given ui to populate additional members,
502 502 extend the class dynamically, etc.
503 503
504 504 This will be called after the configuration is loaded, and/or extensions
505 505 are loaded. In general, it's once per ui instance, but in command-server
506 506 and hgweb, this may be called more than once with the same ui.
507 507 """
508 508 for name, mod in extensions(ui):
509 509 hook = getattr(mod, 'uipopulate', None)
510 510 if not hook:
511 511 continue
512 512 try:
513 513 hook(ui)
514 514 except Exception as inst:
515 515 ui.traceback(force=True)
516 516 ui.warn(
517 517 _(b'*** failed to populate ui by extension %s: %s\n')
518 518 % (name, stringutil.forcebytestr(inst))
519 519 )
520 520
521 521
522 522 def bind(func, *args):
523 523 """Partial function application
524 524
525 525 Returns a new function that is the partial application of args and kwargs
526 526 to func. For example,
527 527
528 528 f(1, 2, bar=3) === bind(f, 1)(2, bar=3)"""
529 529 assert callable(func)
530 530
531 531 def closure(*a, **kw):
532 532 return func(*(args + a), **kw)
533 533
534 534 return closure
535 535
536 536
537 537 def _updatewrapper(wrap, origfn, unboundwrapper):
538 538 '''Copy and add some useful attributes to wrapper'''
539 539 try:
540 540 wrap.__name__ = origfn.__name__
541 541 except AttributeError:
542 542 pass
543 543 wrap.__module__ = getattr(origfn, '__module__')
544 544 wrap.__doc__ = getattr(origfn, '__doc__')
545 545 wrap.__dict__.update(getattr(origfn, '__dict__', {}))
546 546 wrap._origfunc = origfn
547 547 wrap._unboundwrapper = unboundwrapper
548 548
549 549
550 550 def wrapcommand(table, command, wrapper, synopsis=None, docstring=None):
551 551 '''Wrap the command named `command' in table
552 552
553 553 Replace command in the command table with wrapper. The wrapped command will
554 554 be inserted into the command table specified by the table argument.
555 555
556 556 The wrapper will be called like
557 557
558 558 wrapper(orig, *args, **kwargs)
559 559
560 560 where orig is the original (wrapped) function, and *args, **kwargs
561 561 are the arguments passed to it.
562 562
563 563 Optionally append to the command synopsis and docstring, used for help.
564 564 For example, if your extension wraps the ``bookmarks`` command to add the
565 565 flags ``--remote`` and ``--all`` you might call this function like so:
566 566
567 567 synopsis = ' [-a] [--remote]'
568 568 docstring = """
569 569
570 570 The ``remotenames`` extension adds the ``--remote`` and ``--all`` (``-a``)
571 571 flags to the bookmarks command. Either flag will show the remote bookmarks
572 572 known to the repository; ``--remote`` will also suppress the output of the
573 573 local bookmarks.
574 574 """
575 575
576 576 extensions.wrapcommand(commands.table, 'bookmarks', exbookmarks,
577 577 synopsis, docstring)
578 578 '''
579 579 assert callable(wrapper)
580 580 aliases, entry = cmdutil.findcmd(command, table)
581 581 for alias, e in table.items():
582 582 if e is entry:
583 583 key = alias
584 584 break
585 585
586 586 origfn = entry[0]
587 587 wrap = functools.partial(
588 588 util.checksignature(wrapper), util.checksignature(origfn)
589 589 )
590 590 _updatewrapper(wrap, origfn, wrapper)
591 591 if docstring is not None:
592 592 wrap.__doc__ += docstring
593 593
594 594 newentry = list(entry)
595 595 newentry[0] = wrap
596 596 if synopsis is not None:
597 597 newentry[2] += synopsis
598 598 table[key] = tuple(newentry)
599 599 return entry
600 600
601 601
602 602 def wrapfilecache(cls, propname, wrapper):
603 603 """Wraps a filecache property.
604 604
605 605 These can't be wrapped using the normal wrapfunction.
606 606 """
607 607 propname = pycompat.sysstr(propname)
608 608 assert callable(wrapper)
609 609 for currcls in cls.__mro__:
610 610 if propname in currcls.__dict__:
611 611 origfn = currcls.__dict__[propname].func
612 612 assert callable(origfn)
613 613
614 614 def wrap(*args, **kwargs):
615 615 return wrapper(origfn, *args, **kwargs)
616 616
617 617 currcls.__dict__[propname].func = wrap
618 618 break
619 619
620 620 if currcls is object:
621 621 raise AttributeError("type '%s' has no property '%s'" % (cls, propname))
622 622
623 623
624 624 class wrappedfunction:
625 625 '''context manager for temporarily wrapping a function'''
626 626
627 627 def __init__(self, container, funcname, wrapper):
628 628 assert callable(wrapper)
629 if not isinstance(funcname, str):
630 msg = b"pass wrappedfunction target name as `str`, not `bytes`"
631 util.nouideprecwarn(msg, b"6.6", stacklevel=2)
632 funcname = pycompat.sysstr(funcname)
629 633 self._container = container
630 634 self._funcname = funcname
631 635 self._wrapper = wrapper
632 636
633 637 def __enter__(self):
634 638 wrapfunction(self._container, self._funcname, self._wrapper)
635 639
636 640 def __exit__(self, exctype, excvalue, traceback):
637 641 unwrapfunction(self._container, self._funcname, self._wrapper)
638 642
639 643
640 644 def wrapfunction(container, funcname, wrapper):
641 645 """Wrap the function named funcname in container
642 646
643 647 Replace the funcname member in the given container with the specified
644 648 wrapper. The container is typically a module, class, or instance.
645 649
646 650 The wrapper will be called like
647 651
648 652 wrapper(orig, *args, **kwargs)
649 653
650 654 where orig is the original (wrapped) function, and *args, **kwargs
651 655 are the arguments passed to it.
652 656
653 657 Wrapping methods of the repository object is not recommended since
654 658 it conflicts with extensions that extend the repository by
655 659 subclassing. All extensions that need to extend methods of
656 660 localrepository should use this subclassing trick: namely,
657 661 reposetup() should look like
658 662
659 663 def reposetup(ui, repo):
660 664 class myrepo(repo.__class__):
661 665 def whatever(self, *args, **kwargs):
662 666 [...extension stuff...]
663 667 super(myrepo, self).whatever(*args, **kwargs)
664 668 [...extension stuff...]
665 669
666 670 repo.__class__ = myrepo
667 671
668 672 In general, combining wrapfunction() with subclassing does not
669 673 work. Since you cannot control what other extensions are loaded by
670 674 your end users, you should play nicely with others by using the
671 675 subclass trick.
672 676 """
673 677 assert callable(wrapper)
674 678
675 679 origfn = getattr(container, funcname)
676 680 assert callable(origfn)
677 681 if inspect.ismodule(container):
678 682 # origfn is not an instance or class method. "partial" can be used.
679 683 # "partial" won't insert a frame in traceback.
680 684 wrap = functools.partial(wrapper, origfn)
681 685 else:
682 686 # "partial" cannot be safely used. Emulate its effect by using "bind".
683 687 # The downside is one more frame in traceback.
684 688 wrap = bind(wrapper, origfn)
685 689 _updatewrapper(wrap, origfn, wrapper)
686 690 setattr(container, funcname, wrap)
687 691 return origfn
688 692
689 693
690 694 def unwrapfunction(container, funcname, wrapper=None):
691 695 """undo wrapfunction
692 696
693 697 If wrappers is None, undo the last wrap. Otherwise removes the wrapper
694 698 from the chain of wrappers.
695 699
696 700 Return the removed wrapper.
697 701 Raise IndexError if wrapper is None and nothing to unwrap; ValueError if
698 702 wrapper is not None but is not found in the wrapper chain.
699 703 """
700 704 chain = getwrapperchain(container, funcname)
701 705 origfn = chain.pop()
702 706 if wrapper is None:
703 707 wrapper = chain[0]
704 708 chain.remove(wrapper)
705 709 setattr(container, funcname, origfn)
706 710 for w in reversed(chain):
707 711 wrapfunction(container, funcname, w)
708 712 return wrapper
709 713
710 714
711 715 def getwrapperchain(container, funcname):
712 716 """get a chain of wrappers of a function
713 717
714 718 Return a list of functions: [newest wrapper, ..., oldest wrapper, origfunc]
715 719
716 720 The wrapper functions are the ones passed to wrapfunction, whose first
717 721 argument is origfunc.
718 722 """
719 723 result = []
720 724 fn = getattr(container, funcname)
721 725 while fn:
722 726 assert callable(fn)
723 727 result.append(getattr(fn, '_unboundwrapper', fn))
724 728 fn = getattr(fn, '_origfunc', None)
725 729 return result
726 730
727 731
728 732 def _disabledpaths():
729 733 '''find paths of disabled extensions. returns a dict of {name: path}'''
730 734 import hgext
731 735
732 736 exts = {}
733 737
734 738 # The hgext might not have a __file__ attribute (e.g. in PyOxidizer) and
735 739 # it might not be on a filesystem even if it does.
736 740 if util.safehasattr(hgext, '__file__'):
737 741 extpath = os.path.dirname(
738 742 util.abspath(pycompat.fsencode(hgext.__file__))
739 743 )
740 744 try:
741 745 files = os.listdir(extpath)
742 746 except OSError:
743 747 pass
744 748 else:
745 749 for e in files:
746 750 if e.endswith(b'.py'):
747 751 name = e.rsplit(b'.', 1)[0]
748 752 path = os.path.join(extpath, e)
749 753 else:
750 754 name = e
751 755 path = os.path.join(extpath, e, b'__init__.py')
752 756 if not os.path.exists(path):
753 757 continue
754 758 if name in exts or name in _order or name == b'__init__':
755 759 continue
756 760 exts[name] = path
757 761
758 762 for name, path in _disabledextensions.items():
759 763 # If no path was provided for a disabled extension (e.g. "color=!"),
760 764 # don't replace the path we already found by the scan above.
761 765 if path:
762 766 exts[name] = path
763 767 return exts
764 768
765 769
766 770 def _moduledoc(file):
767 771 """return the top-level python documentation for the given file
768 772
769 773 Loosely inspired by pydoc.source_synopsis(), but rewritten to
770 774 handle triple quotes and to return the whole text instead of just
771 775 the synopsis"""
772 776 result = []
773 777
774 778 line = file.readline()
775 779 while line[:1] == b'#' or not line.strip():
776 780 line = file.readline()
777 781 if not line:
778 782 break
779 783
780 784 start = line[:3]
781 785 if start == b'"""' or start == b"'''":
782 786 line = line[3:]
783 787 while line:
784 788 if line.rstrip().endswith(start):
785 789 line = line.split(start)[0]
786 790 if line:
787 791 result.append(line)
788 792 break
789 793 elif not line:
790 794 return None # unmatched delimiter
791 795 result.append(line)
792 796 line = file.readline()
793 797 else:
794 798 return None
795 799
796 800 return b''.join(result)
797 801
798 802
799 803 def _disabledhelp(path):
800 804 '''retrieve help synopsis of a disabled extension (without importing)'''
801 805 try:
802 806 with open(path, b'rb') as src:
803 807 doc = _moduledoc(src)
804 808 except IOError:
805 809 return
806 810
807 811 if doc: # extracting localized synopsis
808 812 return gettext(doc)
809 813 else:
810 814 return _(b'(no help text available)')
811 815
812 816
813 817 def disabled():
814 818 '''find disabled extensions from hgext. returns a dict of {name: desc}'''
815 819 try:
816 820 from hgext import __index__ # pytype: disable=import-error
817 821
818 822 return {
819 823 name: gettext(desc)
820 824 for name, desc in __index__.docs.items()
821 825 if name not in _order
822 826 }
823 827 except (ImportError, AttributeError):
824 828 pass
825 829
826 830 paths = _disabledpaths()
827 831 if not paths:
828 832 return {}
829 833
830 834 exts = {}
831 835 for name, path in paths.items():
832 836 doc = _disabledhelp(path)
833 837 if doc and name != b'__index__':
834 838 exts[name] = stringutil.firstline(doc)
835 839
836 840 return exts
837 841
838 842
839 843 def disabled_help(name):
840 844 """Obtain the full help text for a disabled extension, or None."""
841 845 paths = _disabledpaths()
842 846 if name in paths:
843 847 return _disabledhelp(paths[name])
844 848 else:
845 849 try:
846 850 import hgext
847 851 from hgext import __index__ # pytype: disable=import-error
848 852
849 853 # The extensions are filesystem based, so either an error occurred
850 854 # or all are enabled.
851 855 if util.safehasattr(hgext, '__file__'):
852 856 return
853 857
854 858 if name in _order: # enabled
855 859 return
856 860 else:
857 861 return gettext(__index__.docs.get(name))
858 862 except (ImportError, AttributeError):
859 863 pass
860 864
861 865
862 866 def _walkcommand(node):
863 867 """Scan @command() decorators in the tree starting at node"""
864 868 todo = collections.deque([node])
865 869 while todo:
866 870 node = todo.popleft()
867 871 if not isinstance(node, ast.FunctionDef):
868 872 todo.extend(ast.iter_child_nodes(node))
869 873 continue
870 874 for d in node.decorator_list:
871 875 if not isinstance(d, ast.Call):
872 876 continue
873 877 if not isinstance(d.func, ast.Name):
874 878 continue
875 879 if d.func.id != 'command':
876 880 continue
877 881 yield d
878 882
879 883
880 884 def _disabledcmdtable(path):
881 885 """Construct a dummy command table without loading the extension module
882 886
883 887 This may raise IOError or SyntaxError.
884 888 """
885 889 with open(path, b'rb') as src:
886 890 root = ast.parse(src.read(), path)
887 891 cmdtable = {}
888 892 for node in _walkcommand(root):
889 893 if not node.args:
890 894 continue
891 895 a = node.args[0]
892 896 if isinstance(a, ast.Str):
893 897 name = pycompat.sysbytes(a.s)
894 898 elif isinstance(a, ast.Bytes):
895 899 name = a.s
896 900 else:
897 901 continue
898 902 cmdtable[name] = (None, [], b'')
899 903 return cmdtable
900 904
901 905
902 906 def _finddisabledcmd(ui, cmd, name, path, strict):
903 907 try:
904 908 cmdtable = _disabledcmdtable(path)
905 909 except (IOError, SyntaxError):
906 910 return
907 911 try:
908 912 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
909 913 except (error.AmbiguousCommand, error.UnknownCommand):
910 914 return
911 915 for c in aliases:
912 916 if c.startswith(cmd):
913 917 cmd = c
914 918 break
915 919 else:
916 920 cmd = aliases[0]
917 921 doc = _disabledhelp(path)
918 922 return (cmd, name, doc)
919 923
920 924
921 925 def disabledcmd(ui, cmd, strict=False):
922 926 """find cmd from disabled extensions without importing.
923 927 returns (cmdname, extname, doc)"""
924 928
925 929 paths = _disabledpaths()
926 930 if not paths:
927 931 raise error.UnknownCommand(cmd)
928 932
929 933 ext = None
930 934 # first, search for an extension with the same name as the command
931 935 path = paths.pop(cmd, None)
932 936 if path:
933 937 ext = _finddisabledcmd(ui, cmd, cmd, path, strict=strict)
934 938 if not ext:
935 939 # otherwise, interrogate each extension until there's a match
936 940 for name, path in paths.items():
937 941 ext = _finddisabledcmd(ui, cmd, name, path, strict=strict)
938 942 if ext:
939 943 break
940 944 if ext:
941 945 return ext
942 946
943 947 raise error.UnknownCommand(cmd)
944 948
945 949
946 950 def enabled(shortname=True):
947 951 '''return a dict of {name: desc} of extensions'''
948 952 exts = {}
949 953 for ename, ext in extensions():
950 954 doc = gettext(ext.__doc__) or _(b'(no help text available)')
951 955 assert doc is not None # help pytype
952 956 if shortname:
953 957 ename = ename.split(b'.')[-1]
954 958 exts[ename] = stringutil.firstline(doc).strip()
955 959
956 960 return exts
957 961
958 962
959 963 def notloaded():
960 964 '''return short names of extensions that failed to load'''
961 965 return [name for name, mod in _extensions.items() if mod is None]
962 966
963 967
964 968 def moduleversion(module):
965 969 '''return version information from given module as a string'''
966 970 if util.safehasattr(module, b'getversion') and callable(module.getversion):
967 971 try:
968 972 version = module.getversion()
969 973 except Exception:
970 974 version = b'unknown'
971 975
972 976 elif util.safehasattr(module, b'__version__'):
973 977 version = module.__version__
974 978 else:
975 979 version = b''
976 980 if isinstance(version, (list, tuple)):
977 981 version = b'.'.join(pycompat.bytestr(o) for o in version)
978 982 else:
979 983 # version data should be bytes, but not all extensions are ported
980 984 # to py3.
981 985 version = stringutil.forcebytestr(version)
982 986 return version
983 987
984 988
985 989 def ismoduleinternal(module):
986 990 exttestedwith = getattr(module, 'testedwith', None)
987 991 return exttestedwith == b"ships-with-hg-core"
General Comments 0
You need to be logged in to leave comments. Login now