saltdomain.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. import itertools
  2. import os
  3. import re
  4. import salt
  5. from docutils import nodes
  6. from docutils.parsers.rst import Directive
  7. from docutils.statemachine import ViewList
  8. from sphinx import addnodes
  9. from sphinx.directives import ObjectDescription
  10. from sphinx.domains import Domain, ObjType
  11. from sphinx.domains import python as python_domain
  12. from sphinx.domains.python import PyObject
  13. from sphinx.locale import _
  14. from sphinx.roles import XRefRole
  15. from sphinx.util.nodes import make_refnode, nested_parse_with_titles, set_source_info
  16. class Event(PyObject):
  17. """
  18. Document Salt events
  19. """
  20. domain = "salt"
  21. class LiterateCoding(Directive):
  22. """
  23. Auto-doc SLS files using literate-style comment/code separation
  24. """
  25. has_content = False
  26. required_arguments = 1
  27. optional_arguments = 0
  28. final_argument_whitespace = False
  29. def parse_file(self, fpath):
  30. """
  31. Read a file on the file system (relative to salt's base project dir)
  32. :returns: A file-like object.
  33. :raises IOError: If the file cannot be found or read.
  34. """
  35. sdir = os.path.abspath(os.path.join(os.path.dirname(salt.__file__), os.pardir))
  36. with open(os.path.join(sdir, fpath), "rb") as f:
  37. return f.readlines()
  38. def parse_lit(self, lines):
  39. """
  40. Parse a string line-by-line delineating comments and code
  41. :returns: An tuple of boolean/list-of-string pairs. True designates a
  42. comment; False designates code.
  43. """
  44. comment_char = "#" # TODO: move this into a directive option
  45. comment = re.compile(r"^\s*{0}[ \n]".format(comment_char))
  46. section_test = lambda val: bool(comment.match(val))
  47. sections = []
  48. for is_doc, group in itertools.groupby(lines, section_test):
  49. if is_doc:
  50. text = [comment.sub("", i).rstrip("\r\n") for i in group]
  51. else:
  52. text = [i.rstrip("\r\n") for i in group]
  53. sections.append((is_doc, text))
  54. return sections
  55. def run(self):
  56. try:
  57. lines = self.parse_lit(self.parse_file(self.arguments[0]))
  58. except IOError as exc:
  59. document = self.state.document
  60. return [document.reporter.warning(str(exc), line=self.lineno)]
  61. node = nodes.container()
  62. node["classes"] = ["lit-container"]
  63. node.document = self.state.document
  64. enum = nodes.enumerated_list()
  65. enum["classes"] = ["lit-docs"]
  66. node.append(enum)
  67. # make first list item
  68. list_item = nodes.list_item()
  69. list_item["classes"] = ["lit-item"]
  70. for is_doc, line in lines:
  71. if is_doc and line == [""]:
  72. continue
  73. section = nodes.section()
  74. if is_doc:
  75. section["classes"] = ["lit-annotation"]
  76. nested_parse_with_titles(self.state, ViewList(line), section)
  77. else:
  78. section["classes"] = ["lit-content"]
  79. code = "\n".join(line)
  80. literal = nodes.literal_block(code, code)
  81. literal["language"] = "yaml"
  82. set_source_info(self, literal)
  83. section.append(literal)
  84. list_item.append(section)
  85. # If we have a pair of annotation/content items, append the list
  86. # item and create a new list item
  87. if len(list_item.children) == 2:
  88. enum.append(list_item)
  89. list_item = nodes.list_item()
  90. list_item["classes"] = ["lit-item"]
  91. # Non-semantic div for styling
  92. bg = nodes.container()
  93. bg["classes"] = ["lit-background"]
  94. node.append(bg)
  95. return [node]
  96. class LiterateFormula(LiterateCoding):
  97. """
  98. Customizations to handle finding and parsing SLS files
  99. """
  100. def parse_file(self, sls_path):
  101. """
  102. Given a typical Salt SLS path (e.g.: apache.vhosts.standard), find the
  103. file on the file system and parse it
  104. """
  105. config = self.state.document.settings.env.config
  106. formulas_dirs = config.formulas_dirs
  107. fpath = sls_path.replace(".", "/")
  108. name_options = ("{0}.sls".format(fpath), os.path.join(fpath, "init.sls"))
  109. paths = [
  110. os.path.join(fdir, fname)
  111. for fname in name_options
  112. for fdir in formulas_dirs
  113. ]
  114. for i in paths:
  115. try:
  116. with open(i, "rb") as f:
  117. return f.readlines()
  118. except IOError:
  119. pass
  120. raise IOError("Could not find sls file '{0}'".format(sls_path))
  121. class CurrentFormula(Directive):
  122. domain = "salt"
  123. has_content = False
  124. required_arguments = 1
  125. optional_arguments = 0
  126. final_argument_whitespace = False
  127. option_spec = {}
  128. def run(self):
  129. env = self.state.document.settings.env
  130. modname = self.arguments[0].strip()
  131. if modname == "None":
  132. env.temp_data["salt:formula"] = None
  133. else:
  134. env.temp_data["salt:formula"] = modname
  135. return []
  136. class Formula(Directive):
  137. domain = "salt"
  138. has_content = True
  139. required_arguments = 1
  140. def run(self):
  141. env = self.state.document.settings.env
  142. formname = self.arguments[0].strip()
  143. env.temp_data["salt:formula"] = formname
  144. if "noindex" in self.options:
  145. return []
  146. env.domaindata["salt"]["formulas"][formname] = (
  147. env.docname,
  148. self.options.get("synopsis", ""),
  149. self.options.get("platform", ""),
  150. "deprecated" in self.options,
  151. )
  152. targetnode = nodes.target("", "", ids=["module-" + formname], ismod=True)
  153. self.state.document.note_explicit_target(targetnode)
  154. indextext = u"{0}-formula)".format(formname)
  155. inode = addnodes.index(
  156. entries=[("single", indextext, "module-" + formname, "")]
  157. )
  158. return [targetnode, inode]
  159. class State(Directive):
  160. domain = "salt"
  161. has_content = True
  162. required_arguments = 1
  163. def run(self):
  164. env = self.state.document.settings.env
  165. statename = self.arguments[0].strip()
  166. if "noindex" in self.options:
  167. return []
  168. targetnode = nodes.target("", "", ids=["module-" + statename], ismod=True)
  169. self.state.document.note_explicit_target(targetnode)
  170. formula = env.temp_data.get("salt:formula")
  171. indextext = u"{1} ({0}-formula)".format(formula, statename)
  172. inode = addnodes.index(
  173. entries=[("single", indextext, "module-{0}".format(statename), ""),]
  174. )
  175. return [targetnode, inode]
  176. class SLSXRefRole(XRefRole):
  177. pass
  178. class SaltModuleIndex(python_domain.PythonModuleIndex):
  179. name = "modindex"
  180. localname = _("Salt Module Index")
  181. shortname = _("all salt modules")
  182. class SaltDomain(python_domain.PythonDomain):
  183. name = "salt"
  184. label = "Salt"
  185. data_version = 2
  186. object_types = python_domain.PythonDomain.object_types
  187. object_types.update(
  188. {"state": ObjType(_("state"), "state"),}
  189. )
  190. directives = python_domain.PythonDomain.directives
  191. directives.update(
  192. {
  193. "event": Event,
  194. "state": State,
  195. "formula": LiterateFormula,
  196. "currentformula": CurrentFormula,
  197. "saltconfig": LiterateCoding,
  198. }
  199. )
  200. roles = python_domain.PythonDomain.roles
  201. roles.update(
  202. {"formula": SLSXRefRole(),}
  203. )
  204. initial_data = python_domain.PythonDomain.initial_data
  205. initial_data.update(
  206. {"formulas": {},}
  207. )
  208. indices = [
  209. SaltModuleIndex,
  210. ]
  211. def resolve_xref(self, env, fromdocname, builder, type, target, node, contnode):
  212. if type == "formula" and target in self.data["formulas"]:
  213. doc, _, _, _ = self.data["formulas"].get(target, (None, None))
  214. if doc:
  215. return make_refnode(builder, fromdocname, doc, target, contnode, target)
  216. else:
  217. super(SaltDomain, self).resolve_xref(
  218. env, fromdocname, builder, type, target, node, contnode
  219. )
  220. # Monkey-patch the Python domain remove the python module index
  221. python_domain.PythonDomain.indices = [SaltModuleIndex]
  222. def setup(app):
  223. app.add_domain(SaltDomain)
  224. formulas_path = "templates/formulas"
  225. formulas_dir = os.path.join(
  226. os.path.abspath(os.path.dirname(salt.__file__)), formulas_path
  227. )
  228. app.add_config_value("formulas_dirs", [formulas_dir], "env")
  229. app.add_crossref_type(
  230. directivename="conf_master",
  231. rolename="conf_master",
  232. indextemplate="pair: %s; conf/master",
  233. )
  234. app.add_crossref_type(
  235. directivename="conf_minion",
  236. rolename="conf_minion",
  237. indextemplate="pair: %s; conf/minion",
  238. )
  239. app.add_crossref_type(
  240. directivename="conf_proxy",
  241. rolename="conf_proxy",
  242. indextemplate="pair: %s; conf/proxy",
  243. )
  244. app.add_crossref_type(
  245. directivename="conf_log",
  246. rolename="conf_log",
  247. indextemplate="pair: %s; conf/logging",
  248. )
  249. app.add_crossref_type(
  250. directivename="jinja_ref",
  251. rolename="jinja_ref",
  252. indextemplate="pair: %s; jinja filters",
  253. )