2
"""Convert PEPs to (X)HTML - courtesy of /F
4
Usage: %(PROGRAM)s [options] [<peps> ...]
12
After generating the HTML, direct your web browser to view it
13
(using the Python webbrowser module). If both -i and -b are
14
given, this will browse the on-line HTML; otherwise it will
15
browse the local HTML. If no pep arguments are given, this
19
After generating the HTML, install it and the plaintext source file
20
(.txt) on python.org. In that case the user's name is used in the scp
21
and ssh commands, unless "-u username" is given (in which case, it is
22
used instead). Without -i, -u is ignored.
25
Same as -i/--install, except install on the local machine. Use this
26
when logged in to the python.org machine (creosote).
29
Turn off verbose messages.
32
Print this help message and exit.
34
The optional arguments ``peps`` are either pep numbers or .txt files.
47
REQUIRES = {'python': '2.2',
50
RFCURL = 'http://www.faqs.org/rfcs/rfc%d.html'
51
PEPURL = 'pep-%04d.html'
52
PEPCVSURL = ('http://cvs.sourceforge.net/cgi-bin/viewcvs.cgi/python/python'
53
'/nondist/peps/pep-%04d.txt')
54
PEPDIRRUL = 'http://www.python.org/peps/'
57
HOST = "www.python.org" # host for update
58
HDIR = "/ftp/ftp.python.org/pub/www.python.org/peps" # target host directory
59
LOCALVARS = "Local Variables:"
62
This HTML is auto-generated. DO NOT EDIT THIS FILE! If you are writing a new
63
PEP, see http://www.python.org/peps/pep-0001.html for instructions and links
64
to templates. DO NOT USE THIS HTML FILE AS YOUR TEMPLATE!
67
# The generated HTML doesn't validate -- you cannot use <hr> and <h3> inside
68
# <pre> tags. But if I change that, the result doesn't look very nice...
69
DTD = ('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"\n'
70
' "http://www.w3.org/TR/REC-html40/loose.dtd">')
72
fixpat = re.compile("((http|ftp):[-_a-zA-Z0-9/.+~:?#$=&,]+)|(pep-\d+(.txt)?)|"
73
"(RFC[- ]?(?P<rfcnum>\d+))|"
74
"(PEP\s+(?P<pepnum>\d+))|"
83
def usage(code, msg=''):
84
"""Print usage message and exit. Uses stderr if code != 0."""
89
print >> out, __doc__ % globals()
96
def fixanchor(current, match):
99
if text.startswith('http:') or text.startswith('ftp:'):
100
# Strip off trailing punctuation. Pattern taken from faqwiz.
104
if c not in '();:,.?\'"<>':
107
link = EMPTYSTRING.join(ltext)
108
elif text.startswith('pep-') and text <> current:
109
link = os.path.splitext(text)[0] + ".html"
110
elif text.startswith('PEP'):
111
pepnum = int(match.group('pepnum'))
112
link = PEPURL % pepnum
113
elif text.startswith('RFC'):
114
rfcnum = int(match.group('rfcnum'))
115
link = RFCURL % rfcnum
117
return '<a href="%s">%s</a>' % (cgi.escape(link), cgi.escape(text))
118
return cgi.escape(match.group(0)) # really slow, but it works...
122
NON_MASKED_EMAILS = [
124
'python-list@python.org',
125
'python-dev@python.org',
128
def fixemail(address, pepno):
129
if address.lower() in NON_MASKED_EMAILS:
130
# return hyperlinked version of email address
131
return linkemail(address, pepno)
133
# return masked version of email address
134
parts = address.split('@', 1)
135
return '%s at %s' % (parts[0], parts[1])
138
def linkemail(address, pepno):
139
parts = address.split('@', 1)
140
return ('<a href="mailto:%s@%s?subject=PEP%%20%s">'
141
'%s at %s</a>'
142
% (parts[0], parts[1], pepno, parts[0], parts[1]))
145
def fixfile(inpath, input_lines, outfile):
146
from email.Utils import parseaddr
147
basename = os.path.basename(inpath)
148
infile = iter(input_lines)
149
# convert plaintext pep to minimal XHTML markup
150
print >> outfile, DTD
151
print >> outfile, '<html>'
152
print >> outfile, COMMENT
153
print >> outfile, '<head>'
164
key, value = line.split(":", 1)
165
value = value.strip()
166
header.append((key, value))
169
key, value = header[-1]
171
header[-1] = key, value
172
if key.lower() == "title":
174
elif key.lower() == "pep":
177
title = "PEP " + pep + " -- " + title
179
print >> outfile, ' <title>%s</title>' % cgi.escape(title)
180
r = random.choice(range(64))
182
' <link rel="STYLESHEET" href="style.css" type="text/css" />\n'
184
'<body bgcolor="white">\n'
185
'<table class="navigation" cellpadding="0" cellspacing="0"\n'
186
' width="100%%" border="0">\n'
187
'<tr><td class="navicon" width="150" height="35">\n'
188
'<a href="../" title="Python Home Page">\n'
189
'<img src="../pics/PyBanner%03d.gif" alt="[Python]"\n'
190
' border="0" width="150" height="35" /></a></td>\n'
191
'<td class="textlinks" align="left">\n'
192
'[<b><a href="../">Python Home</a></b>]' % r)
193
if basename <> 'pep-0000.txt':
194
print >> outfile, '[<b><a href=".">PEP Index</a></b>]'
197
print >> outfile, ('[<b><a href="pep-%04d.txt">PEP Source</a>'
199
except ValueError, error:
200
print >> sys.stderr, ('ValueError (invalid PEP number): %s'
202
print >> outfile, '</td></tr></table>'
203
print >> outfile, '<div class="header">\n<table border="0">'
205
if k.lower() in ('author', 'discussions-to'):
207
for part in re.split(',\s*', v):
209
realname, addr = parseaddr(part)
210
if k.lower() == 'discussions-to':
211
m = linkemail(addr, pep)
213
m = fixemail(addr, pep)
214
mailtos.append('%s <%s>' % (realname, m))
215
elif part.startswith('http:'):
217
'<a href="%s">%s</a>' % (part, part))
220
v = COMMASPACE.join(mailtos)
221
elif k.lower() in ('replaces', 'replaced-by', 'requires'):
223
for otherpep in re.split(',?\s+', v):
224
otherpep = int(otherpep)
225
otherpeps += '<a href="pep-%04d.html">%i</a> ' % (otherpep,
228
elif k.lower() in ('last-modified',):
229
date = v or time.strftime('%d-%b-%Y',
230
time.localtime(os.stat(inpath)[8]))
232
url = PEPCVSURL % int(pep)
233
v = '<a href="%s">%s</a> ' % (url, cgi.escape(date))
234
except ValueError, error:
236
elif k.lower() in ('content-type',):
238
pep_type = v or 'text/plain'
239
v = '<a href="%s">%s</a> ' % (url, cgi.escape(pep_type))
242
print >> outfile, ' <tr><th>%s: </th><td>%s</td></tr>' \
244
print >> outfile, '</table>'
245
print >> outfile, '</div>'
246
print >> outfile, '<hr />'
247
print >> outfile, '<div class="content">'
252
if line.strip() == LOCALVARS:
256
print >> outfile, '</pre>'
257
print >> outfile, '<h3>%s</h3>' % line.strip()
259
elif not line.strip() and need_pre:
262
# PEP 0 has some special treatment
263
if basename == 'pep-0000.txt':
265
if len(parts) > 1 and re.match(r'\s*\d{1,4}', parts[1]):
266
# This is a PEP summary line, which we need to hyperlink
267
url = PEPURL % int(parts[1])
269
print >> outfile, '<pre>'
271
print >> outfile, re.sub(
273
'<a href="%s">%s</a>' % (url, parts[1]),
276
elif parts and '@' in parts[-1]:
277
# This is a pep email address line, so filter it.
278
url = fixemail(parts[-1], pep)
280
print >> outfile, '<pre>'
282
print >> outfile, re.sub(
283
parts[-1], url, line, 1),
285
line = fixpat.sub(lambda x, c=inpath: fixanchor(c, x), line)
287
print >> outfile, '<pre>'
291
print >> outfile, '</pre>'
292
print >> outfile, '</div>'
293
print >> outfile, '</body>'
294
print >> outfile, '</html>'
297
docutils_settings = None
298
"""Runtime settings object used by Docutils. Can be set by the client
299
application when this module is imported."""
301
def fix_rst_pep(inpath, input_lines, outfile):
302
from docutils import core
303
output = core.publish_string(
304
source=''.join(input_lines),
306
destination_path=outfile.name,
308
parser_name='restructuredtext',
309
writer_name='pep_html',
310
settings=docutils_settings,
311
# Allow Docutils traceback if there's an exception:
312
settings_overrides={'traceback': 1})
313
outfile.write(output)
316
def get_pep_type(input_lines):
318
Return the Content-Type of the input. "text/plain" is the default.
319
Return ``None`` if the input is not a PEP.
322
for line in input_lines:
323
line = line.rstrip().lower()
325
# End of the RFC 2822 header (first blank line).
327
elif line.startswith('content-type: '):
328
pep_type = line.split()[1] or 'text/plain'
330
elif line.startswith('pep: '):
331
# Default PEP type, used if no explicit content-type specified:
332
pep_type = 'text/plain'
336
def get_input_lines(inpath):
338
infile = open(inpath)
340
if e.errno <> errno.ENOENT: raise
341
print >> sys.stderr, 'Error: Skipping missing PEP file:', e.filename
344
lines = infile.read().splitlines(1) # handles x-platform line endings
349
def find_pep(pep_str):
350
"""Find the .txt file indicated by a cmd line argument"""
351
if os.path.exists(pep_str):
354
return "pep-%04d.txt" % num
356
def make_html(inpath, verbose=0):
357
input_lines = get_input_lines(inpath)
358
pep_type = get_pep_type(input_lines)
360
print >> sys.stderr, 'Error: Input file %s is not a PEP.' % inpath
363
elif not PEP_TYPE_DISPATCH.has_key(pep_type):
364
print >> sys.stderr, ('Error: Unknown PEP type for input file %s: %s'
365
% (inpath, pep_type))
368
elif PEP_TYPE_DISPATCH[pep_type] == None:
369
pep_type_error(inpath, pep_type)
371
outpath = os.path.splitext(inpath)[0] + ".html"
373
print inpath, "(%s)" % pep_type, "->", outpath
375
outfile = open(outpath, "w")
376
PEP_TYPE_DISPATCH[pep_type](inpath, input_lines, outfile)
378
os.chmod(outfile.name, 0664)
381
def push_pep(htmlfiles, txtfiles, username, verbose, local=0):
393
username = username + "@"
394
target = username + HOST + ":" + HDIR
396
chmod_cmd = "ssh %s%s chmod" % (username, HOST)
398
files.extend(txtfiles)
399
files.append("style.css")
400
files.append("pep.css")
401
filelist = SPACE.join(files)
402
rc = os.system("%s %s %s %s" % (copy_cmd, quiet, filelist, target))
405
rc = os.system("%s 664 %s/*" % (chmod_cmd, HDIR))
410
PEP_TYPE_DISPATCH = {'text/plain': fixfile,
411
'text/x-rst': fix_rst_pep}
412
PEP_TYPE_MESSAGES = {}
414
def check_requirements():
417
from email.Utils import parseaddr
419
PEP_TYPE_DISPATCH['text/plain'] = None
420
PEP_TYPE_MESSAGES['text/plain'] = (
421
'Python %s or better required for "%%(pep_type)s" PEP '
422
'processing; %s present (%%(inpath)s).'
423
% (REQUIRES['python'], sys.version.split()[0]))
428
PEP_TYPE_DISPATCH['text/x-rst'] = None
429
PEP_TYPE_MESSAGES['text/x-rst'] = (
430
'Docutils not present for "%(pep_type)s" PEP file %(inpath)s. '
431
'See README.txt for installation.')
433
installed = [int(part) for part in docutils.__version__.split('.')]
434
required = [int(part) for part in REQUIRES['docutils'].split('.')]
435
if installed < required:
436
PEP_TYPE_DISPATCH['text/x-rst'] = None
437
PEP_TYPE_MESSAGES['text/x-rst'] = (
438
'Docutils must be reinstalled for "%%(pep_type)s" PEP '
439
'processing (%%(inpath)s). Version %s or better required; '
440
'%s present. See README.txt for installation.'
441
% (REQUIRES['docutils'], docutils.__version__))
443
def pep_type_error(inpath, pep_type):
444
print >> sys.stderr, 'Error: ' + PEP_TYPE_MESSAGES[pep_type] % locals()
448
def browse_file(pep):
451
if file.endswith(".txt"):
452
file = file[:-3] + "html"
453
file = os.path.abspath(file)
457
def browse_remote(pep):
460
if file.endswith(".txt"):
461
file = file[:-3] + "html"
462
url = PEPDIRRUL + file
480
opts, args = getopt.getopt(
482
['browse', 'install', 'local', 'help', 'quiet', 'user='])
483
except getopt.error, msg:
486
for opt, arg in opts:
487
if opt in ('-h', '--help'):
489
elif opt in ('-i', '--install'):
491
elif opt in ('-l', '--local'):
494
elif opt in ('-u', '--user'):
496
elif opt in ('-q', '--quiet'):
498
elif opt in ('-b', '--browse'):
507
newfile = make_html(file, verbose=verbose)
510
if browse and not update:
516
files = glob.glob("pep-*.txt")
520
newfile = make_html(file, verbose=verbose)
523
if browse and not update:
527
push_pep(html, peptxt, username, verbose, local=local)
537
if __name__ == "__main__":