comparison env/lib/python3.9/site-packages/docutils/transforms/peps.py @ 0:4f3585e2f14b draft default tip

"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author shellac
date Mon, 22 Mar 2021 18:12:50 +0000
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:4f3585e2f14b
1 # $Id: peps.py 7995 2016-12-10 17:50:59Z milde $
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://hg.python.org'
36 '/peps/file/default/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(r',?\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 self.document.reporter)
149 name = language.labels['contents']
150 title = nodes.title('', name)
151 topic = nodes.topic('', title, classes=['contents'])
152 name = nodes.fully_normalize_name(name)
153 if not self.document.has_name(name):
154 topic['names'].append(name)
155 self.document.note_implicit_target(topic)
156 pending = nodes.pending(parts.Contents)
157 topic += pending
158 self.document.insert(1, topic)
159 self.document.note_pending(pending)
160
161
162 class TargetNotes(Transform):
163
164 """
165 Locate the "References" section, insert a placeholder for an external
166 target footnote insertion transform at the end, and schedule the
167 transform to run immediately.
168 """
169
170 default_priority = 520
171
172 def apply(self):
173 doc = self.document
174 i = len(doc) - 1
175 refsect = copyright = None
176 while i >= 0 and isinstance(doc[i], nodes.section):
177 title_words = doc[i][0].astext().lower().split()
178 if 'references' in title_words:
179 refsect = doc[i]
180 break
181 elif 'copyright' in title_words:
182 copyright = i
183 i -= 1
184 if not refsect:
185 refsect = nodes.section()
186 refsect += nodes.title('', 'References')
187 doc.set_id(refsect)
188 if copyright:
189 # Put the new "References" section before "Copyright":
190 doc.insert(copyright, refsect)
191 else:
192 # Put the new "References" section at end of doc:
193 doc.append(refsect)
194 pending = nodes.pending(references.TargetNotes)
195 refsect.append(pending)
196 self.document.note_pending(pending, 0)
197 pending = nodes.pending(misc.CallBack,
198 details={'callback': self.cleanup_callback})
199 refsect.append(pending)
200 self.document.note_pending(pending, 1)
201
202 def cleanup_callback(self, pending):
203 """
204 Remove an empty "References" section.
205
206 Called after the `references.TargetNotes` transform is complete.
207 """
208 if len(pending.parent) == 2: # <title> and <pending>
209 pending.parent.parent.remove(pending.parent)
210
211
212 class PEPZero(Transform):
213
214 """
215 Special processing for PEP 0.
216 """
217
218 default_priority =760
219
220 def apply(self):
221 visitor = PEPZeroSpecial(self.document)
222 self.document.walk(visitor)
223 self.startnode.parent.remove(self.startnode)
224
225
226 class PEPZeroSpecial(nodes.SparseNodeVisitor):
227
228 """
229 Perform the special processing needed by PEP 0:
230
231 - Mask email addresses.
232
233 - Link PEP numbers in the second column of 4-column tables to the PEPs
234 themselves.
235 """
236
237 pep_url = Headers.pep_url
238
239 def unknown_visit(self, node):
240 pass
241
242 def visit_reference(self, node):
243 node.replace_self(mask_email(node))
244
245 def visit_field_list(self, node):
246 if 'rfc2822' in node['classes']:
247 raise nodes.SkipNode
248
249 def visit_tgroup(self, node):
250 self.pep_table = node['cols'] == 4
251 self.entry = 0
252
253 def visit_colspec(self, node):
254 self.entry += 1
255 if self.pep_table and self.entry == 2:
256 node['classes'].append('num')
257
258 def visit_row(self, node):
259 self.entry = 0
260
261 def visit_entry(self, node):
262 self.entry += 1
263 if self.pep_table and self.entry == 2 and len(node) == 1:
264 node['classes'].append('num')
265 p = node[0]
266 if isinstance(p, nodes.paragraph) and len(p) == 1:
267 text = p.astext()
268 try:
269 pep = int(text)
270 ref = (self.document.settings.pep_base_url
271 + self.pep_url % pep)
272 p[0] = nodes.reference(text, text, refuri=ref)
273 except ValueError:
274 pass
275
276
277 non_masked_addresses = ('peps@python.org',
278 'python-list@python.org',
279 'python-dev@python.org')
280
281 def mask_email(ref, pepno=None):
282 """
283 Mask the email address in `ref` and return a replacement node.
284
285 `ref` is returned unchanged if it contains no email address.
286
287 For email addresses such as "user@host", mask the address as "user at
288 host" (text) to thwart simple email address harvesters (except for those
289 listed in `non_masked_addresses`). If a PEP number (`pepno`) is given,
290 return a reference including a default email subject.
291 """
292 if ref.hasattr('refuri') and ref['refuri'].startswith('mailto:'):
293 if ref['refuri'][8:] in non_masked_addresses:
294 replacement = ref[0]
295 else:
296 replacement_text = ref.astext().replace('@', '&#32;&#97;t&#32;')
297 replacement = nodes.raw('', replacement_text, format='html')
298 if pepno is None:
299 return replacement
300 else:
301 ref['refuri'] += '?subject=PEP%%20%s' % pepno
302 ref[:] = [replacement]
303 return ref
304 else:
305 return ref