1
0

saltdomain.py 9.1 KB

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