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