of which are not only used for building the navigation, but also to actually tell py.test where docs should be generated. Also cleaned up and fixed the situation regarding relative links in the navigation menu and stylesheet/js paths. --HG-- branch : trunk
317 lines
11 KiB
Python
317 lines
11 KiB
Python
from __future__ import generators
|
|
import py
|
|
from py.__.misc import rest
|
|
from py.__.apigen.linker import relpath
|
|
import os
|
|
|
|
mypath = py.magic.autopath().dirpath()
|
|
|
|
Option = py.test.config.Option
|
|
option = py.test.config.addoptions("documentation check options",
|
|
Option('-R', '--checkremote',
|
|
action="store_true", dest="checkremote", default=False,
|
|
help="perform tests involving remote accesses (links, svn)"
|
|
),
|
|
Option('', '--forcegen',
|
|
action="store_true", dest="forcegen", default=False,
|
|
help="force generation of html files even if they appear up-to-date"
|
|
),
|
|
)
|
|
|
|
def get_apigenpath():
|
|
from py.__.conftest import option
|
|
path = os.environ.get('APIGENPATH')
|
|
if path is None:
|
|
path = option.apigenpath
|
|
return py.path.local(path)
|
|
|
|
def get_docpath():
|
|
from py.__.conftest import option
|
|
path = os.environ.get('DOCPATH')
|
|
if path is None:
|
|
path = option.docpath
|
|
return py.path.local(path)
|
|
|
|
def get_apigen_relpath():
|
|
return relpath(get_apigenpath().strpath, get_docpath().strpath)
|
|
|
|
def deindent(s, sep='\n'):
|
|
leastspaces = -1
|
|
lines = s.split(sep)
|
|
for line in lines:
|
|
if not line.strip():
|
|
continue
|
|
spaces = len(line) - len(line.lstrip())
|
|
if leastspaces == -1 or spaces < leastspaces:
|
|
leastspaces = spaces
|
|
if leastspaces == -1:
|
|
return s
|
|
for i, line in py.builtin.enumerate(lines):
|
|
if not line.strip():
|
|
lines[i] = ''
|
|
else:
|
|
lines[i] = line[leastspaces:]
|
|
return sep.join(lines)
|
|
|
|
_initialized = False
|
|
def checkdocutils():
|
|
global _initialized
|
|
try:
|
|
import docutils
|
|
except ImportError:
|
|
py.test.skip("docutils not importable")
|
|
if not _initialized:
|
|
from py.__.rest import directive
|
|
directive.register_linkrole('api', resolve_linkrole)
|
|
directive.register_linkrole('source', resolve_linkrole)
|
|
_initialized = True
|
|
|
|
def restcheck(path):
|
|
localpath = path
|
|
if hasattr(path, 'localpath'):
|
|
localpath = path.localpath
|
|
_checkskip(localpath)
|
|
checkdocutils()
|
|
import docutils.utils
|
|
|
|
try:
|
|
cur = localpath
|
|
for x in cur.parts(reverse=True):
|
|
confrest = x.dirpath('confrest.py')
|
|
if confrest.check(file=1):
|
|
confrest = confrest.pyimport()
|
|
confrest.Project().process(path)
|
|
break
|
|
else:
|
|
# defer to default processor
|
|
rest.process(path)
|
|
except KeyboardInterrupt:
|
|
raise
|
|
except docutils.utils.SystemMessage:
|
|
# we assume docutils printed info on stdout
|
|
py.test.fail("docutils processing failed, see captured stderr")
|
|
|
|
def _checkskip(lpath):
|
|
if not option.forcegen:
|
|
if lpath.ext == '.txt':
|
|
htmlpath = get_docpath().join(lpath.new(ext='.html').relto(mypath))
|
|
if htmlpath.check(file=1) and htmlpath.mtime() >= lpath.mtime():
|
|
py.test.skip("html file is up to date, use --forcegen to regenerate")
|
|
#return [] # no need to rebuild
|
|
|
|
class ReSTSyntaxTest(py.test.collect.Item):
|
|
def run(self):
|
|
mypath = self.fspath
|
|
_checkskip(mypath)
|
|
restcheck(py.path.svnwc(mypath))
|
|
|
|
class DoctestText(py.test.collect.Item):
|
|
def run(self):
|
|
# XXX quite nasty... but it works (fixes win32 issues)
|
|
s = self._normalize_linesep()
|
|
l = []
|
|
prefix = '.. >>> '
|
|
mod = py.std.types.ModuleType(self.fspath.purebasename)
|
|
for line in deindent(s).split('\n'):
|
|
stripped = line.strip()
|
|
if stripped.startswith(prefix):
|
|
exec py.code.Source(stripped[len(prefix):]).compile() in \
|
|
mod.__dict__
|
|
line = ""
|
|
else:
|
|
l.append(line)
|
|
docstring = "\n".join(l)
|
|
self.execute(mod, docstring)
|
|
|
|
def execute(self, mod, docstring):
|
|
mod.__doc__ = docstring
|
|
failed, tot = py.compat.doctest.testmod(mod, verbose=1)
|
|
if failed:
|
|
py.test.fail("doctest %s: %s failed out of %s" %(
|
|
self.fspath, failed, tot))
|
|
|
|
def _normalize_linesep(self):
|
|
s = self.fspath.read()
|
|
linesep = '\n'
|
|
if '\r' in s:
|
|
if '\n' not in s:
|
|
linesep = '\r'
|
|
else:
|
|
linesep = '\r\n'
|
|
s = s.replace(linesep, '\n')
|
|
return s
|
|
|
|
class LinkCheckerMaker(py.test.collect.Collector):
|
|
def run(self):
|
|
l = []
|
|
for call, tryfn, path, lineno in genlinkchecks(self.fspath):
|
|
l.append(tryfn)
|
|
return l
|
|
|
|
def join(self, name):
|
|
for call, tryfn, path, lineno in genlinkchecks(self.fspath):
|
|
if tryfn == name:
|
|
return CheckLink(name, parent=self, args=(tryfn, path, lineno), obj=call)
|
|
|
|
class CheckLink(py.test.collect.Function):
|
|
def setup(self):
|
|
pass
|
|
def teardown(self):
|
|
pass
|
|
|
|
class ReSTChecker(py.test.collect.Module):
|
|
DoctestText = DoctestText
|
|
ReSTSyntaxTest = ReSTSyntaxTest
|
|
|
|
def __repr__(self):
|
|
return py.test.collect.Collector.__repr__(self)
|
|
|
|
def setup(self):
|
|
pass
|
|
def teardown(self):
|
|
pass
|
|
def run(self):
|
|
return [self.fspath.basename, 'checklinks', 'doctest']
|
|
def join(self, name):
|
|
if name == self.fspath.basename:
|
|
return self.ReSTSyntaxTest(name, parent=self)
|
|
elif name == 'checklinks':
|
|
return LinkCheckerMaker(name, self)
|
|
elif name == 'doctest':
|
|
return self.DoctestText(name, self)
|
|
|
|
# generating functions + args as single tests
|
|
def genlinkchecks(path):
|
|
for lineno, line in py.builtin.enumerate(path.readlines()):
|
|
line = line.strip()
|
|
if line.startswith('.. _'):
|
|
if line.startswith('.. _`'):
|
|
delim = '`:'
|
|
else:
|
|
delim = ':'
|
|
l = line.split(delim, 1)
|
|
if len(l) != 2:
|
|
continue
|
|
tryfn = l[1].strip()
|
|
if tryfn.startswith('http:') or tryfn.startswith('https'):
|
|
if option.checkremote:
|
|
yield urlcheck, tryfn, path, lineno
|
|
elif tryfn.startswith('webcal:'):
|
|
continue
|
|
else:
|
|
i = tryfn.find('#')
|
|
if i != -1:
|
|
checkfn = tryfn[:i]
|
|
else:
|
|
checkfn = tryfn
|
|
if checkfn.strip() and (1 or checkfn.endswith('.html')):
|
|
yield localrefcheck, tryfn, path, lineno
|
|
|
|
def urlcheck(tryfn, path, lineno):
|
|
try:
|
|
print "trying remote", tryfn
|
|
py.std.urllib2.urlopen(tryfn)
|
|
except (py.std.urllib2.URLError, py.std.urllib2.HTTPError), e:
|
|
if e.code in (401, 403): # authorization required, forbidden
|
|
py.test.skip("%s: %s" %(tryfn, str(e)))
|
|
else:
|
|
py.test.fail("remote reference error %r in %s:%d\n%s" %(
|
|
tryfn, path.basename, lineno+1, e))
|
|
|
|
def localrefcheck(tryfn, path, lineno):
|
|
# assume it should be a file
|
|
i = tryfn.find('#')
|
|
if tryfn.startswith('javascript:'):
|
|
return # don't check JS refs
|
|
if i != -1:
|
|
anchor = tryfn[i+1:]
|
|
tryfn = tryfn[:i]
|
|
else:
|
|
anchor = ''
|
|
fn = path.dirpath(tryfn)
|
|
ishtml = fn.ext == '.html'
|
|
fn = ishtml and fn.new(ext='.txt') or fn
|
|
print "filename is", fn
|
|
if not fn.check(): # not ishtml or not fn.check():
|
|
py.test.fail("reference error %r in %s:%d" %(
|
|
tryfn, path.basename, lineno+1))
|
|
if anchor:
|
|
source = unicode(fn.read(), 'latin1')
|
|
source = source.lower().replace('-', ' ') # aehem
|
|
|
|
anchor = anchor.replace('-', ' ')
|
|
match2 = ".. _`%s`:" % anchor
|
|
match3 = ".. _%s:" % anchor
|
|
candidates = (anchor, match2, match3)
|
|
print "candidates", repr(candidates)
|
|
for line in source.split('\n'):
|
|
line = line.strip()
|
|
if line in candidates:
|
|
break
|
|
else:
|
|
py.test.fail("anchor reference error %s#%s in %s:%d" %(
|
|
tryfn, anchor, path.basename, lineno+1))
|
|
|
|
|
|
# ___________________________________________________________
|
|
#
|
|
# hooking into py.test Directory collector's chain ...
|
|
|
|
class DocDirectory(py.test.collect.Directory):
|
|
ReSTChecker = ReSTChecker
|
|
|
|
def run(self):
|
|
results = super(DocDirectory, self).run()
|
|
for x in self.fspath.listdir('*.txt', sort=True):
|
|
results.append(x.basename)
|
|
return results
|
|
|
|
def join(self, name):
|
|
if not name.endswith('.txt'):
|
|
return super(DocDirectory, self).join(name)
|
|
p = self.fspath.join(name)
|
|
if p.check(file=1):
|
|
return self.ReSTChecker(p, parent=self)
|
|
Directory = DocDirectory
|
|
|
|
def resolve_linkrole(name, text, check=True):
|
|
apigen_relpath = get_apigen_relpath()
|
|
if name == 'api':
|
|
if text == 'py':
|
|
return ('py', apigen_relpath + 'api/index.html')
|
|
else:
|
|
assert text.startswith('py.'), (
|
|
'api link "%s" does not point to the py package') % (text,)
|
|
dotted_name = text
|
|
if dotted_name.find('(') > -1:
|
|
dotted_name = dotted_name[:text.find('(')]
|
|
# remove pkg root
|
|
path = dotted_name.split('.')[1:]
|
|
dotted_name = '.'.join(path)
|
|
obj = py
|
|
if check:
|
|
for chunk in path:
|
|
try:
|
|
obj = getattr(obj, chunk)
|
|
except AttributeError:
|
|
raise AssertionError(
|
|
'problem with linkrole :api:`%s`: can not resolve '
|
|
'dotted name %s' % (text, dotted_name,))
|
|
return (text, apigen_relpath + 'api/%s.html' % (dotted_name,))
|
|
elif name == 'source':
|
|
assert text.startswith('py/'), ('source link "%s" does not point '
|
|
'to the py package') % (text,)
|
|
relpath = '/'.join(text.split('/')[1:])
|
|
if check:
|
|
pkgroot = py.__package__.getpath()
|
|
abspath = pkgroot.join(relpath)
|
|
assert pkgroot.join(relpath).check(), (
|
|
'problem with linkrole :source:`%s`: '
|
|
'path %s does not exist' % (text, relpath))
|
|
if relpath.endswith('/') or not relpath:
|
|
relpath += 'index.html'
|
|
else:
|
|
relpath += '.html'
|
|
return (text, apigen_relpath + 'source/%s' % (relpath,))
|
|
|