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