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