python-peps/pep2pyramid.py

547 lines
17 KiB
Python
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python
"""
Convert PEPs to (X)HTML fragments for Pyramid - courtesy of /F
Usage: %(PROGRAM)s [options] [<peps> ...]
Options:
-d <DIR>, --destdir <DIR>
Specify the base destination directory for Pyramid files.
Default: %(SERVER_DEST_DIR_BASE)s
-f, --force
Force the rebuilding of output files, regardless of modification times.
-k, --keep-going
Continue building past errors if possible.
-q, --quiet
Turn off verbose messages.
-h, --help
Print this help message and exit.
The optional arguments ``peps`` are either pep numbers or .txt files.
"""
import sys
import os
import codecs
import re
import cgi
import glob
import getopt
import errno
import random
import time
import shutil
REQUIRES = {'python': '2.2',
'docutils': '0.5'}
PROGRAM = sys.argv[0]
SERVER_DEST_DIR_BASE = (
'/data/ftp.python.org/pub/beta.python.org/build/data/dev/peps')
RFCURL = 'http://www.faqs.org/rfcs/rfc%d.html'
PEPCVSURL = 'http://svn.python.org/view/*checkout*/peps/trunk/pep-%04d.txt'
PEPDIRURL = '/dev/peps/'
PEPURL = PEPDIRURL + 'pep-%04d'
PEPANCHOR = '<a href="' + PEPURL + '">%i</a>'
LOCALVARS = "Local Variables:"
COMMENT = """<!--
This HTML is auto-generated. DO NOT EDIT THIS FILE! If you are writing a new
PEP, see http://www.python.org/dev/peps/pep-0001 for instructions and links
to templates. DO NOT USE THIS HTML FILE AS YOUR TEMPLATE!
-->"""
# The generated HTML doesn't validate -- you cannot use <hr> and <h3> inside
# <pre> tags. But if I change that, the result doesn't look very nice...
fixpat = re.compile("((https?|ftp):[-_a-zA-Z0-9/.+~:?#$=&,]+)|(pep-\d+(.txt)?)|"
"(RFC[- ]?(?P<rfcnum>\d+))|"
"(PEP\s+(?P<pepnum>\d+))|"
".")
CONTENT_HTML = """\
<n:invisible n:data="content" n:render="mapping">
<div id="breadcrumb" n:data="breadcrumb" n:render="breadcrumb" />
<n:slot name="text"></n:slot>
</n:invisible>
"""
CONTENT_YML = """\
--- !fragment
# Type of template to use
template: content.html
# The data to pass to the template
local:
content:
breadcrumb: !breadcrumb nav.yml nav
text: !htmlfile body.html
"""
INDEX_YML = """\
--- !fragment
template: index.html
# The data to pass to the template
local:
title: "%s"
content: !fragment content.yml
"""
EMPTYSTRING = ''
SPACE = ' '
COMMASPACE = ', '
class Settings:
# defaults:
verbose = True
keep_going = False
force_rebuild = False
dest_dir_base = SERVER_DEST_DIR_BASE
settings = Settings()
def usage(code, msg=''):
"""Print usage message and exit. Uses stderr if code != 0."""
if code == 0:
out = sys.stdout
else:
out = sys.stderr
print >> out, __doc__ % globals()
if msg:
print >> out, msg
sys.exit(code)
def fixanchor(current, match):
text = match.group(0)
link = None
if (text.startswith('http:') or text.startswith('https:')
or text.startswith('ftp:')):
# Strip off trailing punctuation. Pattern taken from faqwiz.
ltext = list(text)
while ltext:
c = ltext.pop()
if c not in '();:,.?\'"<>':
ltext.append(c)
break
link = EMPTYSTRING.join(ltext)
elif text.endswith('.txt') and text <> current:
link = PEPDIRURL + os.path.splitext(text)[0] + '/' + text
elif text.startswith('pep-') and text <> current:
link = os.path.splitext(text)[0] + ".html"
elif text.startswith('PEP'):
pepnum = int(match.group('pepnum'))
link = PEPURL % pepnum
elif text.startswith('RFC'):
rfcnum = int(match.group('rfcnum'))
link = RFCURL % rfcnum
if link:
return '<a href="%s">%s</a>' % (cgi.escape(link), cgi.escape(text))
return cgi.escape(match.group(0)) # really slow, but it works...
NON_MASKED_EMAILS = [
'peps@python.org',
'python-list@python.org',
'python-dev@python.org',
]
def fixemail(address, pepno):
if address.lower() in NON_MASKED_EMAILS:
# return hyperlinked version of email address
return linkemail(address, pepno)
else:
# return masked version of email address
parts = address.split('@', 1)
return '%s&#32;&#97;t&#32;%s' % (parts[0], parts[1])
def linkemail(address, pepno):
parts = address.split('@', 1)
return ('<a href="mailto:%s&#64;%s?subject=PEP%%20%s">'
'%s&#32;&#97;t&#32;%s</a>'
% (parts[0], parts[1], pepno, parts[0], parts[1]))
def fixfile(inpath, input_lines, outfile):
from email.Utils import parseaddr
basename = os.path.basename(inpath)
infile = iter(input_lines)
# head
header = []
pep = ""
title = ""
for line in infile:
if not line.strip():
break
if line[0].strip():
if ":" not in line:
break
key, value = line.split(":", 1)
value = value.strip()
header.append((key, value))
else:
# continuation line
key, value = header[-1]
value = value + line
header[-1] = key, value
if key.lower() == "title":
title = value
elif key.lower() == "pep":
pep = value
if pep:
title = "PEP " + pep + " -- " + title
r = random.choice(range(64))
print >> outfile, COMMENT
print >> outfile, '<div class="header">\n<table border="0" class="rfc2822">'
for k, v in header:
if k.lower() in ('author', 'discussions-to'):
mailtos = []
for part in re.split(',\s*', v):
if '@' in part:
realname, addr = parseaddr(part)
if k.lower() == 'discussions-to':
m = linkemail(addr, pep)
else:
m = fixemail(addr, pep)
mailtos.append('%s &lt;%s&gt;' % (realname, m))
elif part.startswith('http:'):
mailtos.append(
'<a href="%s">%s</a>' % (part, part))
else:
mailtos.append(part)
v = COMMASPACE.join(mailtos)
elif k.lower() in ('replaces', 'replaced-by', 'requires'):
otherpeps = ''
for otherpep in re.split(',?\s+', v):
otherpep = int(otherpep)
otherpeps += PEPANCHOR % (otherpep, otherpep)
v = otherpeps
elif k.lower() in ('last-modified',):
date = v or time.strftime('%Y-%m-%d',
time.localtime(os.stat(inpath)[8]))
if date.startswith('$' 'Date: ') and date.endswith(' $'):
date = date[6:-2]
try:
url = PEPCVSURL % int(pep)
v = '<a href="%s">%s</a> ' % (url, cgi.escape(date))
except ValueError, error:
v = date
elif k.lower() == 'content-type':
url = PEPURL % 9
pep_type = v or 'text/plain'
v = '<a href="%s">%s</a> ' % (url, cgi.escape(pep_type))
elif k.lower() == 'version':
if v.startswith('$' 'Revision: ') and v.endswith(' $'):
v = cgi.escape(v[11:-2])
else:
v = cgi.escape(v)
print >> outfile, (' <tr><th class="field-name">%s:&nbsp;</th>'
'<td>%s</td></tr>' % (cgi.escape(k), v))
print >> outfile, '</table>'
print >> outfile, '</div>'
need_pre = 1
for line in infile:
if line[0] == '\f':
continue
if line.strip() == LOCALVARS:
break
if line[0].strip():
if not need_pre:
print >> outfile, '</pre>'
print >> outfile, '<h3>%s</h3>' % line.strip()
need_pre = 1
elif not line.strip() and need_pre:
continue
else:
# PEP 0 has some special treatment
if basename == 'pep-0000.txt':
parts = line.split()
if len(parts) > 1 and re.match(r'\s*\d{1,4}', parts[1]):
# This is a PEP summary line, which we need to hyperlink
url = PEPURL % int(parts[1])
if need_pre:
print >> outfile, '<pre>'
need_pre = 0
print >> outfile, re.sub(
parts[1],
'<a href="/dev/peps/pep-%04d/">%s</a>' % (int(parts[1]),
parts[1]), line, 1),
continue
elif parts and '@' in parts[-1]:
# This is a pep email address line, so filter it.
url = fixemail(parts[-1], pep)
if need_pre:
print >> outfile, '<pre>'
need_pre = 0
print >> outfile, re.sub(
parts[-1], url, line, 1),
continue
line = fixpat.sub(lambda x, c=inpath: fixanchor(c, x), line)
if need_pre:
print >> outfile, '<pre>'
need_pre = 0
outfile.write(line)
if not need_pre:
print >> outfile, '</pre>'
return title
docutils_settings = None
"""Runtime settings object used by Docutils. Can be set by the client
application when this module is imported."""
def fix_rst_pep(inpath, input_lines, outfile):
from docutils import core
parts = core.publish_parts(
source=''.join(input_lines),
source_path=inpath,
destination_path=outfile.name,
reader_name='pep',
parser_name='restructuredtext',
writer_name='pep_html',
settings=docutils_settings,
# Allow Docutils traceback if there's an exception:
settings_overrides={'traceback': 1})
outfile.write(parts['whole'])
title = 'PEP %s -- %s' % (parts['pepnum'], parts['title'][0])
return title
def get_pep_type(input_lines):
"""
Return the Content-Type of the input. "text/plain" is the default.
Return ``None`` if the input is not a PEP.
"""
pep_type = None
for line in input_lines:
line = line.rstrip().lower()
if not line:
# End of the RFC 2822 header (first blank line).
break
elif line.startswith('content-type: '):
pep_type = line.split()[1] or 'text/plain'
break
elif line.startswith('pep: '):
# Default PEP type, used if no explicit content-type specified:
pep_type = 'text/plain'
return pep_type
def get_input_lines(inpath):
try:
infile = codecs.open(inpath, 'r', 'utf-8')
except IOError, e:
if e.errno <> errno.ENOENT: raise
print >> sys.stderr, 'Error: Skipping missing PEP file:', e.filename
sys.stderr.flush()
return None, None
lines = infile.read().splitlines(1) # handles x-platform line endings
infile.close()
return lines
def find_pep(pep_str):
"""Find the .txt file indicated by a cmd line argument"""
if os.path.exists(pep_str):
return pep_str
num = int(pep_str)
return "pep-%04d.txt" % num
def make_html(inpath):
input_lines = get_input_lines(inpath)
pep_type = get_pep_type(input_lines)
if pep_type is None:
print >> sys.stderr, 'Error: Input file %s is not a PEP.' % inpath
sys.stdout.flush()
return None
elif not PEP_TYPE_DISPATCH.has_key(pep_type):
print >> sys.stderr, ('Error: Unknown PEP type for input file %s: %s'
% (inpath, pep_type))
sys.stdout.flush()
return None
elif PEP_TYPE_DISPATCH[pep_type] == None:
pep_type_error(inpath, pep_type)
return None
destDir, needSvn, pepnum = set_up_pyramid(inpath)
outpath = os.path.join(destDir, 'body.html')
if ( not settings.force_rebuild
and (os.path.exists(outpath)
and os.stat(inpath).st_mtime <= os.stat(outpath).st_mtime)):
if settings.verbose:
print "Skipping %s (outfile up to date)"%(inpath)
return
if settings.verbose:
print inpath, "(%s)" % pep_type, "->", outpath
sys.stdout.flush()
outfile = codecs.open(outpath, "w", "utf-8")
title = PEP_TYPE_DISPATCH[pep_type](inpath, input_lines, outfile)
outfile.close()
os.chmod(outfile.name, 0664)
write_pyramid_index(destDir, title)
# for PEP 0, copy body to parent directory as well
if pepnum == '0000':
shutil.copyfile(outpath, os.path.join(destDir, '..', 'body.html'))
copy_aux_files(inpath, destDir)
return outpath
def set_up_pyramid(inpath):
m = re.search(r'pep-(\d+)\.', inpath)
if not m:
print >>sys.stderr, "Can't find PEP number in file name."
sys.exit(1)
pepnum = m.group(1)
destDir = os.path.join(settings.dest_dir_base, 'pep-%s' % pepnum)
needSvn = 0
if not os.path.exists(destDir):
needSvn = 1
os.mkdir(destDir)
# write content.html
foofilename = os.path.join(destDir, 'content.html')
fp = codecs.open(foofilename, 'w', 'utf-8')
fp.write(CONTENT_HTML)
fp.close()
os.chmod(foofilename, 0664)
# write content.yml
foofilename = os.path.join(destDir, 'content.yml')
fp = codecs.open(foofilename, 'w', 'utf-8')
fp.write(CONTENT_YML)
os.chmod(foofilename, 0664)
return destDir, needSvn, pepnum
def write_pyramid_index(destDir, title):
filename = os.path.join(destDir, 'index.yml')
fp = codecs.open(filename, 'w', 'utf-8')
fp.write(INDEX_YML % title.replace('"', '\\"'))
fp.close()
os.chmod(filename, 0664)
def copy_aux_files(pep_path, dest_dir):
"""
Copy auxiliary files whose names match 'pep-XXXX-*.*'.
"""
dirname, pepname = os.path.split(pep_path)
base, ext = os.path.splitext(pepname)
files = glob.glob(os.path.join(dirname, base) + '-*.*')
for path in files:
filename = os.path.basename(path)
dest_path = os.path.join(dest_dir, filename)
print '%s -> %s' % (path, dest_path)
shutil.copy(path, dest_path)
PEP_TYPE_DISPATCH = {'text/plain': fixfile,
'text/x-rst': fix_rst_pep}
PEP_TYPE_MESSAGES = {}
def check_requirements():
# Check Python:
try:
from email.Utils import parseaddr
except ImportError:
PEP_TYPE_DISPATCH['text/plain'] = None
PEP_TYPE_MESSAGES['text/plain'] = (
'Python %s or better required for "%%(pep_type)s" PEP '
'processing; %s present (%%(inpath)s).'
% (REQUIRES['python'], sys.version.split()[0]))
# Check Docutils:
try:
import docutils
except ImportError:
PEP_TYPE_DISPATCH['text/x-rst'] = None
PEP_TYPE_MESSAGES['text/x-rst'] = (
'Docutils not present for "%(pep_type)s" PEP file %(inpath)s. '
'See README.txt for installation.')
else:
installed = [int(part) for part in docutils.__version__.split('.')]
required = [int(part) for part in REQUIRES['docutils'].split('.')]
if installed < required:
PEP_TYPE_DISPATCH['text/x-rst'] = None
PEP_TYPE_MESSAGES['text/x-rst'] = (
'Docutils must be reinstalled for "%%(pep_type)s" PEP '
'processing (%%(inpath)s). Version %s or better required; '
'%s present. See README.txt for installation.'
% (REQUIRES['docutils'], docutils.__version__))
def pep_type_error(inpath, pep_type):
print >> sys.stderr, 'Error: ' + PEP_TYPE_MESSAGES[pep_type] % locals()
sys.stdout.flush()
def build_peps(args=None):
if args:
filenames = pep_filename_generator(args)
else:
# do them all
filenames = glob.glob("pep-*.txt")
filenames.sort()
for filename in filenames:
try:
make_html(filename)
except (KeyboardInterrupt, SystemExit):
raise
except:
print "While building PEPs: %s" % filename
if settings.keep_going:
ee, ev, et = sys.exc_info()
traceback.print_exception(ee, ev, et, file=sys.stdout)
print "--keep-going/-k specified, continuing"
continue
else:
raise
def pep_filename_generator(args):
for pep in args:
filename = find_pep(pep)
yield filename
def main(argv=None):
check_requirements()
if argv is None:
argv = sys.argv[1:]
try:
opts, args = getopt.getopt(
argv, 'hd:fkq',
['help', 'destdir=', 'force', 'keep-going', 'quiet'])
except getopt.error, msg:
usage(1, msg)
for opt, arg in opts:
if opt in ('-h', '--help'):
usage(0)
elif opt in ('-d', '--destdir'):
settings.dest_dir_base = arg
elif opt in ('-f', '--force'):
settings.force_rebuild = True
elif opt in ('-k', '--keep-going'):
settings.force_rebuild = True
elif opt in ('-q', '--quiet'):
settings.verbose = False
build_peps(args)
if __name__ == "__main__":
main()