##// END OF EJS Templates
extension: check the command attributes using `sysstr`...
marmoute -
r51815:0e6cea0c default
parent child Browse files
Show More
@@ -1,1011 +1,1011
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 158 k = pycompat.sysstr(k)
159 159 _rejectunicode('%s.%s' % (name, k), v)
160 160 elif isinstance(xs, str):
161 161 raise error.ProgrammingError(
162 162 b"unicode %r found in %s" % (xs, stringutil.forcebytestr(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 _cmdfuncattrs = (b'norepo', b'optionalrepo', b'inferrepo')
168 _cmdfuncattrs = ('norepo', 'optionalrepo', '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 raise error.ProgrammingError(
179 b'missing attributes: %s' % b', '.join(missing),
180 hint=b"use @command decorator to register '%s'" % c,
181 )
178 msg = b'missing attributes: %s'
179 msg %= b', '.join([stringutil.forcebytestr(m) for m in missing])
180 hint = b"use @command decorator to register '%s'" % c
181 raise error.ProgrammingError(msg, hint=hint)
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 ['cmdtable', 'colortable', 'configtable']:
187 187 _rejectunicode(t, getattr(mod, t, {}))
188 188 for t in [
189 189 'filesetpredicate',
190 190 'internalmerge',
191 191 'revsetpredicate',
192 192 'templatefilter',
193 193 'templatefunc',
194 194 '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 ('configtable', configitems, '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 ('cmdtable', commands, 'loadcmdtable'),
439 439 ('colortable', color, 'loadcolortable'),
440 440 ('filesetpredicate', fileset, 'loadpredicate'),
441 441 ('internalmerge', filemerge, 'loadinternalmerge'),
442 442 ('revsetpredicate', revset, 'loadpredicate'),
443 443 ('templatefilter', templatefilters, 'loadfilter'),
444 444 ('templatefunc', templatefuncs, 'loadfunction'),
445 445 ('templatekeyword', templatekw, '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 629 if not isinstance(funcname, str):
630 630 msg = b"pass wrappedfunction target name as `str`, not `bytes`"
631 631 util.nouideprecwarn(msg, b"6.6", stacklevel=2)
632 632 funcname = pycompat.sysstr(funcname)
633 633 self._container = container
634 634 self._funcname = funcname
635 635 self._wrapper = wrapper
636 636
637 637 def __enter__(self):
638 638 wrapfunction(self._container, self._funcname, self._wrapper)
639 639
640 640 def __exit__(self, exctype, excvalue, traceback):
641 641 unwrapfunction(self._container, self._funcname, self._wrapper)
642 642
643 643
644 644 def wrapfunction(container, funcname, wrapper):
645 645 """Wrap the function named funcname in container
646 646
647 647 Replace the funcname member in the given container with the specified
648 648 wrapper. The container is typically a module, class, or instance.
649 649
650 650 The wrapper will be called like
651 651
652 652 wrapper(orig, *args, **kwargs)
653 653
654 654 where orig is the original (wrapped) function, and *args, **kwargs
655 655 are the arguments passed to it.
656 656
657 657 Wrapping methods of the repository object is not recommended since
658 658 it conflicts with extensions that extend the repository by
659 659 subclassing. All extensions that need to extend methods of
660 660 localrepository should use this subclassing trick: namely,
661 661 reposetup() should look like
662 662
663 663 def reposetup(ui, repo):
664 664 class myrepo(repo.__class__):
665 665 def whatever(self, *args, **kwargs):
666 666 [...extension stuff...]
667 667 super(myrepo, self).whatever(*args, **kwargs)
668 668 [...extension stuff...]
669 669
670 670 repo.__class__ = myrepo
671 671
672 672 In general, combining wrapfunction() with subclassing does not
673 673 work. Since you cannot control what other extensions are loaded by
674 674 your end users, you should play nicely with others by using the
675 675 subclass trick.
676 676 """
677 677 assert callable(wrapper)
678 678
679 679 if not isinstance(funcname, str):
680 680 msg = b"pass wrapfunction target name as `str`, not `bytes`"
681 681 util.nouideprecwarn(msg, b"6.6", stacklevel=2)
682 682 funcname = pycompat.sysstr(funcname)
683 683
684 684 origfn = getattr(container, funcname)
685 685 assert callable(origfn)
686 686 if inspect.ismodule(container):
687 687 # origfn is not an instance or class method. "partial" can be used.
688 688 # "partial" won't insert a frame in traceback.
689 689 wrap = functools.partial(wrapper, origfn)
690 690 else:
691 691 # "partial" cannot be safely used. Emulate its effect by using "bind".
692 692 # The downside is one more frame in traceback.
693 693 wrap = bind(wrapper, origfn)
694 694 _updatewrapper(wrap, origfn, wrapper)
695 695 setattr(container, funcname, wrap)
696 696 return origfn
697 697
698 698
699 699 def unwrapfunction(container, funcname, wrapper=None):
700 700 """undo wrapfunction
701 701
702 702 If wrappers is None, undo the last wrap. Otherwise removes the wrapper
703 703 from the chain of wrappers.
704 704
705 705 Return the removed wrapper.
706 706 Raise IndexError if wrapper is None and nothing to unwrap; ValueError if
707 707 wrapper is not None but is not found in the wrapper chain.
708 708 """
709 709 chain = getwrapperchain(container, funcname)
710 710 origfn = chain.pop()
711 711 if wrapper is None:
712 712 wrapper = chain[0]
713 713 chain.remove(wrapper)
714 714 setattr(container, funcname, origfn)
715 715 for w in reversed(chain):
716 716 wrapfunction(container, funcname, w)
717 717 return wrapper
718 718
719 719
720 720 def getwrapperchain(container, funcname):
721 721 """get a chain of wrappers of a function
722 722
723 723 Return a list of functions: [newest wrapper, ..., oldest wrapper, origfunc]
724 724
725 725 The wrapper functions are the ones passed to wrapfunction, whose first
726 726 argument is origfunc.
727 727 """
728 728 result = []
729 729 fn = getattr(container, funcname)
730 730 while fn:
731 731 assert callable(fn)
732 732 result.append(getattr(fn, '_unboundwrapper', fn))
733 733 fn = getattr(fn, '_origfunc', None)
734 734 return result
735 735
736 736
737 737 def _disabledpaths():
738 738 '''find paths of disabled extensions. returns a dict of {name: path}'''
739 739 import hgext
740 740
741 741 exts = {}
742 742
743 743 # The hgext might not have a __file__ attribute (e.g. in PyOxidizer) and
744 744 # it might not be on a filesystem even if it does.
745 745 if util.safehasattr(hgext, '__file__'):
746 746 extpath = os.path.dirname(
747 747 util.abspath(pycompat.fsencode(hgext.__file__))
748 748 )
749 749 try:
750 750 files = os.listdir(extpath)
751 751 except OSError:
752 752 pass
753 753 else:
754 754 for e in files:
755 755 if e.endswith(b'.py'):
756 756 name = e.rsplit(b'.', 1)[0]
757 757 path = os.path.join(extpath, e)
758 758 else:
759 759 name = e
760 760 path = os.path.join(extpath, e, b'__init__.py')
761 761 if not os.path.exists(path):
762 762 continue
763 763 if name in exts or name in _order or name == b'__init__':
764 764 continue
765 765 exts[name] = path
766 766
767 767 for name, path in _disabledextensions.items():
768 768 # If no path was provided for a disabled extension (e.g. "color=!"),
769 769 # don't replace the path we already found by the scan above.
770 770 if path:
771 771 exts[name] = path
772 772 return exts
773 773
774 774
775 775 def _moduledoc(file):
776 776 """return the top-level python documentation for the given file
777 777
778 778 Loosely inspired by pydoc.source_synopsis(), but rewritten to
779 779 handle triple quotes and to return the whole text instead of just
780 780 the synopsis"""
781 781 result = []
782 782
783 783 line = file.readline()
784 784 while line[:1] == b'#' or not line.strip():
785 785 line = file.readline()
786 786 if not line:
787 787 break
788 788
789 789 start = line[:3]
790 790 if start == b'"""' or start == b"'''":
791 791 line = line[3:]
792 792 while line:
793 793 if line.rstrip().endswith(start):
794 794 line = line.split(start)[0]
795 795 if line:
796 796 result.append(line)
797 797 break
798 798 elif not line:
799 799 return None # unmatched delimiter
800 800 result.append(line)
801 801 line = file.readline()
802 802 else:
803 803 return None
804 804
805 805 return b''.join(result)
806 806
807 807
808 808 def _disabledhelp(path):
809 809 '''retrieve help synopsis of a disabled extension (without importing)'''
810 810 try:
811 811 with open(path, b'rb') as src:
812 812 doc = _moduledoc(src)
813 813 except IOError:
814 814 return
815 815
816 816 if doc: # extracting localized synopsis
817 817 return gettext(doc)
818 818 else:
819 819 return _(b'(no help text available)')
820 820
821 821
822 822 def disabled():
823 823 '''find disabled extensions from hgext. returns a dict of {name: desc}'''
824 824 try:
825 825 from hgext import __index__ # pytype: disable=import-error
826 826
827 827 return {
828 828 name: gettext(desc)
829 829 for name, desc in __index__.docs.items()
830 830 if name not in _order
831 831 }
832 832 except (ImportError, AttributeError):
833 833 pass
834 834
835 835 paths = _disabledpaths()
836 836 if not paths:
837 837 return {}
838 838
839 839 exts = {}
840 840 for name, path in paths.items():
841 841 doc = _disabledhelp(path)
842 842 if doc and name != b'__index__':
843 843 exts[name] = stringutil.firstline(doc)
844 844
845 845 return exts
846 846
847 847
848 848 def disabled_help(name):
849 849 """Obtain the full help text for a disabled extension, or None."""
850 850 paths = _disabledpaths()
851 851 if name in paths:
852 852 return _disabledhelp(paths[name])
853 853 else:
854 854 try:
855 855 import hgext
856 856 from hgext import __index__ # pytype: disable=import-error
857 857
858 858 # The extensions are filesystem based, so either an error occurred
859 859 # or all are enabled.
860 860 if util.safehasattr(hgext, '__file__'):
861 861 return
862 862
863 863 if name in _order: # enabled
864 864 return
865 865 else:
866 866 return gettext(__index__.docs.get(name))
867 867 except (ImportError, AttributeError):
868 868 pass
869 869
870 870
871 871 def _walkcommand(node):
872 872 """Scan @command() decorators in the tree starting at node"""
873 873 todo = collections.deque([node])
874 874 while todo:
875 875 node = todo.popleft()
876 876 if not isinstance(node, ast.FunctionDef):
877 877 todo.extend(ast.iter_child_nodes(node))
878 878 continue
879 879 for d in node.decorator_list:
880 880 if not isinstance(d, ast.Call):
881 881 continue
882 882 if not isinstance(d.func, ast.Name):
883 883 continue
884 884 if d.func.id != 'command':
885 885 continue
886 886 yield d
887 887
888 888
889 889 def _disabledcmdtable(path):
890 890 """Construct a dummy command table without loading the extension module
891 891
892 892 This may raise IOError or SyntaxError.
893 893 """
894 894 with open(path, b'rb') as src:
895 895 root = ast.parse(src.read(), path)
896 896 cmdtable = {}
897 897
898 898 # Python 3.12 started removing Bytes and Str and deprecate harder
899 899 use_constant = 'Bytes' not in vars(ast)
900 900
901 901 for node in _walkcommand(root):
902 902 if not node.args:
903 903 continue
904 904 a = node.args[0]
905 905 if use_constant: # Valid since Python 3.8
906 906 if isinstance(a, ast.Constant):
907 907 if isinstance(a.value, str):
908 908 name = pycompat.sysbytes(a.value)
909 909 elif isinstance(a.value, bytes):
910 910 name = a.value
911 911 else:
912 912 continue
913 913 else:
914 914 continue
915 915 else: # Valid until 3.11
916 916 if isinstance(a, ast.Str):
917 917 name = pycompat.sysbytes(a.s)
918 918 elif isinstance(a, ast.Bytes):
919 919 name = a.s
920 920 else:
921 921 continue
922 922 cmdtable[name] = (None, [], b'')
923 923 return cmdtable
924 924
925 925
926 926 def _finddisabledcmd(ui, cmd, name, path, strict):
927 927 try:
928 928 cmdtable = _disabledcmdtable(path)
929 929 except (IOError, SyntaxError):
930 930 return
931 931 try:
932 932 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
933 933 except (error.AmbiguousCommand, error.UnknownCommand):
934 934 return
935 935 for c in aliases:
936 936 if c.startswith(cmd):
937 937 cmd = c
938 938 break
939 939 else:
940 940 cmd = aliases[0]
941 941 doc = _disabledhelp(path)
942 942 return (cmd, name, doc)
943 943
944 944
945 945 def disabledcmd(ui, cmd, strict=False):
946 946 """find cmd from disabled extensions without importing.
947 947 returns (cmdname, extname, doc)"""
948 948
949 949 paths = _disabledpaths()
950 950 if not paths:
951 951 raise error.UnknownCommand(cmd)
952 952
953 953 ext = None
954 954 # first, search for an extension with the same name as the command
955 955 path = paths.pop(cmd, None)
956 956 if path:
957 957 ext = _finddisabledcmd(ui, cmd, cmd, path, strict=strict)
958 958 if not ext:
959 959 # otherwise, interrogate each extension until there's a match
960 960 for name, path in paths.items():
961 961 ext = _finddisabledcmd(ui, cmd, name, path, strict=strict)
962 962 if ext:
963 963 break
964 964 if ext:
965 965 return ext
966 966
967 967 raise error.UnknownCommand(cmd)
968 968
969 969
970 970 def enabled(shortname=True):
971 971 '''return a dict of {name: desc} of extensions'''
972 972 exts = {}
973 973 for ename, ext in extensions():
974 974 doc = gettext(ext.__doc__) or _(b'(no help text available)')
975 975 assert doc is not None # help pytype
976 976 if shortname:
977 977 ename = ename.split(b'.')[-1]
978 978 exts[ename] = stringutil.firstline(doc).strip()
979 979
980 980 return exts
981 981
982 982
983 983 def notloaded():
984 984 '''return short names of extensions that failed to load'''
985 985 return [name for name, mod in _extensions.items() if mod is None]
986 986
987 987
988 988 def moduleversion(module):
989 989 '''return version information from given module as a string'''
990 990 if util.safehasattr(module, 'getversion') and callable(module.getversion):
991 991 try:
992 992 version = module.getversion()
993 993 except Exception:
994 994 version = b'unknown'
995 995
996 996 elif util.safehasattr(module, '__version__'):
997 997 version = module.__version__
998 998 else:
999 999 version = b''
1000 1000 if isinstance(version, (list, tuple)):
1001 1001 version = b'.'.join(pycompat.bytestr(o) for o in version)
1002 1002 else:
1003 1003 # version data should be bytes, but not all extensions are ported
1004 1004 # to py3.
1005 1005 version = stringutil.forcebytestr(version)
1006 1006 return version
1007 1007
1008 1008
1009 1009 def ismoduleinternal(module):
1010 1010 exttestedwith = getattr(module, 'testedwith', None)
1011 1011 return exttestedwith == b"ships-with-hg-core"
General Comments 0
You need to be logged in to leave comments. Login now