mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
243 lines
7.5 KiB
Python
Executable File
243 lines
7.5 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Checker for file headers
|
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Make sure each Python file has a correct file header
|
|
including copyright and license information.
|
|
|
|
:copyright: Copyright 2007-2015 by the Sphinx team, see AUTHORS.
|
|
:license: BSD, see LICENSE for details.
|
|
"""
|
|
from __future__ import print_function
|
|
|
|
import sys, os, re
|
|
import cStringIO
|
|
from optparse import OptionParser
|
|
from os.path import join, splitext, abspath
|
|
|
|
|
|
checkers = {}
|
|
|
|
def checker(*suffixes, **kwds):
|
|
only_pkg = kwds.pop('only_pkg', False)
|
|
def deco(func):
|
|
for suffix in suffixes:
|
|
checkers.setdefault(suffix, []).append(func)
|
|
func.only_pkg = only_pkg
|
|
return func
|
|
return deco
|
|
|
|
|
|
name_mail_re = r'[\w ]+(<.*?>)?'
|
|
copyright_re = re.compile(br'^ :copyright: Copyright 200\d(-20\d\d)? '
|
|
br'by %s(, %s)*[,.]$' %
|
|
(name_mail_re, name_mail_re))
|
|
license_re = re.compile(br" :license: (.*?).\n")
|
|
copyright_2_re = re.compile(br'^ %s(, %s)*[,.]$' %
|
|
(name_mail_re, name_mail_re))
|
|
coding_re = re.compile(br'coding[:=]\s*([-\w.]+)')
|
|
not_ix_re = re.compile(br'\bnot\s+\S+?\s+i[sn]\s\S+')
|
|
is_const_re = re.compile(br'if.*?==\s+(None|False|True)\b')
|
|
|
|
misspellings = [b"developement", b"adress", # ALLOW-MISSPELLING
|
|
b"verificate", b"informations"] # ALLOW-MISSPELLING
|
|
|
|
if sys.version_info < (3, 0):
|
|
@checker('.py')
|
|
def check_syntax(fn, lines):
|
|
try:
|
|
compile(b''.join(lines), fn, "exec")
|
|
except SyntaxError as err:
|
|
yield 0, "not compilable: %s" % err
|
|
|
|
|
|
@checker('.py')
|
|
def check_style_and_encoding(fn, lines):
|
|
encoding = 'ascii'
|
|
for lno, line in enumerate(lines):
|
|
if len(line) > 90:
|
|
yield lno+1, "line too long"
|
|
if lno < 2:
|
|
co = coding_re.search(line)
|
|
if co:
|
|
encoding = co.group(1).decode('ascii')
|
|
if line.strip().startswith(b'#'):
|
|
continue
|
|
#m = not_ix_re.search(line)
|
|
#if m:
|
|
# yield lno+1, '"' + m.group() + '"'
|
|
if is_const_re.search(line):
|
|
yield lno+1, 'using == None/True/False'
|
|
try:
|
|
line.decode(encoding)
|
|
except UnicodeDecodeError as err:
|
|
yield lno+1, "not decodable: %s\n Line: %r" % (err, line)
|
|
except LookupError as err:
|
|
yield 0, "unknown encoding: %s" % encoding
|
|
encoding = 'latin1'
|
|
|
|
|
|
@checker('.py', only_pkg=True)
|
|
def check_fileheader(fn, lines):
|
|
# line number correction
|
|
c = 1
|
|
if lines[0:1] == [b'#!/usr/bin/env python\n']:
|
|
lines = lines[1:]
|
|
c = 2
|
|
|
|
llist = []
|
|
docopen = False
|
|
for lno, l in enumerate(lines):
|
|
llist.append(l)
|
|
if lno == 0:
|
|
if l == b'# -*- coding: rot13 -*-\n':
|
|
# special-case pony package
|
|
return
|
|
elif l != b'# -*- coding: utf-8 -*-\n':
|
|
yield 1, "missing coding declaration"
|
|
elif lno == 1:
|
|
if l != b'"""\n' and l != b'r"""\n':
|
|
yield 2, 'missing docstring begin (""")'
|
|
else:
|
|
docopen = True
|
|
elif docopen:
|
|
if l == b'"""\n':
|
|
# end of docstring
|
|
if lno <= 4:
|
|
yield lno+c, "missing module name in docstring"
|
|
break
|
|
|
|
if l != b"\n" and l[:4] != b' ' and docopen:
|
|
yield lno+c, "missing correct docstring indentation"
|
|
|
|
if lno == 2:
|
|
# if not in package, don't check the module name
|
|
modname = fn[:-3].replace('/', '.').replace('.__init__', '')
|
|
while modname:
|
|
if l.lower()[4:-1] == bytes(modname):
|
|
break
|
|
modname = '.'.join(modname.split('.')[1:])
|
|
else:
|
|
yield 3, "wrong module name in docstring heading"
|
|
modnamelen = len(l.strip())
|
|
elif lno == 3:
|
|
if l.strip() != modnamelen * b"~":
|
|
yield 4, "wrong module name underline, should be ~~~...~"
|
|
|
|
else:
|
|
yield 0, "missing end and/or start of docstring..."
|
|
|
|
# check for copyright and license fields
|
|
license = llist[-2:-1]
|
|
if not license or not license_re.match(license[0]):
|
|
yield 0, "no correct license info"
|
|
|
|
ci = -3
|
|
copyright = llist[ci:ci+1]
|
|
while copyright and copyright_2_re.match(copyright[0]):
|
|
ci -= 1
|
|
copyright = llist[ci:ci+1]
|
|
if not copyright or not copyright_re.match(copyright[0]):
|
|
yield 0, "no correct copyright info"
|
|
|
|
|
|
@checker('.py', '.html', '.rst')
|
|
def check_whitespace_and_spelling(fn, lines):
|
|
for lno, line in enumerate(lines):
|
|
if b"\t" in line:
|
|
yield lno+1, "OMG TABS!!!1 "
|
|
if line[:-1].rstrip(b' \t') != line[:-1]:
|
|
yield lno+1, "trailing whitespace"
|
|
for word in misspellings:
|
|
if word in line and b'ALLOW-MISSPELLING' not in line:
|
|
yield lno+1, '"%s" used' % word
|
|
|
|
|
|
bad_tags = [b'<u>', b'<s>', b'<strike>', b'<center>', b'<font']
|
|
|
|
@checker('.html')
|
|
def check_xhtml(fn, lines):
|
|
for lno, line in enumerate(lines):
|
|
for bad_tag in bad_tags:
|
|
if bad_tag in line:
|
|
yield lno+1, "used " + bad_tag
|
|
|
|
|
|
def main(argv):
|
|
parser = OptionParser(usage='Usage: %prog [-v] [-i ignorepath]* [path]')
|
|
parser.add_option('-v', '--verbose', dest='verbose', default=False,
|
|
action='store_true')
|
|
parser.add_option('-i', '--ignore-path', dest='ignored_paths',
|
|
default=[], action='append')
|
|
options, args = parser.parse_args(argv[1:])
|
|
|
|
if len(args) == 0:
|
|
path = '.'
|
|
elif len(args) == 1:
|
|
path = args[0]
|
|
else:
|
|
print(args)
|
|
parser.error('No more then one path supported')
|
|
|
|
verbose = options.verbose
|
|
ignored_paths = set(abspath(p) for p in options.ignored_paths)
|
|
|
|
num = 0
|
|
out = cStringIO.StringIO()
|
|
|
|
for root, dirs, files in os.walk(path):
|
|
for vcs_dir in ['.svn', '.hg', '.git']:
|
|
if vcs_dir in dirs:
|
|
dirs.remove(vcs_dir)
|
|
if abspath(root) in ignored_paths:
|
|
del dirs[:]
|
|
continue
|
|
in_check_pkg = root.startswith('./sphinx')
|
|
for fn in files:
|
|
|
|
fn = join(root, fn)
|
|
if fn[:2] == './': fn = fn[2:]
|
|
|
|
if abspath(fn) in ignored_paths:
|
|
continue
|
|
|
|
ext = splitext(fn)[1]
|
|
checkerlist = checkers.get(ext, None)
|
|
if not checkerlist:
|
|
continue
|
|
|
|
if verbose:
|
|
print("Checking %s..." % fn)
|
|
|
|
try:
|
|
f = open(fn, 'rb')
|
|
try:
|
|
lines = list(f)
|
|
finally:
|
|
f.close()
|
|
except (IOError, OSError) as err:
|
|
print("%s: cannot open: %s" % (fn, err))
|
|
num += 1
|
|
continue
|
|
|
|
for checker in checkerlist:
|
|
if not in_check_pkg and checker.only_pkg:
|
|
continue
|
|
for lno, msg in checker(fn, lines):
|
|
print("%s:%d: %s" % (fn, lno, msg), file=out)
|
|
num += 1
|
|
if verbose:
|
|
print()
|
|
if num == 0:
|
|
print("No errors found.")
|
|
else:
|
|
print(out.getvalue().rstrip('\n'))
|
|
print("%d error%s found." % (num, num > 1 and "s" or ""))
|
|
return int(num > 0)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main(sys.argv))
|