dochelpers.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. #!/usr/bin/python2 -O
  2. # vim: fileencoding=utf-8
  3. #
  4. # The Qubes OS Project, https://www.qubes-os.org/
  5. #
  6. # Copyright (C) 2010-2015 Joanna Rutkowska <joanna@invisiblethingslab.com>
  7. # Copyright (C) 2014-2015 Wojtek Porczyk <woju@invisiblethingslab.com>
  8. #
  9. # This program is free software; you can redistribute it and/or modify
  10. # it under the terms of the GNU General Public License as published by
  11. # the Free Software Foundation; either version 2 of the License, or
  12. # (at your option) any later version.
  13. #
  14. # This program is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. # GNU General Public License for more details.
  18. #
  19. # You should have received a copy of the GNU General Public License along
  20. # with this program; if not, write to the Free Software Foundation, Inc.,
  21. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  22. #
  23. '''Documentation helpers.
  24. This module contains classes and functions which help to maintain documentation,
  25. particularly our custom Sphinx extension.
  26. '''
  27. import csv
  28. import os
  29. import posixpath
  30. import re
  31. import StringIO
  32. import urllib2
  33. import docutils
  34. import docutils.nodes
  35. import docutils.parsers.rst
  36. import docutils.parsers.rst.roles
  37. import docutils.statemachine
  38. import sphinx
  39. import sphinx.errors
  40. import sphinx.locale
  41. import sphinx.util.docfields
  42. import qubes.tools
  43. def fetch_ticket_info(uri):
  44. '''Fetch info about particular trac ticket given
  45. :param str uri: URI at which ticket resides
  46. :rtype: mapping
  47. :raises: urllib2.HTTPError
  48. '''
  49. data = urllib2.urlopen(uri + '?format=csv').read()
  50. reader = csv.reader((line + '\n' for line in data.split('\r\n')),
  51. quoting=csv.QUOTE_MINIMAL, quotechar='"')
  52. return dict(zip(*((cell.decode('utf-8') for cell in row)
  53. for row in list(reader)[:2])))
  54. def ticket(name, rawtext, text, lineno, inliner, options=None, content=None):
  55. '''Link to qubes ticket
  56. :param str name: The role name used in the document
  57. :param str rawtext: The entire markup snippet, with role
  58. :param str text: The text marked with the role
  59. :param int lineno: The line number where rawtext appears in the input
  60. :param docutils.parsers.rst.states.Inliner inliner: The inliner instance \
  61. that called this function
  62. :param options: Directive options for customisation
  63. :param content: The directive content for customisation
  64. ''' # pylint: disable=unused-argument
  65. if options is None:
  66. options = {}
  67. ticketno = text.lstrip('#')
  68. if not ticketno.isdigit():
  69. msg = inliner.reporter.error(
  70. 'Invalid ticket identificator: {!r}'.format(text), line=lineno)
  71. prb = inliner.problematic(rawtext, rawtext, msg)
  72. return [prb], [msg]
  73. app = inliner.document.settings.env.app
  74. uri = posixpath.join(app.config.ticket_base_uri, ticketno)
  75. try:
  76. info = fetch_ticket_info(uri)
  77. except urllib2.HTTPError, e:
  78. msg = inliner.reporter.error(
  79. 'Error while fetching ticket info: {!s}'.format(e), line=lineno)
  80. prb = inliner.problematic(rawtext, rawtext, msg)
  81. return [prb], [msg]
  82. docutils.parsers.rst.roles.set_classes(options)
  83. node = docutils.nodes.reference(
  84. rawtext,
  85. '#{} ({})'.format(ticketno, info['summary']),
  86. refuri=uri,
  87. **options)
  88. return [node], []
  89. class versioncheck(docutils.nodes.warning):
  90. # pylint: disable=invalid-name
  91. pass
  92. def visit(self, node):
  93. self.visit_admonition(node, 'version')
  94. def depart(self, node):
  95. self.depart_admonition(node)
  96. sphinx.locale.admonitionlabels['version'] = 'Version mismatch'
  97. class VersionCheck(docutils.parsers.rst.Directive):
  98. '''Directive versioncheck
  99. Check if current version (from ``conf.py``) equals version specified as
  100. argument. If not, generate warning.'''
  101. has_content = True
  102. required_arguments = 1
  103. optional_arguments = 0
  104. final_argument_whitespace = True
  105. option_spec = {}
  106. def run(self):
  107. current = self.state.document.settings.env.app.config.version
  108. version = self.arguments[0]
  109. if current == version:
  110. return []
  111. text = ' '.join('''This manual page was written for version **{}**, but
  112. current version at the time when this page was generated is **{}**.
  113. This may or may not mean that page is outdated or has
  114. inconsistencies.'''.format(version, current).split())
  115. node = versioncheck(text)
  116. node['classes'] = ['admonition', 'warning']
  117. self.state.nested_parse(docutils.statemachine.StringList([text]),
  118. self.content_offset, node)
  119. return [node]
  120. def make_rst_section(heading, char):
  121. return '{}\n{}\n\n'.format(heading, char[0] * len(heading))
  122. def prepare_manpage(command):
  123. parser = qubes.tools.get_parser_for_command(command)
  124. stream = StringIO.StringIO()
  125. stream.write('.. program:: {}\n\n'.format(command))
  126. stream.write(make_rst_section(
  127. ':program:`{}` -- {}'.format(command, parser.description), '='))
  128. stream.write('''.. warning::
  129. This page was autogenerated from command-line parser. It shouldn't be 1:1
  130. conversion, because it would add little value. Please revise it and add
  131. more descriptive help, which normally won't fit in standard ``--help``
  132. option.
  133. After rewrite, please remove this admonition.\n\n''')
  134. stream.write(make_rst_section('Synopsis', '-'))
  135. usage = ' '.join(parser.format_usage().strip().split())
  136. if usage.startswith('usage: '):
  137. usage = usage[len('usage: '):]
  138. # replace METAVARS with *METAVARS*
  139. usage = re.sub(r'\b([A-Z]{2,})\b', r'*\1*', usage)
  140. stream.write(':command:`{}` {}\n\n'.format(command, usage))
  141. stream.write(make_rst_section('Options', '-'))
  142. for action in parser._actions: # pylint: disable=protected-access
  143. stream.write('.. option:: ')
  144. if action.metavar:
  145. stream.write(', '.join('{}{}{}'.format(
  146. option,
  147. '=' if option.startswith('--') else ' ',
  148. action.metavar)
  149. for option in sorted(action.option_strings)))
  150. else:
  151. stream.write(', '.join(sorted(action.option_strings)))
  152. stream.write('\n\n {}\n\n'.format(action.help))
  153. stream.write(make_rst_section('Authors', '-'))
  154. stream.write('''\
  155. | Joanna Rutkowska <joanna at invisiblethingslab dot com>
  156. | Rafal Wojtczuk <rafal at invisiblethingslab dot com>
  157. | Marek Marczykowski <marmarek at invisiblethingslab dot com>
  158. | Wojtek Porczyk <woju at invisiblethingslab dot com>
  159. .. vim: ts=3 sw=3 et tw=80
  160. ''')
  161. return stream.getvalue()
  162. class ArgumentCheckVisitor(docutils.nodes.SparseNodeVisitor):
  163. def __init__(self, app, command, document):
  164. docutils.nodes.SparseNodeVisitor.__init__(self, document)
  165. self.app = app
  166. self.command = command
  167. self.args = set()
  168. try:
  169. parser = qubes.tools.get_parser_for_command(command)
  170. except ImportError:
  171. self.app.warn('cannot import module for command')
  172. self.command = None
  173. return
  174. except AttributeError:
  175. raise sphinx.errors.SphinxError('cannot find parser in module')
  176. # pylint: disable=protected-access
  177. for action in parser._actions:
  178. self.args.update(action.option_strings)
  179. # pylint: disable=no-self-use,unused-argument
  180. def visit_desc(self, node):
  181. if not node.get('desctype', None) == 'option':
  182. raise docutils.nodes.SkipChildren
  183. def visit_desc_name(self, node):
  184. if self.command is None:
  185. return
  186. if not isinstance(node[0], docutils.nodes.Text):
  187. raise sphinx.errors.SphinxError('first child should be Text')
  188. arg = str(node[0])
  189. try:
  190. self.args.remove(arg)
  191. except KeyError:
  192. raise sphinx.errors.SphinxError(
  193. 'No such argument for {!r}: {!r}'.format(self.command, arg))
  194. def depart_document(self, node):
  195. if self.args:
  196. raise sphinx.errors.SphinxError(
  197. 'Undocumented arguments: {!r}'.format(
  198. ', '.join(sorted(self.args))))
  199. def check_man_args(app, doctree, docname):
  200. command = os.path.split(docname)[1]
  201. app.info('Checking arguments for {!r}'.format(command))
  202. doctree.walk(ArgumentCheckVisitor(app, command, doctree))
  203. #
  204. # this is lifted from sphinx' own conf.py
  205. #
  206. event_sig_re = re.compile(r'([a-zA-Z-:<>]+)\s*\((.*)\)')
  207. def parse_event(env, sig, signode):
  208. # pylint: disable=unused-argument
  209. m = event_sig_re.match(sig)
  210. if not m:
  211. signode += sphinx.addnodes.desc_name(sig, sig)
  212. return sig
  213. name, args = m.groups()
  214. signode += sphinx.addnodes.desc_name(name, name)
  215. plist = sphinx.addnodes.desc_parameterlist()
  216. for arg in args.split(','):
  217. arg = arg.strip()
  218. plist += sphinx.addnodes.desc_parameter(arg, arg)
  219. signode += plist
  220. return name
  221. #
  222. # end of codelifting
  223. #
  224. def break_to_pdb(app, *dummy):
  225. if not app.config.break_to_pdb:
  226. return
  227. import pdb
  228. pdb.set_trace()
  229. def setup(app):
  230. app.add_role('ticket', ticket)
  231. app.add_config_value('ticket_base_uri',
  232. 'https://wiki.qubes-os.org/ticket/', 'env')
  233. app.add_config_value('break_to_pdb', False, 'env')
  234. app.add_node(versioncheck,
  235. html=(visit, depart),
  236. man=(visit, depart))
  237. app.add_directive('versioncheck', VersionCheck)
  238. fdesc = sphinx.util.docfields.GroupedField('parameter', label='Parameters',
  239. names=['param'], can_collapse=True)
  240. app.add_object_type('event', 'event', 'pair: %s; event', parse_event,
  241. doc_field_types=[fdesc])
  242. app.connect('doctree-resolved', break_to_pdb)
  243. app.connect('doctree-resolved', check_man_args)
  244. # vim: ts=4 sw=4 et