sphinx.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. # -*- coding: utf-8 -*-
  2. import datetime
  3. import json
  4. import collections
  5. import logging
  6. import os
  7. import posixpath
  8. from sphinx import config as sphinx_config
  9. from sphinx.util import i18n as sphinx_i18n
  10. from sphinx.locale import _
  11. logger = logging.getLogger(__name__)
  12. DATE_FMT = "%Y-%m-%d %H:%M:%S %z"
  13. DEFAULT_TAG_WHITELIST = r"^.*$"
  14. DEFAULT_BRANCH_WHITELIST = r"^.*$"
  15. DEFAULT_REMOTE_WHITELIST = None
  16. DEFAULT_RELEASED_PATTERN = r"^tags/.*$"
  17. DEFAULT_OUTPUTDIR_FORMAT = r"{ref.name}"
  18. Version = collections.namedtuple(
  19. "Version",
  20. [
  21. "name",
  22. "url",
  23. "version",
  24. "release",
  25. "is_released",
  26. ],
  27. )
  28. class VersionInfo:
  29. def __init__(self, app, context, metadata, current_version_name):
  30. self.app = app
  31. self.context = context
  32. self.metadata = metadata
  33. self.current_version_name = current_version_name
  34. def _dict_to_versionobj(self, v):
  35. return Version(
  36. name=v["name"],
  37. url=self.vpathto(v["name"]),
  38. version=v["version"],
  39. release=v["release"],
  40. is_released=v["is_released"],
  41. )
  42. @property
  43. def tags(self):
  44. return [
  45. self._dict_to_versionobj(v)
  46. for v in self.metadata.values()
  47. if v["source"] == "tags"
  48. ]
  49. @property
  50. def branches(self):
  51. return [
  52. self._dict_to_versionobj(v)
  53. for v in self.metadata.values()
  54. if v["source"] != "tags"
  55. ]
  56. @property
  57. def releases(self):
  58. return [
  59. self._dict_to_versionobj(v)
  60. for v in self.metadata.values()
  61. if v["is_released"]
  62. ]
  63. @property
  64. def in_development(self):
  65. return [
  66. self._dict_to_versionobj(v)
  67. for v in self.metadata.values()
  68. if not v["is_released"]
  69. ]
  70. def __iter__(self):
  71. for item in self.tags:
  72. yield item
  73. for item in self.branches:
  74. yield item
  75. def __getitem__(self, name):
  76. v = self.metadata.get(name)
  77. if v:
  78. return self._dict_to_versionobj(v)
  79. def vhasdoc(self, other_version_name):
  80. if self.current_version_name == other_version_name:
  81. return True
  82. other_version = self.metadata[other_version_name]
  83. return self.context["pagename"] in other_version["docnames"]
  84. def vpathto(self, other_version_name):
  85. if self.current_version_name == other_version_name:
  86. return "{}.html".format(
  87. posixpath.split(self.context["pagename"])[-1]
  88. )
  89. # Find relative outputdir paths from common output root
  90. current_version = self.metadata[self.current_version_name]
  91. other_version = self.metadata[other_version_name]
  92. current_outputroot = os.path.abspath(current_version["outputdir"])
  93. other_outputroot = os.path.abspath(other_version["outputdir"])
  94. outputroot = os.path.commonpath((current_outputroot, other_outputroot))
  95. current_outputroot = os.path.relpath(
  96. current_outputroot, start=outputroot
  97. )
  98. other_outputroot = os.path.relpath(other_outputroot, start=outputroot)
  99. # Ensure that we use POSIX separators in the path (for the HTML code)
  100. if os.sep != posixpath.sep:
  101. current_outputroot = posixpath.join(
  102. *os.path.split(current_outputroot)
  103. )
  104. other_outputroot = posixpath.join(*os.path.split(other_outputroot))
  105. # Find relative path to root of other_version's outputdir
  106. current_outputdir = posixpath.dirname(
  107. posixpath.join(current_outputroot, self.context["pagename"])
  108. )
  109. other_outputdir = posixpath.relpath(
  110. other_outputroot, start=current_outputdir
  111. )
  112. if not self.vhasdoc(other_version_name):
  113. return posixpath.join(other_outputdir, "index.html")
  114. return posixpath.join(
  115. other_outputdir, "{}.html".format(self.context["pagename"])
  116. )
  117. def html_page_context(app, pagename, templatename, context, doctree):
  118. versioninfo = VersionInfo(
  119. app, context, app.config.smv_metadata, app.config.smv_current_version
  120. )
  121. context["versions"] = versioninfo
  122. context["vhasdoc"] = versioninfo.vhasdoc
  123. context["vpathto"] = versioninfo.vpathto
  124. context["current_version"] = versioninfo[app.config.smv_current_version]
  125. context["latest_version"] = versioninfo[app.config.smv_latest_version]
  126. context["html_theme"] = app.config.html_theme
  127. def config_inited(app, config):
  128. """Update the Sphinx builder.
  129. :param sphinx.application.Sphinx app: Sphinx application object.
  130. """
  131. if not config.smv_metadata:
  132. if not config.smv_metadata_path:
  133. return
  134. with open(config.smv_metadata_path, mode="r") as f:
  135. metadata = json.load(f)
  136. config.smv_metadata = metadata
  137. if not config.smv_current_version:
  138. return
  139. try:
  140. data = app.config.smv_metadata[config.smv_current_version]
  141. except KeyError:
  142. return
  143. app.connect("html-page-context", html_page_context)
  144. # Restore config values
  145. old_config = sphinx_config.Config.read(data["confdir"])
  146. old_config.pre_init_values()
  147. old_config.init_values()
  148. config.version = data["version"]
  149. config.release = data["release"]
  150. config.rst_prolog = data["rst_prolog"]
  151. config.today = old_config.today
  152. if not config.today:
  153. config.today = sphinx_i18n.format_date(
  154. format=config.today_fmt or _("%b %d, %Y"),
  155. date=datetime.datetime.strptime(data["creatordate"], DATE_FMT),
  156. language=config.language,
  157. )
  158. def setup(app):
  159. app.add_config_value("smv_metadata", {}, "html")
  160. app.add_config_value("smv_metadata_path", "", "html")
  161. app.add_config_value("smv_current_version", "", "html")
  162. app.add_config_value("smv_latest_version", "master", "html")
  163. app.add_config_value("smv_tag_whitelist", DEFAULT_TAG_WHITELIST, "html")
  164. app.add_config_value(
  165. "smv_branch_whitelist", DEFAULT_BRANCH_WHITELIST, "html"
  166. )
  167. app.add_config_value(
  168. "smv_remote_whitelist", DEFAULT_REMOTE_WHITELIST, "html"
  169. )
  170. app.add_config_value(
  171. "smv_released_pattern", DEFAULT_RELEASED_PATTERN, "html"
  172. )
  173. app.add_config_value(
  174. "smv_outputdir_format", DEFAULT_OUTPUTDIR_FORMAT, "html"
  175. )
  176. app.connect("config-inited", config_inited)
  177. return {
  178. "version": "0.2",
  179. "parallel_read_safe": True,
  180. "parallel_write_safe": True,
  181. }