/Doc/tools/rstlint.py

http://unladen-swallow.googlecode.com/ · Python · 233 lines · 175 code · 36 blank · 22 comment · 60 complexity · df4fbcb29e2ff751d4cd77f2853f832e MD5 · raw file

  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. # Check for stylistic and formal issues in .rst and .py
  4. # files included in the documentation.
  5. #
  6. # 01/2009, Georg Brandl
  7. # TODO: - wrong versions in versionadded/changed
  8. # - wrong markup after versionchanged directive
  9. from __future__ import with_statement
  10. import os
  11. import re
  12. import sys
  13. import getopt
  14. import subprocess
  15. from os.path import join, splitext, abspath, exists
  16. from collections import defaultdict
  17. directives = [
  18. # standard docutils ones
  19. 'admonition', 'attention', 'caution', 'class', 'compound', 'container',
  20. 'contents', 'csv-table', 'danger', 'date', 'default-role', 'epigraph',
  21. 'error', 'figure', 'footer', 'header', 'highlights', 'hint', 'image',
  22. 'important', 'include', 'line-block', 'list-table', 'meta', 'note',
  23. 'parsed-literal', 'pull-quote', 'raw', 'replace',
  24. 'restructuredtext-test-directive', 'role', 'rubric', 'sectnum', 'sidebar',
  25. 'table', 'target-notes', 'tip', 'title', 'topic', 'unicode', 'warning',
  26. # Sphinx custom ones
  27. 'acks', 'attribute', 'autoattribute', 'autoclass', 'autodata',
  28. 'autoexception', 'autofunction', 'automethod', 'automodule', 'centered',
  29. 'cfunction', 'class', 'classmethod', 'cmacro', 'cmdoption', 'cmember',
  30. 'code-block', 'confval', 'cssclass', 'ctype', 'currentmodule', 'cvar',
  31. 'data', 'deprecated', 'describe', 'directive', 'doctest', 'envvar', 'event',
  32. 'exception', 'function', 'glossary', 'highlight', 'highlightlang', 'index',
  33. 'literalinclude', 'method', 'module', 'moduleauthor', 'productionlist',
  34. 'program', 'role', 'sectionauthor', 'seealso', 'sourcecode', 'staticmethod',
  35. 'tabularcolumns', 'testcode', 'testoutput', 'testsetup', 'toctree', 'todo',
  36. 'todolist', 'versionadded', 'versionchanged'
  37. ]
  38. all_directives = '(' + '|'.join(directives) + ')'
  39. seems_directive_re = re.compile(r'\.\. %s([^a-z:]|:(?!:))' % all_directives)
  40. default_role_re = re.compile(r'(^| )`\w([^`]*?\w)?`($| )')
  41. leaked_markup_re = re.compile(r'[a-z]::[^=]|:[a-z]+:|`|\.\.\s*\w+:')
  42. checkers = {}
  43. checker_props = {'severity': 1, 'falsepositives': False}
  44. def checker(*suffixes, **kwds):
  45. """Decorator to register a function as a checker."""
  46. def deco(func):
  47. for suffix in suffixes:
  48. checkers.setdefault(suffix, []).append(func)
  49. for prop in checker_props:
  50. setattr(func, prop, kwds.get(prop, checker_props[prop]))
  51. return func
  52. return deco
  53. @checker('.py', severity=4)
  54. def check_syntax(fn, lines):
  55. """Check Python examples for valid syntax."""
  56. code = ''.join(lines)
  57. if '\r' in code:
  58. if os.name != 'nt':
  59. yield 0, '\\r in code file'
  60. code = code.replace('\r', '')
  61. try:
  62. compile(code, fn, 'exec')
  63. except SyntaxError, err:
  64. yield err.lineno, 'not compilable: %s' % err
  65. @checker('.rst', severity=2)
  66. def check_suspicious_constructs(fn, lines):
  67. """Check for suspicious reST constructs."""
  68. inprod = False
  69. for lno, line in enumerate(lines):
  70. if seems_directive_re.match(line):
  71. yield lno+1, 'comment seems to be intended as a directive'
  72. if '.. productionlist::' in line:
  73. inprod = True
  74. elif not inprod and default_role_re.search(line):
  75. yield lno+1, 'default role used'
  76. elif inprod and not line.strip():
  77. inprod = False
  78. @checker('.py', '.rst')
  79. def check_whitespace(fn, lines):
  80. """Check for whitespace and line length issues."""
  81. for lno, line in enumerate(lines):
  82. if '\r' in line:
  83. yield lno+1, '\\r in line'
  84. if '\t' in line:
  85. yield lno+1, 'OMG TABS!!!1'
  86. if line[:-1].rstrip(' \t') != line[:-1]:
  87. yield lno+1, 'trailing whitespace'
  88. @checker('.rst', severity=0)
  89. def check_line_length(fn, lines):
  90. """Check for line length; this checker is not run by default."""
  91. for lno, line in enumerate(lines):
  92. if len(line) > 81:
  93. # don't complain about tables, links and function signatures
  94. if line.lstrip()[0] not in '+|' and \
  95. 'http://' not in line and \
  96. not line.lstrip().startswith(('.. function',
  97. '.. method',
  98. '.. cfunction')):
  99. yield lno+1, "line too long"
  100. @checker('.html', severity=2, falsepositives=True)
  101. def check_leaked_markup(fn, lines):
  102. """Check HTML files for leaked reST markup; this only works if
  103. the HTML files have been built.
  104. """
  105. for lno, line in enumerate(lines):
  106. if leaked_markup_re.search(line):
  107. yield lno+1, 'possibly leaked markup: %r' % line
  108. def main(argv):
  109. usage = '''\
  110. Usage: %s [-v] [-f] [-s sev] [-i path]* [path]
  111. Options: -v verbose (print all checked file names)
  112. -f enable checkers that yield many false positives
  113. -s sev only show problems with severity >= sev
  114. -i path ignore subdir or file path
  115. ''' % argv[0]
  116. try:
  117. gopts, args = getopt.getopt(argv[1:], 'vfs:i:')
  118. except getopt.GetoptError:
  119. print usage
  120. return 2
  121. verbose = False
  122. severity = 1
  123. ignore = []
  124. falsepos = False
  125. for opt, val in gopts:
  126. if opt == '-v':
  127. verbose = True
  128. elif opt == '-f':
  129. falsepos = True
  130. elif opt == '-s':
  131. severity = int(val)
  132. elif opt == '-i':
  133. ignore.append(abspath(val))
  134. if len(args) == 0:
  135. path = '.'
  136. elif len(args) == 1:
  137. path = args[0]
  138. else:
  139. print usage
  140. return 2
  141. if not exists(path):
  142. print 'Error: path %s does not exist' % path
  143. return 2
  144. count = defaultdict(int)
  145. out = sys.stdout
  146. for root, dirs, files in os.walk(path):
  147. # ignore subdirs controlled by svn
  148. if '.svn' in dirs:
  149. dirs.remove('.svn')
  150. # ignore subdirs in ignore list
  151. if abspath(root) in ignore:
  152. del dirs[:]
  153. continue
  154. for fn in files:
  155. fn = join(root, fn)
  156. if fn[:2] == './':
  157. fn = fn[2:]
  158. # ignore files in ignore list
  159. if abspath(fn) in ignore:
  160. continue
  161. ext = splitext(fn)[1]
  162. checkerlist = checkers.get(ext, None)
  163. if not checkerlist:
  164. continue
  165. if verbose:
  166. print 'Checking %s...' % fn
  167. try:
  168. with open(fn, 'r') as f:
  169. lines = list(f)
  170. except (IOError, OSError), err:
  171. print '%s: cannot open: %s' % (fn, err)
  172. count[4] += 1
  173. continue
  174. for checker in checkerlist:
  175. if checker.falsepositives and not falsepos:
  176. continue
  177. csev = checker.severity
  178. if csev >= severity:
  179. for lno, msg in checker(fn, lines):
  180. print >>out, '[%d] %s:%d: %s' % (csev, fn, lno, msg)
  181. count[csev] += 1
  182. if verbose:
  183. print
  184. if not count:
  185. if severity > 1:
  186. print 'No problems with severity >= %d found.' % severity
  187. else:
  188. print 'No problems found.'
  189. else:
  190. for severity in sorted(count):
  191. number = count[severity]
  192. print '%d problem%s with severity %d found.' % \
  193. (number, number > 1 and 's' or '', severity)
  194. return int(bool(count))
  195. if __name__ == '__main__':
  196. sys.exit(main(sys.argv))