comparison mercurial/extensions.py @ 38162:bdf344aea0ee

extensions: peek command table of disabled extensions without importing With chg where demandimport disabled, and if disk cache not warm, it took more than 5 seconds to get "unknown command" error when you typo a command name. This is horrible UX. The new implementation is less accurate than the original one as Python can do anything at import time and cmdtable may be imported from another module, but I think it's good enough. Note that the new implementation has to parse .py files, which is slightly slower than executing .pyc if demandimport is enabled.
author Yuya Nishihara <yuya@tcha.org>
date Thu, 03 May 2018 18:38:02 +0900
parents 9d44c71bd892
children b39958d6b81b
comparison
equal deleted inserted replaced
38161:aa10675c5dd6 38162:bdf344aea0ee
5 # This software may be used and distributed according to the terms of the 5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version. 6 # GNU General Public License version 2 or any later version.
7 7
8 from __future__ import absolute_import 8 from __future__ import absolute_import
9 9
10 import ast
11 import collections
10 import functools 12 import functools
11 import imp 13 import imp
12 import inspect 14 import inspect
13 import os 15 import os
14 16
653 655
654 paths = _disabledpaths() 656 paths = _disabledpaths()
655 if name in paths: 657 if name in paths:
656 return _disabledhelp(paths[name]) 658 return _disabledhelp(paths[name])
657 659
660 def _walkcommand(node):
661 """Scan @command() decorators in the tree starting at node"""
662 todo = collections.deque([node])
663 while todo:
664 node = todo.popleft()
665 if not isinstance(node, ast.FunctionDef):
666 todo.extend(ast.iter_child_nodes(node))
667 continue
668 for d in node.decorator_list:
669 if not isinstance(d, ast.Call):
670 continue
671 if not isinstance(d.func, ast.Name):
672 continue
673 if d.func.id != r'command':
674 continue
675 yield d
676
677 def _disabledcmdtable(path):
678 """Construct a dummy command table without loading the extension module
679
680 This may raise IOError or SyntaxError.
681 """
682 with open(path, 'rb') as src:
683 root = ast.parse(src.read(), path)
684 cmdtable = {}
685 for node in _walkcommand(root):
686 if not node.args:
687 continue
688 a = node.args[0]
689 if isinstance(a, ast.Str):
690 name = pycompat.sysbytes(a.s)
691 elif pycompat.ispy3 and isinstance(a, ast.Bytes):
692 name = a.s
693 else:
694 continue
695 cmdtable[name] = (None, [], b'')
696 return cmdtable
697
658 def _finddisabledcmd(ui, cmd, name, path, strict): 698 def _finddisabledcmd(ui, cmd, name, path, strict):
659 try: 699 try:
660 mod = loadpath(path, 'hgext.%s' % name) 700 cmdtable = _disabledcmdtable(path)
661 except Exception: 701 except (IOError, SyntaxError):
662 return 702 return
663 try: 703 try:
664 aliases, entry = cmdutil.findcmd(cmd, 704 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
665 getattr(mod, 'cmdtable', {}), strict)
666 except (error.AmbiguousCommand, error.UnknownCommand): 705 except (error.AmbiguousCommand, error.UnknownCommand):
667 return
668 except Exception:
669 ui.warn(_('warning: error finding commands in %s\n') % path)
670 ui.traceback()
671 return 706 return
672 for c in aliases: 707 for c in aliases:
673 if c.startswith(cmd): 708 if c.startswith(cmd):
674 cmd = c 709 cmd = c
675 break 710 break
676 else: 711 else:
677 cmd = aliases[0] 712 cmd = aliases[0]
678 doc = gettext(pycompat.getdoc(mod)) 713 doc = _disabledhelp(path)
679 return (cmd, name, doc) 714 return (cmd, name, doc)
680 715
681 def disabledcmd(ui, cmd, strict=False): 716 def disabledcmd(ui, cmd, strict=False):
682 '''import disabled extensions until cmd is found. 717 '''find cmd from disabled extensions without importing.
683 returns (cmdname, extname, doc)''' 718 returns (cmdname, extname, doc)'''
684 719
685 paths = _disabledpaths(strip_init=True) 720 paths = _disabledpaths()
686 if not paths: 721 if not paths:
687 raise error.UnknownCommand(cmd) 722 raise error.UnknownCommand(cmd)
688 723
689 ext = None 724 ext = None
690 # first, search for an extension with the same name as the command 725 # first, search for an extension with the same name as the command