mercurial/extensions.py
changeset 38162 bdf344aea0ee
parent 38020 9d44c71bd892
child 38163 b39958d6b81b
--- a/mercurial/extensions.py	Thu Apr 26 23:00:19 2018 -0400
+++ b/mercurial/extensions.py	Thu May 03 18:38:02 2018 +0900
@@ -7,6 +7,8 @@
 
 from __future__ import absolute_import
 
+import ast
+import collections
 import functools
 import imp
 import inspect
@@ -655,34 +657,67 @@
     if name in paths:
         return _disabledhelp(paths[name])
 
+def _walkcommand(node):
+    """Scan @command() decorators in the tree starting at node"""
+    todo = collections.deque([node])
+    while todo:
+        node = todo.popleft()
+        if not isinstance(node, ast.FunctionDef):
+            todo.extend(ast.iter_child_nodes(node))
+            continue
+        for d in node.decorator_list:
+            if not isinstance(d, ast.Call):
+                continue
+            if not isinstance(d.func, ast.Name):
+                continue
+            if d.func.id != r'command':
+                continue
+            yield d
+
+def _disabledcmdtable(path):
+    """Construct a dummy command table without loading the extension module
+
+    This may raise IOError or SyntaxError.
+    """
+    with open(path, 'rb') as src:
+        root = ast.parse(src.read(), path)
+    cmdtable = {}
+    for node in _walkcommand(root):
+        if not node.args:
+            continue
+        a = node.args[0]
+        if isinstance(a, ast.Str):
+            name = pycompat.sysbytes(a.s)
+        elif pycompat.ispy3 and isinstance(a, ast.Bytes):
+            name = a.s
+        else:
+            continue
+        cmdtable[name] = (None, [], b'')
+    return cmdtable
+
 def _finddisabledcmd(ui, cmd, name, path, strict):
     try:
-        mod = loadpath(path, 'hgext.%s' % name)
-    except Exception:
+        cmdtable = _disabledcmdtable(path)
+    except (IOError, SyntaxError):
         return
     try:
-        aliases, entry = cmdutil.findcmd(cmd,
-            getattr(mod, 'cmdtable', {}), strict)
+        aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
     except (error.AmbiguousCommand, error.UnknownCommand):
         return
-    except Exception:
-        ui.warn(_('warning: error finding commands in %s\n') % path)
-        ui.traceback()
-        return
     for c in aliases:
         if c.startswith(cmd):
             cmd = c
             break
     else:
         cmd = aliases[0]
-    doc = gettext(pycompat.getdoc(mod))
+    doc = _disabledhelp(path)
     return (cmd, name, doc)
 
 def disabledcmd(ui, cmd, strict=False):
-    '''import disabled extensions until cmd is found.
+    '''find cmd from disabled extensions without importing.
     returns (cmdname, extname, doc)'''
 
-    paths = _disabledpaths(strip_init=True)
+    paths = _disabledpaths()
     if not paths:
         raise error.UnknownCommand(cmd)