[PATCH 1 of 1] extensions: extract doc using python facilities instead of custom parser

Cédric Duval cedricduval at free.fr
Fri Aug 7 14:46:59 CDT 2009


# HG changeset patch
# User Cédric Duval <cedricduval at free.fr>
# Date 1248811268 -7200
# Node ID f21de9bbc4374078d1d82bc4ede060244d6eed4f
# Parent  9a69ab6d7cf765c1a3ed43ae8b0e5d770c6eacf6
extensions: extract doc using python facilities instead of custom parser

For disabled extensions, the docstring is obtained using the extension's
code object. This way, no code needs to be imported/executed.

Special handling is required for zip libraries as imp.find_module does
not provide transparent support for them.

Thanks to Henrik Stuart for the marshal/compile trick and persistence
in looking for solutions avoiding module import.

diff --git a/mercurial/extensions.py b/mercurial/extensions.py
--- a/mercurial/extensions.py
+++ b/mercurial/extensions.py
@@ -5,8 +5,9 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2, incorporated herein by reference.
 
-import imp, os
-import util, cmdutil, help
+import imp, marshal, os, sys, zipimport
+import util, cmdutil
+from opcode import HAVE_ARGUMENT
 from i18n import _, gettext
 
 _extensions = {}
@@ -118,6 +119,81 @@
     setattr(container, funcname, wrap)
     return origfn
 
+def listexts(path):
+    '''return a set of extensions from a given path'''
+    exts = set()
+    path = os.path.abspath(path)
+    try: # might not be a filesystem path
+        list = os.listdir(path)
+    except OSError:
+        return exts
+
+    for e in list:
+        if e.endswith('.py') and e != '__init__.py':
+            exts.add(e.rsplit('.', 1)[0])
+        else:
+            f = os.path.join(path, e, '__init__.py')
+            if os.path.exists(f):
+                exts.add(e)
+    return exts
+
+def getcode(name):
+    '''return the code object of a given extension'''
+    for path in sys.path:
+        if path.endswith('.zip'):
+            z = zipimport.zipimporter('%s/hgext' % path)
+            try:
+                return z.get_code(name)
+            except ImportError:
+                continue
+        else:
+            try:
+                extpath = imp.find_module('hgext', [path])[1]
+                fh, extpath, desc = imp.find_module(name, [extpath])
+                if not fh:
+                    fh, path, desc = imp.find_module('__init__', [extpath])
+            except ImportError:
+                continue
+
+            if desc[0] == '.py':
+                return compile(fh.read(), path, 'exec')
+            elif desc[0] == '.pyc':
+                fh.read(8)
+                return marshal.load(fh)
+
+def codedoc(code):
+    '''return the docstring for a given code object'''
+    co = [ord(c) for c in code.co_code]
+    codelen = len(co)
+    i = 0
+    idx = None
+    while i != codelen:
+        opcode = co[i]
+        if opcode == 100: # LOAD_CONST
+            idx = co[i+1] + (co[i+2] << 8)
+        elif idx is not None and opcode == 90: # STORE_NAME
+            if code.co_names[co[i+1] + (co[i+2] << 8)] == '__doc__':
+                return code.co_consts[idx]
+        else:
+            idx = None
+        i += 1
+        if opcode >= HAVE_ARGUMENT:
+            i += 2
+
+def rawdoc(name):
+    '''return the untranslated docstring or None for a given extension'''
+    try:
+        return find(name).__doc__
+    except KeyError: # not loaded
+        code = getcode(name)
+        if code is not None:
+            return codedoc(code)
+        return None
+
+def doc(name):
+    '''return the translated docstring of a given extension'''
+    return gettext(rawdoc(name)) or _('(no help text available)')
+
 def disabled():
     '''find disabled extensions from hgext
     returns a dict of {name: desc}, and the max name length'''
@@ -125,39 +201,13 @@
     import hgext
     extpath = os.path.dirname(os.path.abspath(hgext.__file__))
 
-    try: # might not be a filesystem path
-        files = os.listdir(extpath)
-    except OSError:
-        return None, 0
-
     exts = {}
     maxlength = 0
-    for e in files:
-
-        if e.endswith('.py'):
-            name = e.rsplit('.', 1)[0]
-            path = os.path.join(extpath, e)
-        else:
-            name = e
-            path = os.path.join(extpath, e, '__init__.py')
-            if not os.path.exists(path):
-                continue
-
-        if name in exts or name in _order or name == '__init__':
+    for name in listexts(extpath):
+        if name in _order:
             continue
 
-        try:
-            file = open(path)
-        except IOError:
-            continue
-        else:
-            doc = help.moduledoc(file)
-            file.close()
-
-        if doc: # extracting localized synopsis
-            exts[name] = gettext(doc).splitlines()[0]
-        else:
-            exts[name] = _('(no help text available)')
+        exts[name] = doc(name).splitlines()[0]
 
         if len(name) > maxlength:
             maxlength = len(name)
diff --git a/mercurial/help.py b/mercurial/help.py
--- a/mercurial/help.py
+++ b/mercurial/help.py
@@ -9,36 +9,6 @@
 import extensions, util
 
 
-def moduledoc(file):
-    '''return the top-level python documentation for the given file
-
-    Loosely inspired by pydoc.source_synopsis(), but rewritten to handle \'''
-    as well as """ and to return the whole text instead of just the synopsis'''
-    result = []
-
-    line = file.readline()
-    while line[:1] == '#' or not line.strip():
-        line = file.readline()
-        if not line: break
-
-    start = line[:3]
-    if start == '"""' or start == "'''":
-        line = line[3:]
-        while line:
-            if line.rstrip().endswith(start):
-                line = line.split(start)[0]
-                if line:
-                    result.append(line)
-                break
-            elif not line:
-                return None # unmatched delimiter
-            result.append(line)
-            line = file.readline()
-    else:
-        return None
-
-    return ''.join(result)
-
 def listexts(header, exts, maxlength):
     '''return a text listing of the given extensions'''
     if not exts:



More information about the Mercurial-devel mailing list