builddoc.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. # Copyright 2011 OpenStack Foundation
  2. # Copyright 2012-2013 Hewlett-Packard Development Company, L.P.
  3. # All Rights Reserved.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  6. # not use this file except in compliance with the License. You may obtain
  7. # a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  13. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  14. # License for the specific language governing permissions and limitations
  15. # under the License.
  16. from distutils import log
  17. import fnmatch
  18. import os
  19. import sys
  20. try:
  21. import cStringIO
  22. except ImportError:
  23. import io as cStringIO
  24. try:
  25. import sphinx
  26. # NOTE(dhellmann): Newer versions of Sphinx have moved the apidoc
  27. # module into sphinx.ext and the API is slightly different (the
  28. # function expects sys.argv[1:] instead of sys.argv[:]. So, figure
  29. # out where we can import it from and set a flag so we can invoke
  30. # it properly. See this change in sphinx for details:
  31. # https://github.com/sphinx-doc/sphinx/commit/87630c8ae8bff8c0e23187676e6343d8903003a6
  32. try:
  33. from sphinx.ext import apidoc
  34. apidoc_use_padding = False
  35. except ImportError:
  36. from sphinx import apidoc
  37. apidoc_use_padding = True
  38. from sphinx import application
  39. from sphinx import setup_command
  40. except Exception as e:
  41. # NOTE(dhellmann): During the installation of docutils, setuptools
  42. # tries to import pbr code to find the egg_info.writer hooks. That
  43. # imports this module, which imports sphinx, which imports
  44. # docutils, which is being installed. Because docutils uses 2to3
  45. # to convert its code during installation under python 3, the
  46. # import fails, but it fails with an error other than ImportError
  47. # (today it's a NameError on StandardError, an exception base
  48. # class). Convert the exception type here so it can be caught in
  49. # packaging.py where we try to determine if we can import and use
  50. # sphinx by importing this module. See bug #1403510 for details.
  51. raise ImportError(str(e))
  52. from pbr import git
  53. from pbr import options
  54. from pbr import version
  55. _rst_template = """%(heading)s
  56. %(underline)s
  57. .. automodule:: %(module)s
  58. :members:
  59. :undoc-members:
  60. :show-inheritance:
  61. """
  62. def _find_modules(arg, dirname, files):
  63. for filename in files:
  64. if filename.endswith('.py') and filename != '__init__.py':
  65. arg["%s.%s" % (dirname.replace('/', '.'),
  66. filename[:-3])] = True
  67. class LocalBuildDoc(setup_command.BuildDoc):
  68. builders = ['html']
  69. command_name = 'build_sphinx'
  70. sphinx_initialized = False
  71. def _get_source_dir(self):
  72. option_dict = self.distribution.get_option_dict('build_sphinx')
  73. pbr_option_dict = self.distribution.get_option_dict('pbr')
  74. _, api_doc_dir = pbr_option_dict.get('api_doc_dir', (None, 'api'))
  75. if 'source_dir' in option_dict:
  76. source_dir = os.path.join(option_dict['source_dir'][1],
  77. api_doc_dir)
  78. else:
  79. source_dir = 'doc/source/' + api_doc_dir
  80. if not os.path.exists(source_dir):
  81. os.makedirs(source_dir)
  82. return source_dir
  83. def generate_autoindex(self, excluded_modules=None):
  84. log.info("[pbr] Autodocumenting from %s"
  85. % os.path.abspath(os.curdir))
  86. modules = {}
  87. source_dir = self._get_source_dir()
  88. for pkg in self.distribution.packages:
  89. if '.' not in pkg:
  90. for dirpath, dirnames, files in os.walk(pkg):
  91. _find_modules(modules, dirpath, files)
  92. def include(module):
  93. return not any(fnmatch.fnmatch(module, pat)
  94. for pat in excluded_modules)
  95. module_list = sorted(mod for mod in modules.keys() if include(mod))
  96. autoindex_filename = os.path.join(source_dir, 'autoindex.rst')
  97. with open(autoindex_filename, 'w') as autoindex:
  98. autoindex.write(""".. toctree::
  99. :maxdepth: 1
  100. """)
  101. for module in module_list:
  102. output_filename = os.path.join(source_dir,
  103. "%s.rst" % module)
  104. heading = "The :mod:`%s` Module" % module
  105. underline = "=" * len(heading)
  106. values = dict(module=module, heading=heading,
  107. underline=underline)
  108. log.info("[pbr] Generating %s"
  109. % output_filename)
  110. with open(output_filename, 'w') as output_file:
  111. output_file.write(_rst_template % values)
  112. autoindex.write(" %s.rst\n" % module)
  113. def _sphinx_tree(self):
  114. source_dir = self._get_source_dir()
  115. cmd = ['-H', 'Modules', '-o', source_dir, '.']
  116. if apidoc_use_padding:
  117. cmd.insert(0, 'apidoc')
  118. apidoc.main(cmd + self.autodoc_tree_excludes)
  119. def _sphinx_run(self):
  120. if not self.verbose:
  121. status_stream = cStringIO.StringIO()
  122. else:
  123. status_stream = sys.stdout
  124. confoverrides = {}
  125. if self.project:
  126. confoverrides['project'] = self.project
  127. if self.version:
  128. confoverrides['version'] = self.version
  129. if self.release:
  130. confoverrides['release'] = self.release
  131. if self.today:
  132. confoverrides['today'] = self.today
  133. if self.sphinx_initialized:
  134. confoverrides['suppress_warnings'] = [
  135. 'app.add_directive', 'app.add_role',
  136. 'app.add_generic_role', 'app.add_node',
  137. 'image.nonlocal_uri',
  138. ]
  139. app = application.Sphinx(
  140. self.source_dir, self.config_dir,
  141. self.builder_target_dir, self.doctree_dir,
  142. self.builder, confoverrides, status_stream,
  143. freshenv=self.fresh_env, warningiserror=self.warning_is_error)
  144. self.sphinx_initialized = True
  145. try:
  146. app.build(force_all=self.all_files)
  147. except Exception as err:
  148. from docutils import utils
  149. if isinstance(err, utils.SystemMessage):
  150. sys.stder.write('reST markup error:\n')
  151. sys.stderr.write(err.args[0].encode('ascii',
  152. 'backslashreplace'))
  153. sys.stderr.write('\n')
  154. else:
  155. raise
  156. if self.link_index:
  157. src = app.config.master_doc + app.builder.out_suffix
  158. dst = app.builder.get_outfilename('index')
  159. os.symlink(src, dst)
  160. def run(self):
  161. option_dict = self.distribution.get_option_dict('pbr')
  162. if git._git_is_installed():
  163. git.write_git_changelog(option_dict=option_dict)
  164. git.generate_authors(option_dict=option_dict)
  165. tree_index = options.get_boolean_option(option_dict,
  166. 'autodoc_tree_index_modules',
  167. 'AUTODOC_TREE_INDEX_MODULES')
  168. auto_index = options.get_boolean_option(option_dict,
  169. 'autodoc_index_modules',
  170. 'AUTODOC_INDEX_MODULES')
  171. if not os.getenv('SPHINX_DEBUG'):
  172. # NOTE(afazekas): These options can be used together,
  173. # but they do a very similar thing in a different way
  174. if tree_index:
  175. self._sphinx_tree()
  176. if auto_index:
  177. self.generate_autoindex(
  178. set(option_dict.get(
  179. "autodoc_exclude_modules",
  180. [None, ""])[1].split()))
  181. self.finalize_options()
  182. is_multibuilder_sphinx = version.SemanticVersion.from_pip_string(
  183. sphinx.__version__) >= version.SemanticVersion(1, 6)
  184. # TODO(stephenfin): Remove support for Sphinx < 1.6 in 4.0
  185. if not is_multibuilder_sphinx:
  186. log.warn('[pbr] Support for Sphinx < 1.6 will be dropped in '
  187. 'pbr 4.0. Upgrade to Sphinx 1.6+')
  188. # TODO(stephenfin): Remove this at the next MAJOR version bump
  189. if self.builders != ['html']:
  190. log.warn("[pbr] Sphinx 1.6 added native support for "
  191. "specifying multiple builders in the "
  192. "'[sphinx_build] builder' configuration option, "
  193. "found in 'setup.cfg'. As a result, the "
  194. "'[sphinx_build] builders' option has been "
  195. "deprecated and will be removed in pbr 4.0. Migrate "
  196. "to the 'builder' configuration option.")
  197. if is_multibuilder_sphinx:
  198. self.builder = self.builders
  199. if is_multibuilder_sphinx:
  200. # Sphinx >= 1.6
  201. return setup_command.BuildDoc.run(self)
  202. # Sphinx < 1.6
  203. for builder in self.builder:
  204. self.builder = builder
  205. self.finalize_options()
  206. self._sphinx_run()
  207. def initialize_options(self):
  208. # Not a new style class, super keyword does not work.
  209. setup_command.BuildDoc.initialize_options(self)
  210. # NOTE(dstanek): exclude setup.py from the autodoc tree index
  211. # builds because all projects will have an issue with it
  212. self.autodoc_tree_excludes = ['setup.py']
  213. def finalize_options(self):
  214. from pbr import util
  215. # Not a new style class, super keyword does not work.
  216. setup_command.BuildDoc.finalize_options(self)
  217. # Handle builder option from command line - override cfg
  218. option_dict = self.distribution.get_option_dict('build_sphinx')
  219. if 'command line' in option_dict.get('builder', [[]])[0]:
  220. self.builders = option_dict['builder'][1]
  221. # Allow builders to be configurable - as a comma separated list.
  222. if not isinstance(self.builders, list) and self.builders:
  223. self.builders = self.builders.split(',')
  224. self.project = self.distribution.get_name()
  225. self.version = self.distribution.get_version()
  226. self.release = self.distribution.get_version()
  227. # NOTE(dstanek): check for autodoc tree exclusion overrides
  228. # in the setup.cfg
  229. opt = 'autodoc_tree_excludes'
  230. option_dict = self.distribution.get_option_dict('pbr')
  231. if opt in option_dict:
  232. self.autodoc_tree_excludes = util.split_multiline(
  233. option_dict[opt][1])
  234. # handle Sphinx < 1.5.0
  235. if not hasattr(self, 'warning_is_error'):
  236. self.warning_is_error = False