[PATCH 3 of 3 RFC] setup: install & build documentation using docutils

Dan Villiom Podlaski Christiansen danchr at gmail.com
Tue Nov 17 15:36:44 CST 2009


# HG changeset patch
# User Dan Villiom Podlaski Christiansen <danchr at gmail.com>
# Date 1257772950 -3600
# Node ID a1ec03ca952b86cc057587cc36ccb5ccfd289c59
# Parent  9cf85ff4e6016a227dba8f94708f8e89a9814440
setup: install & build documentation using docutils.

diff --git a/doc/__init__.py b/doc/__init__.py
new file mode 100644
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -29,6 +29,8 @@ except:
 import os, subprocess, time
 import shutil
 import tempfile
+import re
+
 from distutils.core import setup, Extension
 from distutils.dist import Distribution
 from distutils.command.install_data import install_data
@@ -50,6 +52,9 @@ extra = {}
 scripts = ['hg']
 if os.name == 'nt':
     scripts.append('contrib/win32/hg.bat')
+else:
+    # required for generated sources
+    os.environ['LANG'] = os.environ['LC_ALL'] = 'C'
 
 # simplified version of distutils.ccompiler.CCompiler.has_function
 # that actually removes its temporary files.
@@ -212,6 +217,10 @@ class build_mo(build):
             self.distribution.package_data_files = []
         datafiles = self.distribution.package_data_files
 
+        if not hasattr(self.distribution, 'package_data_files'):
+            self.distribution.package_data_files = []
+        datafiles = self.distribution.package_data_files
+
         join = os.path.join
         for po in os.listdir(podir):
             if not po.endswith('.po'):
@@ -229,6 +238,148 @@ class build_mo(build):
 
 build.sub_commands.append(('build_mo', None))
 
+class build_doc(build):
+
+    description = "build documentation from ReST sources"
+
+    user_options = [('build-dir=', 'd', 'directory to build to'),
+                    ('force', 'f',
+                     'forcibly build everything (ignore file timestamps')]
+    boolean_options = ['force']
+
+    def initialize_options (self):
+        self.build_dir = None
+        self.force = None
+
+    def finalize_options (self):
+        # bit of a hack for turning "lib.<whatever>" into "data.<whatever>"
+        undefined = self.build_dir is None
+        self.set_undefined_options('build',
+                                   ('build_lib', 'build_dir'),
+                                   ('force', 'force'))
+        if undefined:
+            self.build_dir = self.build_dir.replace('lib', 'data')
+
+    def gendoc(self, output):
+        from doc import gendoc
+        from cStringIO import StringIO
+
+        s = StringIO()
+        gendoc.show_doc(s)
+        s = s.getvalue()
+
+        # only write to the file if the output has changed
+        if not (os.path.exists(output) and file(output).read() == s):
+            file(output, 'w').write(s)
+
+    def run(self):
+        '''
+        This method will build the Mercurial documentation from its ReST
+        sources, as well as extracting docstrings using the `gendoc' method.
+
+        For each source file, we use docutils to read it and obtain its
+        dependancies. Then, both manual pages and HTML documentation is
+        generated, but only when needed. This is approach is reasonably fast
+        and flexible, but at the cost of some implementation complexity.
+        '''
+        from os.path import join
+
+        try:
+            # import early, so we can fail early
+            from docutils import frontend, io, readers, writers
+        except ImportError:
+            self.warn('skipping documentation; docutils not found.')
+            return
+
+        try:
+            from docutils.writers import manpage
+        except ImportError:
+            # load & use the bundled rst2man
+            from doc import rst2man as manpage
+
+        if not os.path.isdir('doc'):
+            self.warn("could not find doc directory")
+            return
+
+        # avoid cluttering the source directory by copying all documentation
+        # files to the build directory
+        self.mkpath(self.build_dir)
+        self.copy_tree('doc', self.build_dir)
+
+        # update extracted documentation
+        self.execute(self.gendoc,
+                     [join(self.build_dir, 'hg.1.gendoc.txt')],
+                     ('extracting %s from source files'
+                      % join(self.build_dir, 'hg.1.gendoc.txt')))
+
+        datafiles = self.distribution.data_files
+        pattern = re.compile(r'^((.*)\.([0-9]))\.txt$')
+
+        stylesheet = 'style.css'
+        datafiles.append((join('doc', self.distribution.get_name()),
+                          [join(self.build_dir, stylesheet)]))
+
+        for srcfile in os.listdir(self.build_dir):
+            match = pattern.match(srcfile)
+            if not match:
+                continue
+
+            srcfile = join(self.build_dir, srcfile)
+            htmlfile = join(self.build_dir, match.group(1) + '.html')
+            manfile = join(self.build_dir, match.group(1))
+
+            # add resulting files to distrubution
+            datafiles.append((join('doc', self.distribution.get_name()),
+                              [htmlfile]))
+            datafiles.append((join('man', 'man' + manfile.rsplit('.', 1)[1]),
+                              [manfile]))
+
+            # reader & writer classes
+            # NOTE: writers.get_writer_class('manpage') will be used eventually
+            reader = readers.get_reader_class('standalone')(parser_name='rst')
+            htmlwriter = writers.get_writer_class('html')()
+            manwriter = manpage.Writer()
+            components = (reader, reader.parser, htmlwriter, manwriter,)
+
+            # get & set docutils settings
+            optparser = frontend.OptionParser(components)
+            settings = optparser.parse_args(['--halt', 'warning',
+                                             '--link-stylesheet',
+                                             '--stylesheet-path', 'style.css'
+                                             '--option-limit' '0'])
+
+            # read & parse ReST source
+            reader.read(io.FileInput(source_path=srcfile, encoding='ascii'),
+                        None, settings)
+
+            document = reader.document
+            document.transformer.populate_from_components(components)
+            document.transformer.apply_transforms()
+
+            # extract dependancies from docutils
+            dependancies = [srcfile] + settings.record_dependencies.list
+
+            # generate the documentation files; using distutils to avoid
+            # unnecessary processing
+            self.make_file(infiles=[srcfile] + dependancies, outfile=htmlfile,
+                           func=htmlwriter.write,
+                           args=(reader.document,
+                                 io.FileOutput(destination_path=htmlfile,
+                                               encoding='utf-8')),
+                           exec_msg='generating %s' % htmlfile)
+
+            settings = optparser.parse_args(['--strip-elements-with-class',
+                                             'htmlonly'], settings)
+
+            self.make_file(infiles=[srcfile] + dependancies, outfile=manfile,
+                           func=manwriter.write,
+                           args=(reader.document,
+                                 io.FileOutput(destination_path=manfile,
+                                               encoding='ascii')),
+                           exec_msg='generating %s' % manfile)
+
+build.sub_commands.append(('build_doc', None))
+
 Distribution.pure = 0
 Distribution.global_options.append(('pure', None, "use pure (slow) Python "
                                     "code instead of C extensions"))
@@ -257,6 +408,7 @@ class hg_build_py(build_py):
 
 cmdclass = {'install_data': install_package_data,
             'build_mo': build_mo,
+            'build_doc': build_doc,
             'build_py': hg_build_py}
 
 ext_modules=[


More information about the Mercurial-devel mailing list