dochelpers.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. #!/usr/bin/python2 -O
  2. # -*- coding: utf-8 -*-
  3. '''Documentation helpers
  4. This module contains classes and functions which help to maintain documentation,
  5. particularly our custom Sphinx extension.
  6. '''
  7. import csv
  8. import posixpath
  9. import re
  10. import sys
  11. import urllib2
  12. import docutils
  13. import docutils.nodes
  14. import docutils.parsers.rst
  15. import docutils.parsers.rst.roles
  16. import docutils.statemachine
  17. import sphinx
  18. import sphinx.locale
  19. import sphinx.util.docfields
  20. def fetch_ticket_info(uri):
  21. '''Fetch info about particular trac ticket given
  22. :param str uri: URI at which ticket resides
  23. :rtype: mapping
  24. :raises: urllib2.HTTPError
  25. '''
  26. data = urllib2.urlopen(uri + '?format=csv').read()
  27. reader = csv.reader((line + '\n' for line in data.split('\r\n')),
  28. quoting=csv.QUOTE_MINIMAL, quotechar='"')
  29. return dict(zip(*((cell.decode('utf-8') for cell in row)
  30. for row in list(reader)[:2])))
  31. def ticket(name, rawtext, text, lineno, inliner, options={}, content=[]):
  32. '''Link to qubes ticket
  33. :param str name: The role name used in the document
  34. :param str rawtext: The entire markup snippet, with role
  35. :param str text: The text marked with the role
  36. :param int lineno: The line number where rawtext appears in the input
  37. :param docutils.parsers.rst.states.Inliner inliner: The inliner instance \
  38. that called this function
  39. :param options: Directive options for customisation
  40. :param content: The directive content for customisation
  41. '''
  42. ticket = text.lstrip('#')
  43. if not ticket.isdigit():
  44. msg = inliner.reporter.error(
  45. 'Invalid ticket identificator: {!r}'.format(text), line=lineno)
  46. prb = inliner.problematic(rawtext, rawtext, msg)
  47. return [prb], [msg]
  48. app = inliner.document.settings.env.app
  49. uri = posixpath.join(app.config.ticket_base_uri, ticket)
  50. try:
  51. info = fetch_ticket_info(uri)
  52. except urllib2.HTTPError, e:
  53. msg = inliner.reporter.error(
  54. 'Error while fetching ticket info: {!s}'.format(e), line=lineno)
  55. prb = inliner.problematic(rawtext, rawtext, msg)
  56. return [prb], [msg]
  57. docutils.parsers.rst.roles.set_classes(options)
  58. node = docutils.nodes.reference(
  59. rawtext,
  60. '#{} ({})'.format(ticket, info['summary']),
  61. refuri=uri,
  62. **options)
  63. return [node], []
  64. class versioncheck(docutils.nodes.warning):
  65. pass
  66. def visit(self, node):
  67. self.visit_admonition(node, 'version')
  68. def depart(self, node):
  69. self.depart_admonition(node)
  70. sphinx.locale.admonitionlabels['version'] = 'Version mismatch'
  71. class VersionCheck(docutils.parsers.rst.Directive):
  72. '''Directive versioncheck
  73. Check if current version (from ``conf.py``) equals version specified as
  74. argument. If not, generate warning.'''
  75. has_content = True
  76. required_arguments = 1
  77. optional_arguments = 0
  78. final_argument_whitespace = True
  79. option_spec = {}
  80. def run(self):
  81. current = self.state.document.settings.env.app.config.version
  82. version = self.arguments[0]
  83. if current == version:
  84. return []
  85. text = ' '.join('''This manual page was written for version **{}**, but
  86. current version at the time when this page was generated is **{}**.
  87. This may or may not mean that page is outdated or has
  88. inconsistencies.'''.format(version, current).split())
  89. node = versioncheck(text)
  90. node['classes'] = ['admonition', 'warning']
  91. self.state.nested_parse(docutils.statemachine.StringList([text]),
  92. self.content_offset, node)
  93. return [node]
  94. #
  95. # this is lifted from sphinx' own conf.py
  96. #
  97. event_sig_re = re.compile(r'([a-zA-Z-:<>]+)\s*\((.*)\)')
  98. def parse_event(env, sig, signode):
  99. m = event_sig_re.match(sig)
  100. if not m:
  101. signode += sphinx.addnodes.desc_name(sig, sig)
  102. return sig
  103. name, args = m.groups()
  104. signode += sphinx.addnodes.desc_name(name, name)
  105. plist = sphinx.addnodes.desc_parameterlist()
  106. for arg in args.split(','):
  107. arg = arg.strip()
  108. plist += sphinx.addnodes.desc_parameter(arg, arg)
  109. signode += plist
  110. return name
  111. #
  112. # end of codelifting
  113. #
  114. def setup(app):
  115. app.add_role('ticket', ticket)
  116. app.add_config_value('ticket_base_uri',
  117. 'https://wiki.qubes-os.org/ticket/', 'env')
  118. app.add_node(versioncheck,
  119. html=(visit, depart),
  120. man=(visit, depart))
  121. app.add_directive('versioncheck', VersionCheck)
  122. fdesc = sphinx.util.docfields.GroupedField('parameter', label='Parameters',
  123. names=['param'], can_collapse=True)
  124. app.add_object_type('event', 'event', 'pair: %s; event', parse_event,
  125. doc_field_types=[fdesc])
  126. # vim: ts=4 sw=4 et