Mercurial > public > mercurial-scm > hg
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 |