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