builddoc.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  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. _deprecated_options = ['autodoc_tree_index_modules', 'autodoc_index_modules',
  56. 'autodoc_tree_excludes', 'autodoc_exclude_modules']
  57. _deprecated_envs = ['AUTODOC_TREE_INDEX_MODULES', 'AUTODOC_INDEX_MODULES']
  58. _rst_template = """%(heading)s
  59. %(underline)s
  60. .. automodule:: %(module)s
  61. :members:
  62. :undoc-members:
  63. :show-inheritance:
  64. """
  65. def _find_modules(arg, dirname, files):
  66. for filename in files:
  67. if filename.endswith('.py') and filename != '__init__.py':
  68. arg["%s.%s" % (dirname.replace('/', '.'),
  69. filename[:-3])] = True
  70. class LocalBuildDoc(setup_command.BuildDoc):
  71. builders = ['html']
  72. command_name = 'build_sphinx'
  73. sphinx_initialized = False
  74. def _get_source_dir(self):
  75. option_dict = self.distribution.get_option_dict('build_sphinx')
  76. pbr_option_dict = self.distribution.get_option_dict('pbr')
  77. _, api_doc_dir = pbr_option_dict.get('api_doc_dir', (None, 'api'))
  78. if 'source_dir' in option_dict:
  79. source_dir = os.path.join(option_dict['source_dir'][1],
  80. api_doc_dir)
  81. else:
  82. source_dir = 'doc/source/' + api_doc_dir
  83. if not os.path.exists(source_dir):
  84. os.makedirs(source_dir)
  85. return source_dir
  86. def generate_autoindex(self, excluded_modules=None):
  87. log.info("[pbr] Autodocumenting from %s"
  88. % os.path.abspath(os.curdir))
  89. modules = {}
  90. source_dir = self._get_source_dir()
  91. for pkg in self.distribution.packages:
  92. if '.' not in pkg:
  93. for dirpath, dirnames, files in os.walk(pkg):
  94. _find_modules(modules, dirpath, files)
  95. def include(module):
  96. return not any(fnmatch.fnmatch(module, pat)
  97. for pat in excluded_modules)
  98. module_list = sorted(mod for mod in modules.keys() if include(mod))
  99. autoindex_filename = os.path.join(source_dir, 'autoindex.rst')
  100. with open(autoindex_filename, 'w') as autoindex:
  101. autoindex.write(""".. toctree::
  102. :maxdepth: 1
  103. """)
  104. for module in module_list:
  105. output_filename = os.path.join(source_dir,
  106. "%s.rst" % module)
  107. heading = "The :mod:`%s` Module" % module
  108. underline = "=" * len(heading)
  109. values = dict(module=module, heading=heading,
  110. underline=underline)
  111. log.info("[pbr] Generating %s"
  112. % output_filename)
  113. with open(output_filename, 'w') as output_file:
  114. output_file.write(_rst_template % values)
  115. autoindex.write(" %s.rst\n" % module)
  116. def _sphinx_tree(self):
  117. source_dir = self._get_source_dir()
  118. cmd = ['-H', 'Modules', '-o', source_dir, '.']
  119. if apidoc_use_padding:
  120. cmd.insert(0, 'apidoc')
  121. apidoc.main(cmd + self.autodoc_tree_excludes)
  122. def _sphinx_run(self):
  123. if not self.verbose:
  124. status_stream = cStringIO.StringIO()
  125. else:
  126. status_stream = sys.stdout
  127. confoverrides = {}
  128. if self.project:
  129. confoverrides['project'] = self.project
  130. if self.version:
  131. confoverrides['version'] = self.version
  132. if self.release:
  133. confoverrides['release'] = self.release
  134. if self.today:
  135. confoverrides['today'] = self.today
  136. if self.sphinx_initialized:
  137. confoverrides['suppress_warnings'] = [
  138. 'app.add_directive', 'app.add_role',
  139. 'app.add_generic_role', 'app.add_node',
  140. 'image.nonlocal_uri',
  141. ]
  142. app = application.Sphinx(
  143. self.source_dir, self.config_dir,
  144. self.builder_target_dir, self.doctree_dir,
  145. self.builder, confoverrides, status_stream,
  146. freshenv=self.fresh_env, warningiserror=self.warning_is_error)
  147. self.sphinx_initialized = True
  148. try:
  149. app.build(force_all=self.all_files)
  150. except Exception as err:
  151. from docutils import utils
  152. if isinstance(err, utils.SystemMessage):
  153. sys.stder.write('reST markup error:\n')
  154. sys.stderr.write(err.args[0].encode('ascii',
  155. 'backslashreplace'))
  156. sys.stderr.write('\n')
  157. else:
  158. raise
  159. if self.link_index:
  160. src = app.config.master_doc + app.builder.out_suffix
  161. dst = app.builder.get_outfilename('index')
  162. os.symlink(src, dst)
  163. def run(self):
  164. option_dict = self.distribution.get_option_dict('pbr')
  165. # TODO(stephenfin): Remove this (and the entire file) when 5.0 is
  166. # released
  167. warn_opts = set(option_dict.keys()).intersection(_deprecated_options)
  168. warn_env = list(filter(lambda x: os.getenv(x), _deprecated_envs))
  169. if warn_opts or warn_env:
  170. msg = ('The autodoc and autodoc_tree features are deprecated in '
  171. '4.2 and will be removed in a future release. You should '
  172. 'use the sphinxcontrib-apidoc Sphinx extension instead. '
  173. 'Refer to the pbr documentation for more information.')
  174. if warn_opts:
  175. msg += ' Deprecated options: %s' % list(warn_opts)
  176. if warn_env:
  177. msg += ' Deprecated environment variables: %s' % warn_env
  178. log.warn(msg)
  179. if git._git_is_installed():
  180. git.write_git_changelog(option_dict=option_dict)
  181. git.generate_authors(option_dict=option_dict)
  182. tree_index = options.get_boolean_option(option_dict,
  183. 'autodoc_tree_index_modules',
  184. 'AUTODOC_TREE_INDEX_MODULES')
  185. auto_index = options.get_boolean_option(option_dict,
  186. 'autodoc_index_modules',
  187. 'AUTODOC_INDEX_MODULES')
  188. if not os.getenv('SPHINX_DEBUG'):
  189. # NOTE(afazekas): These options can be used together,
  190. # but they do a very similar thing in a different way
  191. if tree_index:
  192. self._sphinx_tree()
  193. if auto_index:
  194. self.generate_autoindex(
  195. set(option_dict.get(
  196. "autodoc_exclude_modules",
  197. [None, ""])[1].split()))
  198. self.finalize_options()
  199. is_multibuilder_sphinx = version.SemanticVersion.from_pip_string(
  200. sphinx.__version__) >= version.SemanticVersion(1, 6)
  201. # TODO(stephenfin): Remove support for Sphinx < 1.6 in 4.0
  202. if not is_multibuilder_sphinx:
  203. log.warn('[pbr] Support for Sphinx < 1.6 will be dropped in '
  204. 'pbr 4.0. Upgrade to Sphinx 1.6+')
  205. # TODO(stephenfin): Remove this at the next MAJOR version bump
  206. if self.builders != ['html']:
  207. log.warn("[pbr] Sphinx 1.6 added native support for "
  208. "specifying multiple builders in the "
  209. "'[sphinx_build] builder' configuration option, "
  210. "found in 'setup.cfg'. As a result, the "
  211. "'[sphinx_build] builders' option has been "
  212. "deprecated and will be removed in pbr 4.0. Migrate "
  213. "to the 'builder' configuration option.")
  214. if is_multibuilder_sphinx:
  215. self.builder = self.builders
  216. if is_multibuilder_sphinx:
  217. # Sphinx >= 1.6
  218. return setup_command.BuildDoc.run(self)
  219. # Sphinx < 1.6
  220. for builder in self.builders:
  221. self.builder = builder
  222. self.finalize_options()
  223. self._sphinx_run()
  224. def initialize_options(self):
  225. # Not a new style class, super keyword does not work.
  226. setup_command.BuildDoc.initialize_options(self)
  227. # NOTE(dstanek): exclude setup.py from the autodoc tree index
  228. # builds because all projects will have an issue with it
  229. self.autodoc_tree_excludes = ['setup.py']
  230. def finalize_options(self):
  231. from pbr import util
  232. # Not a new style class, super keyword does not work.
  233. setup_command.BuildDoc.finalize_options(self)
  234. # Handle builder option from command line - override cfg
  235. option_dict = self.distribution.get_option_dict('build_sphinx')
  236. if 'command line' in option_dict.get('builder', [[]])[0]:
  237. self.builders = option_dict['builder'][1]
  238. # Allow builders to be configurable - as a comma separated list.
  239. if not isinstance(self.builders, list) and self.builders:
  240. self.builders = self.builders.split(',')
  241. self.project = self.distribution.get_name()
  242. self.version = self.distribution.get_version()
  243. self.release = self.distribution.get_version()
  244. # NOTE(dstanek): check for autodoc tree exclusion overrides
  245. # in the setup.cfg
  246. opt = 'autodoc_tree_excludes'
  247. option_dict = self.distribution.get_option_dict('pbr')
  248. if opt in option_dict:
  249. self.autodoc_tree_excludes = util.split_multiline(
  250. option_dict[opt][1])
  251. # handle Sphinx < 1.5.0
  252. if not hasattr(self, 'warning_is_error'):
  253. self.warning_is_error = False