179
|
1 |
# $Id: peps.py 4564 2006-05-21 20:44:42Z wiemann $
|
|
2 |
# Author: David Goodger <goodger@python.org>
|
|
3 |
# Copyright: This module has been placed in the public domain.
|
|
4 |
|
|
5 |
"""
|
|
6 |
Transforms for PEP processing.
|
|
7 |
|
|
8 |
- `Headers`: Used to transform a PEP's initial RFC-2822 header. It remains a
|
|
9 |
field list, but some entries get processed.
|
|
10 |
- `Contents`: Auto-inserts a table of contents.
|
|
11 |
- `PEPZero`: Special processing for PEP 0.
|
|
12 |
"""
|
|
13 |
|
|
14 |
__docformat__ = 'reStructuredText'
|
|
15 |
|
|
16 |
import sys
|
|
17 |
import os
|
|
18 |
import re
|
|
19 |
import time
|
|
20 |
from docutils import nodes, utils, languages
|
|
21 |
from docutils import ApplicationError, DataError
|
|
22 |
from docutils.transforms import Transform, TransformError
|
|
23 |
from docutils.transforms import parts, references, misc
|
|
24 |
|
|
25 |
|
|
26 |
class Headers(Transform):
|
|
27 |
|
|
28 |
"""
|
|
29 |
Process fields in a PEP's initial RFC-2822 header.
|
|
30 |
"""
|
|
31 |
|
|
32 |
default_priority = 360
|
|
33 |
|
|
34 |
pep_url = 'pep-%04d'
|
|
35 |
pep_cvs_url = ('http://svn.python.org/view/*checkout*'
|
|
36 |
'/peps/trunk/pep-%04d.txt')
|
|
37 |
rcs_keyword_substitutions = (
|
|
38 |
(re.compile(r'\$' r'RCSfile: (.+),v \$$', re.IGNORECASE), r'\1'),
|
|
39 |
(re.compile(r'\$[a-zA-Z]+: (.+) \$$'), r'\1'),)
|
|
40 |
|
|
41 |
def apply(self):
|
|
42 |
if not len(self.document):
|
|
43 |
# @@@ replace these DataErrors with proper system messages
|
|
44 |
raise DataError('Document tree is empty.')
|
|
45 |
header = self.document[0]
|
|
46 |
if not isinstance(header, nodes.field_list) or \
|
|
47 |
'rfc2822' not in header['classes']:
|
|
48 |
raise DataError('Document does not begin with an RFC-2822 '
|
|
49 |
'header; it is not a PEP.')
|
|
50 |
pep = None
|
|
51 |
for field in header:
|
|
52 |
if field[0].astext().lower() == 'pep': # should be the first field
|
|
53 |
value = field[1].astext()
|
|
54 |
try:
|
|
55 |
pep = int(value)
|
|
56 |
cvs_url = self.pep_cvs_url % pep
|
|
57 |
except ValueError:
|
|
58 |
pep = value
|
|
59 |
cvs_url = None
|
|
60 |
msg = self.document.reporter.warning(
|
|
61 |
'"PEP" header must contain an integer; "%s" is an '
|
|
62 |
'invalid value.' % pep, base_node=field)
|
|
63 |
msgid = self.document.set_id(msg)
|
|
64 |
prb = nodes.problematic(value, value or '(none)',
|
|
65 |
refid=msgid)
|
|
66 |
prbid = self.document.set_id(prb)
|
|
67 |
msg.add_backref(prbid)
|
|
68 |
if len(field[1]):
|
|
69 |
field[1][0][:] = [prb]
|
|
70 |
else:
|
|
71 |
field[1] += nodes.paragraph('', '', prb)
|
|
72 |
break
|
|
73 |
if pep is None:
|
|
74 |
raise DataError('Document does not contain an RFC-2822 "PEP" '
|
|
75 |
'header.')
|
|
76 |
if pep == 0:
|
|
77 |
# Special processing for PEP 0.
|
|
78 |
pending = nodes.pending(PEPZero)
|
|
79 |
self.document.insert(1, pending)
|
|
80 |
self.document.note_pending(pending)
|
|
81 |
if len(header) < 2 or header[1][0].astext().lower() != 'title':
|
|
82 |
raise DataError('No title!')
|
|
83 |
for field in header:
|
|
84 |
name = field[0].astext().lower()
|
|
85 |
body = field[1]
|
|
86 |
if len(body) > 1:
|
|
87 |
raise DataError('PEP header field body contains multiple '
|
|
88 |
'elements:\n%s' % field.pformat(level=1))
|
|
89 |
elif len(body) == 1:
|
|
90 |
if not isinstance(body[0], nodes.paragraph):
|
|
91 |
raise DataError('PEP header field body may only contain '
|
|
92 |
'a single paragraph:\n%s'
|
|
93 |
% field.pformat(level=1))
|
|
94 |
elif name == 'last-modified':
|
|
95 |
date = time.strftime(
|
|
96 |
'%d-%b-%Y',
|
|
97 |
time.localtime(os.stat(self.document['source'])[8]))
|
|
98 |
if cvs_url:
|
|
99 |
body += nodes.paragraph(
|
|
100 |
'', '', nodes.reference('', date, refuri=cvs_url))
|
|
101 |
else:
|
|
102 |
# empty
|
|
103 |
continue
|
|
104 |
para = body[0]
|
|
105 |
if name == 'author':
|
|
106 |
for node in para:
|
|
107 |
if isinstance(node, nodes.reference):
|
|
108 |
node.replace_self(mask_email(node))
|
|
109 |
elif name == 'discussions-to':
|
|
110 |
for node in para:
|
|
111 |
if isinstance(node, nodes.reference):
|
|
112 |
node.replace_self(mask_email(node, pep))
|
|
113 |
elif name in ('replaces', 'replaced-by', 'requires'):
|
|
114 |
newbody = []
|
|
115 |
space = nodes.Text(' ')
|
|
116 |
for refpep in re.split(',?\s+', body.astext()):
|
|
117 |
pepno = int(refpep)
|
|
118 |
newbody.append(nodes.reference(
|
|
119 |
refpep, refpep,
|
|
120 |
refuri=(self.document.settings.pep_base_url
|
|
121 |
+ self.pep_url % pepno)))
|
|
122 |
newbody.append(space)
|
|
123 |
para[:] = newbody[:-1] # drop trailing space
|
|
124 |
elif name == 'last-modified':
|
|
125 |
utils.clean_rcs_keywords(para, self.rcs_keyword_substitutions)
|
|
126 |
if cvs_url:
|
|
127 |
date = para.astext()
|
|
128 |
para[:] = [nodes.reference('', date, refuri=cvs_url)]
|
|
129 |
elif name == 'content-type':
|
|
130 |
pep_type = para.astext()
|
|
131 |
uri = self.document.settings.pep_base_url + self.pep_url % 12
|
|
132 |
para[:] = [nodes.reference('', pep_type, refuri=uri)]
|
|
133 |
elif name == 'version' and len(body):
|
|
134 |
utils.clean_rcs_keywords(para, self.rcs_keyword_substitutions)
|
|
135 |
|
|
136 |
|
|
137 |
class Contents(Transform):
|
|
138 |
|
|
139 |
"""
|
|
140 |
Insert an empty table of contents topic and a transform placeholder into
|
|
141 |
the document after the RFC 2822 header.
|
|
142 |
"""
|
|
143 |
|
|
144 |
default_priority = 380
|
|
145 |
|
|
146 |
def apply(self):
|
|
147 |
language = languages.get_language(self.document.settings.language_code)
|
|
148 |
name = language.labels['contents']
|
|
149 |
title = nodes.title('', name)
|
|
150 |
topic = nodes.topic('', title, classes=['contents'])
|
|
151 |
name = nodes.fully_normalize_name(name)
|
|
152 |
if not self.document.has_name(name):
|
|
153 |
topic['names'].append(name)
|
|
154 |
self.document.note_implicit_target(topic)
|
|
155 |
pending = nodes.pending(parts.Contents)
|
|
156 |
topic += pending
|
|
157 |
self.document.insert(1, topic)
|
|
158 |
self.document.note_pending(pending)
|
|
159 |
|
|
160 |
|
|
161 |
class TargetNotes(Transform):
|
|
162 |
|
|
163 |
"""
|
|
164 |
Locate the "References" section, insert a placeholder for an external
|
|
165 |
target footnote insertion transform at the end, and schedule the
|
|
166 |
transform to run immediately.
|
|
167 |
"""
|
|
168 |
|
|
169 |
default_priority = 520
|
|
170 |
|
|
171 |
def apply(self):
|
|
172 |
doc = self.document
|
|
173 |
i = len(doc) - 1
|
|
174 |
refsect = copyright = None
|
|
175 |
while i >= 0 and isinstance(doc[i], nodes.section):
|
|
176 |
title_words = doc[i][0].astext().lower().split()
|
|
177 |
if 'references' in title_words:
|
|
178 |
refsect = doc[i]
|
|
179 |
break
|
|
180 |
elif 'copyright' in title_words:
|
|
181 |
copyright = i
|
|
182 |
i -= 1
|
|
183 |
if not refsect:
|
|
184 |
refsect = nodes.section()
|
|
185 |
refsect += nodes.title('', 'References')
|
|
186 |
doc.set_id(refsect)
|
|
187 |
if copyright:
|
|
188 |
# Put the new "References" section before "Copyright":
|
|
189 |
doc.insert(copyright, refsect)
|
|
190 |
else:
|
|
191 |
# Put the new "References" section at end of doc:
|
|
192 |
doc.append(refsect)
|
|
193 |
pending = nodes.pending(references.TargetNotes)
|
|
194 |
refsect.append(pending)
|
|
195 |
self.document.note_pending(pending, 0)
|
|
196 |
pending = nodes.pending(misc.CallBack,
|
|
197 |
details={'callback': self.cleanup_callback})
|
|
198 |
refsect.append(pending)
|
|
199 |
self.document.note_pending(pending, 1)
|
|
200 |
|
|
201 |
def cleanup_callback(self, pending):
|
|
202 |
"""
|
|
203 |
Remove an empty "References" section.
|
|
204 |
|
|
205 |
Called after the `references.TargetNotes` transform is complete.
|
|
206 |
"""
|
|
207 |
if len(pending.parent) == 2: # <title> and <pending>
|
|
208 |
pending.parent.parent.remove(pending.parent)
|
|
209 |
|
|
210 |
|
|
211 |
class PEPZero(Transform):
|
|
212 |
|
|
213 |
"""
|
|
214 |
Special processing for PEP 0.
|
|
215 |
"""
|
|
216 |
|
|
217 |
default_priority =760
|
|
218 |
|
|
219 |
def apply(self):
|
|
220 |
visitor = PEPZeroSpecial(self.document)
|
|
221 |
self.document.walk(visitor)
|
|
222 |
self.startnode.parent.remove(self.startnode)
|
|
223 |
|
|
224 |
|
|
225 |
class PEPZeroSpecial(nodes.SparseNodeVisitor):
|
|
226 |
|
|
227 |
"""
|
|
228 |
Perform the special processing needed by PEP 0:
|
|
229 |
|
|
230 |
- Mask email addresses.
|
|
231 |
|
|
232 |
- Link PEP numbers in the second column of 4-column tables to the PEPs
|
|
233 |
themselves.
|
|
234 |
"""
|
|
235 |
|
|
236 |
pep_url = Headers.pep_url
|
|
237 |
|
|
238 |
def unknown_visit(self, node):
|
|
239 |
pass
|
|
240 |
|
|
241 |
def visit_reference(self, node):
|
|
242 |
node.replace_self(mask_email(node))
|
|
243 |
|
|
244 |
def visit_field_list(self, node):
|
|
245 |
if 'rfc2822' in node['classes']:
|
|
246 |
raise nodes.SkipNode
|
|
247 |
|
|
248 |
def visit_tgroup(self, node):
|
|
249 |
self.pep_table = node['cols'] == 4
|
|
250 |
self.entry = 0
|
|
251 |
|
|
252 |
def visit_colspec(self, node):
|
|
253 |
self.entry += 1
|
|
254 |
if self.pep_table and self.entry == 2:
|
|
255 |
node['classes'].append('num')
|
|
256 |
|
|
257 |
def visit_row(self, node):
|
|
258 |
self.entry = 0
|
|
259 |
|
|
260 |
def visit_entry(self, node):
|
|
261 |
self.entry += 1
|
|
262 |
if self.pep_table and self.entry == 2 and len(node) == 1:
|
|
263 |
node['classes'].append('num')
|
|
264 |
p = node[0]
|
|
265 |
if isinstance(p, nodes.paragraph) and len(p) == 1:
|
|
266 |
text = p.astext()
|
|
267 |
try:
|
|
268 |
pep = int(text)
|
|
269 |
ref = (self.document.settings.pep_base_url
|
|
270 |
+ self.pep_url % pep)
|
|
271 |
p[0] = nodes.reference(text, text, refuri=ref)
|
|
272 |
except ValueError:
|
|
273 |
pass
|
|
274 |
|
|
275 |
|
|
276 |
non_masked_addresses = ('peps@python.org',
|
|
277 |
'python-list@python.org',
|
|
278 |
'python-dev@python.org')
|
|
279 |
|
|
280 |
def mask_email(ref, pepno=None):
|
|
281 |
"""
|
|
282 |
Mask the email address in `ref` and return a replacement node.
|
|
283 |
|
|
284 |
`ref` is returned unchanged if it contains no email address.
|
|
285 |
|
|
286 |
For email addresses such as "user@host", mask the address as "user at
|
|
287 |
host" (text) to thwart simple email address harvesters (except for those
|
|
288 |
listed in `non_masked_addresses`). If a PEP number (`pepno`) is given,
|
|
289 |
return a reference including a default email subject.
|
|
290 |
"""
|
|
291 |
if ref.hasattr('refuri') and ref['refuri'].startswith('mailto:'):
|
|
292 |
if ref['refuri'][8:] in non_masked_addresses:
|
|
293 |
replacement = ref[0]
|
|
294 |
else:
|
|
295 |
replacement_text = ref.astext().replace('@', ' at ')
|
|
296 |
replacement = nodes.raw('', replacement_text, format='html')
|
|
297 |
if pepno is None:
|
|
298 |
return replacement
|
|
299 |
else:
|
|
300 |
ref['refuri'] += '?subject=PEP%%20%s' % pepno
|
|
301 |
ref[:] = [replacement]
|
|
302 |
return ref
|
|
303 |
else:
|
|
304 |
return ref
|