~ubuntu-branches/ubuntu/karmic/python-docutils/karmic

« back to all changes in this revision

Viewing changes to tools/pep2html.py

  • Committer: Bazaar Package Importer
  • Author(s): martin f. krafft
  • Date: 2006-07-10 11:45:05 UTC
  • mfrom: (2.1.4 edgy)
  • Revision ID: james.westby@ubuntu.com-20060710114505-otkhqcslevewxmz5
Tags: 0.4-3
Added build dependency on python-central (closes: #377580).

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/env python
2
 
"""Convert PEPs to (X)HTML - courtesy of /F
3
 
 
4
 
Usage: %(PROGRAM)s [options] [<peps> ...]
5
 
 
6
 
Options:
7
 
 
8
 
-u, --user
9
 
    python.org username
10
 
 
11
 
-b, --browse
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
16
 
    will browse PEP 0.
17
 
 
18
 
-i, --install
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.
23
 
 
24
 
-l, --local
25
 
    Same as -i/--install, except install on the local machine.  Use this
26
 
    when logged in to the python.org machine (creosote).
27
 
 
28
 
-q, --quiet
29
 
    Turn off verbose messages.
30
 
 
31
 
-h, --help
32
 
    Print this help message and exit.
33
 
 
34
 
The optional arguments ``peps`` are either pep numbers or .txt files.
35
 
"""
36
 
 
37
 
import sys
38
 
import os
39
 
import re
40
 
import cgi
41
 
import glob
42
 
import getopt
43
 
import errno
44
 
import random
45
 
import time
46
 
 
47
 
REQUIRES = {'python': '2.2',
48
 
            'docutils': '0.2.7'}
49
 
PROGRAM = sys.argv[0]
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/'
55
 
 
56
 
 
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:"
60
 
 
61
 
COMMENT = """<!--
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!
65
 
-->"""
66
 
 
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">')
71
 
 
72
 
fixpat = re.compile("((http|ftp):[-_a-zA-Z0-9/.+~:?#$=&,]+)|(pep-\d+(.txt)?)|"
73
 
                    "(RFC[- ]?(?P<rfcnum>\d+))|"
74
 
                    "(PEP\s+(?P<pepnum>\d+))|"
75
 
                    ".")
76
 
 
77
 
EMPTYSTRING = ''
78
 
SPACE = ' '
79
 
COMMASPACE = ', '
80
 
 
81
 
 
82
 
 
83
 
def usage(code, msg=''):
84
 
    """Print usage message and exit.  Uses stderr if code != 0."""
85
 
    if code == 0:
86
 
        out = sys.stdout
87
 
    else:
88
 
        out = sys.stderr
89
 
    print >> out, __doc__ % globals()
90
 
    if msg:
91
 
        print >> out, msg
92
 
    sys.exit(code)
93
 
 
94
 
 
95
 
 
96
 
def fixanchor(current, match):
97
 
    text = match.group(0)
98
 
    link = None
99
 
    if text.startswith('http:') or text.startswith('ftp:'):
100
 
        # Strip off trailing punctuation.  Pattern taken from faqwiz.
101
 
        ltext = list(text)
102
 
        while ltext:
103
 
            c = ltext.pop()
104
 
            if c not in '();:,.?\'"<>':
105
 
                ltext.append(c)
106
 
                break
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
116
 
    if link:
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...
119
 
 
120
 
 
121
 
 
122
 
NON_MASKED_EMAILS = [
123
 
    'peps@python.org',
124
 
    'python-list@python.org',
125
 
    'python-dev@python.org',
126
 
    ]
127
 
 
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)
132
 
    else:
133
 
        # return masked version of email address
134
 
        parts = address.split('@', 1)
135
 
        return '%s&#32;&#97;t&#32;%s' % (parts[0], parts[1])
136
 
 
137
 
 
138
 
def linkemail(address, pepno):
139
 
    parts = address.split('@', 1)
140
 
    return ('<a href="mailto:%s&#64;%s?subject=PEP%%20%s">'
141
 
            '%s&#32;&#97;t&#32;%s</a>'
142
 
            % (parts[0], parts[1], pepno, parts[0], parts[1]))
143
 
 
144
 
 
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>'
154
 
    # head
155
 
    header = []
156
 
    pep = ""
157
 
    title = ""
158
 
    for line in infile:
159
 
        if not line.strip():
160
 
            break
161
 
        if line[0].strip():
162
 
            if ":" not in line:
163
 
                break
164
 
            key, value = line.split(":", 1)
165
 
            value = value.strip()
166
 
            header.append((key, value))
167
 
        else:
168
 
            # continuation line
169
 
            key, value = header[-1]
170
 
            value = value + line
171
 
            header[-1] = key, value
172
 
        if key.lower() == "title":
173
 
            title = value
174
 
        elif key.lower() == "pep":
175
 
            pep = value
176
 
    if pep:
177
 
        title = "PEP " + pep + " -- " + title
178
 
    if title:
179
 
        print >> outfile, '  <title>%s</title>' % cgi.escape(title)
180
 
    r = random.choice(range(64))
181
 
    print >> outfile, (
182
 
        '  <link rel="STYLESHEET" href="style.css" type="text/css" />\n'
183
 
        '</head>\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>]'
195
 
    if pep:
196
 
        try:
197
 
            print >> outfile, ('[<b><a href="pep-%04d.txt">PEP Source</a>'
198
 
                               '</b>]' % int(pep))
199
 
        except ValueError, error:
200
 
            print >> sys.stderr, ('ValueError (invalid PEP number): %s'
201
 
                                  % error)
202
 
    print >> outfile, '</td></tr></table>'
203
 
    print >> outfile, '<div class="header">\n<table border="0">'
204
 
    for k, v in header:
205
 
        if k.lower() in ('author', 'discussions-to'):
206
 
            mailtos = []
207
 
            for part in re.split(',\s*', v):
208
 
                if '@' in part:
209
 
                    realname, addr = parseaddr(part)
210
 
                    if k.lower() == 'discussions-to':
211
 
                        m = linkemail(addr, pep)
212
 
                    else:
213
 
                        m = fixemail(addr, pep)
214
 
                    mailtos.append('%s &lt;%s&gt;' % (realname, m))
215
 
                elif part.startswith('http:'):
216
 
                    mailtos.append(
217
 
                        '<a href="%s">%s</a>' % (part, part))
218
 
                else:
219
 
                    mailtos.append(part)
220
 
            v = COMMASPACE.join(mailtos)
221
 
        elif k.lower() in ('replaces', 'replaced-by', 'requires'):
222
 
            otherpeps = ''
223
 
            for otherpep in re.split(',?\s+', v):
224
 
                otherpep = int(otherpep)
225
 
                otherpeps += '<a href="pep-%04d.html">%i</a> ' % (otherpep,
226
 
                                                                  otherpep)
227
 
            v = otherpeps
228
 
        elif k.lower() in ('last-modified',):
229
 
            date = v or time.strftime('%d-%b-%Y',
230
 
                                      time.localtime(os.stat(inpath)[8]))
231
 
            try:
232
 
                url = PEPCVSURL % int(pep)
233
 
                v = '<a href="%s">%s</a> ' % (url, cgi.escape(date))
234
 
            except ValueError, error:
235
 
                v = date
236
 
        elif k.lower() in ('content-type',):
237
 
            url = PEPURL % 9
238
 
            pep_type = v or 'text/plain'
239
 
            v = '<a href="%s">%s</a> ' % (url, cgi.escape(pep_type))
240
 
        else:
241
 
            v = cgi.escape(v)
242
 
        print >> outfile, '  <tr><th>%s:&nbsp;</th><td>%s</td></tr>' \
243
 
              % (cgi.escape(k), v)
244
 
    print >> outfile, '</table>'
245
 
    print >> outfile, '</div>'
246
 
    print >> outfile, '<hr />'
247
 
    print >> outfile, '<div class="content">'
248
 
    need_pre = 1
249
 
    for line in infile:
250
 
        if line[0] == '\f':
251
 
            continue
252
 
        if line.strip() == LOCALVARS:
253
 
            break
254
 
        if line[0].strip():
255
 
            if not need_pre:
256
 
                print >> outfile, '</pre>'
257
 
            print >> outfile, '<h3>%s</h3>' % line.strip()
258
 
            need_pre = 1
259
 
        elif not line.strip() and need_pre:
260
 
            continue
261
 
        else:
262
 
            # PEP 0 has some special treatment
263
 
            if basename == 'pep-0000.txt':
264
 
                parts = line.split()
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])
268
 
                    if need_pre:
269
 
                        print >> outfile, '<pre>'
270
 
                        need_pre = 0
271
 
                    print >> outfile, re.sub(
272
 
                        parts[1],
273
 
                        '<a href="%s">%s</a>' % (url, parts[1]),
274
 
                        line, 1),
275
 
                    continue
276
 
                elif parts and '@' in parts[-1]:
277
 
                    # This is a pep email address line, so filter it.
278
 
                    url = fixemail(parts[-1], pep)
279
 
                    if need_pre:
280
 
                        print >> outfile, '<pre>'
281
 
                        need_pre = 0
282
 
                    print >> outfile, re.sub(
283
 
                        parts[-1], url, line, 1),
284
 
                    continue
285
 
            line = fixpat.sub(lambda x, c=inpath: fixanchor(c, x), line)
286
 
            if need_pre:
287
 
                print >> outfile, '<pre>'
288
 
                need_pre = 0
289
 
            outfile.write(line)
290
 
    if not need_pre:
291
 
        print >> outfile, '</pre>'
292
 
    print >> outfile, '</div>'
293
 
    print >> outfile, '</body>'
294
 
    print >> outfile, '</html>'
295
 
 
296
 
 
297
 
docutils_settings = None
298
 
"""Runtime settings object used by Docutils.  Can be set by the client
299
 
application when this module is imported."""
300
 
 
301
 
def fix_rst_pep(inpath, input_lines, outfile):
302
 
    from docutils import core
303
 
    output = core.publish_string(
304
 
        source=''.join(input_lines),
305
 
        source_path=inpath,
306
 
        destination_path=outfile.name,
307
 
        reader_name='pep',
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)
314
 
 
315
 
 
316
 
def get_pep_type(input_lines):
317
 
    """
318
 
    Return the Content-Type of the input.  "text/plain" is the default.
319
 
    Return ``None`` if the input is not a PEP.
320
 
    """
321
 
    pep_type = None
322
 
    for line in input_lines:
323
 
        line = line.rstrip().lower()
324
 
        if not line:
325
 
            # End of the RFC 2822 header (first blank line).
326
 
            break
327
 
        elif line.startswith('content-type: '):
328
 
            pep_type = line.split()[1] or 'text/plain'
329
 
            break
330
 
        elif line.startswith('pep: '):
331
 
            # Default PEP type, used if no explicit content-type specified:
332
 
            pep_type = 'text/plain'
333
 
    return pep_type
334
 
 
335
 
 
336
 
def get_input_lines(inpath):
337
 
    try:
338
 
        infile = open(inpath)
339
 
    except IOError, e:
340
 
        if e.errno <> errno.ENOENT: raise
341
 
        print >> sys.stderr, 'Error: Skipping missing PEP file:', e.filename
342
 
        sys.stderr.flush()
343
 
        return None, None
344
 
    lines = infile.read().splitlines(1) # handles x-platform line endings
345
 
    infile.close()
346
 
    return lines
347
 
 
348
 
 
349
 
def find_pep(pep_str):
350
 
    """Find the .txt file indicated by a cmd line argument"""
351
 
    if os.path.exists(pep_str):
352
 
        return pep_str
353
 
    num = int(pep_str)
354
 
    return "pep-%04d.txt" % num
355
 
 
356
 
def make_html(inpath, verbose=0):
357
 
    input_lines = get_input_lines(inpath)
358
 
    pep_type = get_pep_type(input_lines)
359
 
    if pep_type is None:
360
 
        print >> sys.stderr, 'Error: Input file %s is not a PEP.' % inpath
361
 
        sys.stdout.flush()
362
 
        return None
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))
366
 
        sys.stdout.flush()
367
 
        return None
368
 
    elif PEP_TYPE_DISPATCH[pep_type] == None:
369
 
        pep_type_error(inpath, pep_type)
370
 
        return None
371
 
    outpath = os.path.splitext(inpath)[0] + ".html"
372
 
    if verbose:
373
 
        print inpath, "(%s)" % pep_type, "->", outpath
374
 
        sys.stdout.flush()
375
 
    outfile = open(outpath, "w")
376
 
    PEP_TYPE_DISPATCH[pep_type](inpath, input_lines, outfile)
377
 
    outfile.close()
378
 
    os.chmod(outfile.name, 0664)
379
 
    return outpath
380
 
 
381
 
def push_pep(htmlfiles, txtfiles, username, verbose, local=0):
382
 
    quiet = ""
383
 
    if local:
384
 
        if verbose:
385
 
            quiet = "-v"
386
 
        target = HDIR
387
 
        copy_cmd = "cp"
388
 
        chmod_cmd = "chmod"
389
 
    else:
390
 
        if not verbose:
391
 
            quiet = "-q"
392
 
        if username:
393
 
            username = username + "@"
394
 
        target = username + HOST + ":" + HDIR
395
 
        copy_cmd = "scp"
396
 
        chmod_cmd = "ssh %s%s chmod" % (username, HOST)
397
 
    files = htmlfiles[:]
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))
403
 
    if rc:
404
 
        sys.exit(rc)
405
 
    rc = os.system("%s 664 %s/*" % (chmod_cmd, HDIR))
406
 
    if rc:
407
 
        sys.exit(rc)
408
 
 
409
 
 
410
 
PEP_TYPE_DISPATCH = {'text/plain': fixfile,
411
 
                     'text/x-rst': fix_rst_pep}
412
 
PEP_TYPE_MESSAGES = {}
413
 
 
414
 
def check_requirements():
415
 
    # Check Python:
416
 
    try:
417
 
        from email.Utils import parseaddr
418
 
    except ImportError:
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]))
424
 
    # Check Docutils:
425
 
    try:
426
 
        import docutils
427
 
    except ImportError:
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.')
432
 
    else:
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__))
442
 
 
443
 
def pep_type_error(inpath, pep_type):
444
 
    print >> sys.stderr, 'Error: ' + PEP_TYPE_MESSAGES[pep_type] % locals()
445
 
    sys.stdout.flush()
446
 
 
447
 
 
448
 
def browse_file(pep):
449
 
    import webbrowser
450
 
    file = find_pep(pep)
451
 
    if file.endswith(".txt"):
452
 
        file = file[:-3] + "html"
453
 
    file = os.path.abspath(file)
454
 
    url = "file:" + file
455
 
    webbrowser.open(url)
456
 
 
457
 
def browse_remote(pep):
458
 
    import webbrowser
459
 
    file = find_pep(pep)
460
 
    if file.endswith(".txt"):
461
 
        file = file[:-3] + "html"
462
 
    url = PEPDIRRUL + file
463
 
    webbrowser.open(url)
464
 
 
465
 
 
466
 
def main(argv=None):
467
 
    # defaults
468
 
    update = 0
469
 
    local = 0
470
 
    username = ''
471
 
    verbose = 1
472
 
    browse = 0
473
 
 
474
 
    check_requirements()
475
 
 
476
 
    if argv is None:
477
 
        argv = sys.argv[1:]
478
 
 
479
 
    try:
480
 
        opts, args = getopt.getopt(
481
 
            argv, 'bilhqu:',
482
 
            ['browse', 'install', 'local', 'help', 'quiet', 'user='])
483
 
    except getopt.error, msg:
484
 
        usage(1, msg)
485
 
 
486
 
    for opt, arg in opts:
487
 
        if opt in ('-h', '--help'):
488
 
            usage(0)
489
 
        elif opt in ('-i', '--install'):
490
 
            update = 1
491
 
        elif opt in ('-l', '--local'):
492
 
            update = 1
493
 
            local = 1
494
 
        elif opt in ('-u', '--user'):
495
 
            username = arg
496
 
        elif opt in ('-q', '--quiet'):
497
 
            verbose = 0
498
 
        elif opt in ('-b', '--browse'):
499
 
            browse = 1
500
 
 
501
 
    if args:
502
 
        peptxt = []
503
 
        html = []
504
 
        for pep in args:
505
 
            file = find_pep(pep)
506
 
            peptxt.append(file)
507
 
            newfile = make_html(file, verbose=verbose)
508
 
            if newfile:
509
 
                html.append(newfile)
510
 
            if browse and not update:
511
 
                browse_file(pep)
512
 
    else:
513
 
        # do them all
514
 
        peptxt = []
515
 
        html = []
516
 
        files = glob.glob("pep-*.txt")
517
 
        files.sort()
518
 
        for file in files:
519
 
            peptxt.append(file)
520
 
            newfile = make_html(file, verbose=verbose)
521
 
            if newfile:
522
 
                html.append(newfile)
523
 
        if browse and not update:
524
 
            browse_file("0")
525
 
 
526
 
    if update:
527
 
        push_pep(html, peptxt, username, verbose, local=local)
528
 
        if browse:
529
 
            if args:
530
 
                for pep in args:
531
 
                    browse_remote(pep)
532
 
            else:
533
 
                browse_remote("0")
534
 
 
535
 
 
536
 
 
537
 
if __name__ == "__main__":
538
 
    main()