forked from python/peps
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwriter.py
More file actions
368 lines (321 loc) · 14 KB
/
writer.py
File metadata and controls
368 lines (321 loc) · 14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
"""Code to handle the output of PEP 0."""
from __future__ import annotations
from typing import TYPE_CHECKING
import unicodedata
from pep_sphinx_extensions.pep_processor.transforms.pep_headers import ABBREVIATED_STATUSES
from pep_sphinx_extensions.pep_processor.transforms.pep_headers import ABBREVIATED_TYPES
from pep_sphinx_extensions.pep_zero_generator.constants import DEAD_STATUSES
from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_ACCEPTED
from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_ACTIVE
from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_DEFERRED
from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_DRAFT
from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_FINAL
from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_PROVISIONAL
from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_REJECTED
from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_VALUES
from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_WITHDRAWN
from pep_sphinx_extensions.pep_zero_generator.constants import SUBINDICES_BY_TOPIC
from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_INFO
from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_PROCESS
from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_VALUES
from pep_sphinx_extensions.pep_zero_generator.errors import PEPError
if TYPE_CHECKING:
from pep_sphinx_extensions.pep_zero_generator.parser import PEP
HEADER = """\
PEP: 0
Title: Index of Python Enhancement Proposals (PEPs)
Author: The PEP Editors
Status: Active
Type: Informational
Content-Type: text/x-rst
Created: 13-Jul-2000
"""
INTRO = """\
This PEP contains the index of all Python Enhancement Proposals,
known as PEPs. PEP numbers are :pep:`assigned <1#pep-editors>`
by the PEP editors, and once assigned are never changed. The
`version control history <https://github.com/python/peps>`_ of
the PEP texts represent their historical record.
"""
class PEPZeroWriter:
# This is a list of reserved PEP numbers. Reservations are not to be used for
# the normal PEP number allocation process - just give out the next available
# PEP number. These are for "special" numbers that may be used for semantic,
# humorous, or other such reasons, e.g. 401, 666, 754.
#
# PEP numbers may only be reserved with the approval of a PEP editor. Fields
# here are the PEP number being reserved and the claimants for the PEP.
# Although the output is sorted when PEP 0 is generated, please keep this list
# sorted as well.
RESERVED = {
801: "Warsaw",
}
def __init__(self):
self.output: list[str] = []
def emit_text(self, content: str) -> None:
# Appends content argument to the output list
self.output.append(content)
def emit_newline(self) -> None:
self.output.append("")
def emit_author_table_separator(self, max_name_len: int) -> None:
author_table_separator = "=" * max_name_len + " " + "=" * len("email address")
self.output.append(author_table_separator)
def emit_pep_row(
self,
*,
shorthand: str,
number: int,
title: str,
authors: str,
python_version: str | None = None,
) -> None:
self.emit_text(f" * - {shorthand}")
self.emit_text(f" - :pep:`{number} <{number}>`")
self.emit_text(f" - :pep:`{title.replace('`', '')} <{number}>`")
self.emit_text(f" - {authors}")
if python_version is not None:
self.emit_text(f" - {python_version}")
def emit_column_headers(self, *, include_version=True) -> None:
"""Output the column headers for the PEP indices."""
self.emit_text(".. list-table::")
self.emit_text(" :header-rows: 1")
self.emit_text(" :widths: auto")
self.emit_text(" :class: pep-zero-table")
self.emit_newline()
self.emit_text(" * - ")
self.emit_text(" - PEP")
self.emit_text(" - Title")
self.emit_text(" - Authors")
if include_version:
self.emit_text(" - ") # for Python-Version
def emit_title(self, text: str, *, symbol: str = "=") -> None:
self.output.append(text)
self.output.append(symbol * len(text))
self.emit_newline()
def emit_subtitle(self, text: str) -> None:
self.emit_title(text, symbol="-")
def emit_table(self, peps: list[PEP]) -> None:
include_version = any(pep.details["python_version"] for pep in peps)
self.emit_column_headers(include_version=include_version)
for pep in peps:
details = pep.details
if not include_version:
details.pop("python_version")
self.emit_pep_row(**details)
def emit_pep_category(self, category: str, peps: list[PEP]) -> None:
self.emit_subtitle(category)
self.emit_table(peps)
# list-table must have at least one body row
if len(peps) == 0:
self.emit_text(" * -")
self.emit_text(" -")
self.emit_text(" -")
self.emit_text(" -")
self.emit_text(" -")
self.emit_newline()
def write_numerical_index(self, peps: list[PEP]) -> str:
"""Write PEPs by number."""
self.emit_text(".. _numerical-index:")
self.emit_newline()
self.emit_title("Numerical Index")
self.emit_table(peps)
self.emit_newline()
numerical_index_string = "\n".join(self.output)
return numerical_index_string
def write_pep0(
self,
peps: list[PEP],
header: str = HEADER,
intro: str = INTRO,
is_pep0: bool = True,
builder: str = None,
) -> str:
if len(peps) == 0:
return ""
# PEP metadata
self.emit_text(header)
self.emit_newline()
# Introduction
self.emit_title("Introduction")
self.emit_text(intro)
self.emit_newline()
# PEPs by topic
if is_pep0:
self.emit_title("Topics")
self.emit_text(
"PEPs for specialist subjects are :doc:`indexed by topic <topic/index>`."
)
self.emit_newline()
for subindex in SUBINDICES_BY_TOPIC:
target = (
f"topic/{subindex}.html"
if builder == "html"
else f"../topic/{subindex}/"
)
self.emit_text(f"* `{subindex.title()} PEPs <{target}>`_")
self.emit_newline()
self.emit_newline()
self.emit_title("API")
self.emit_text(
"The `PEPS API </api/peps.json>`__ is a JSON file of metadata about "
"all the published PEPs. :doc:`Read more here <api/index>`."
)
self.emit_newline()
# PEPs by number
if is_pep0:
self.emit_title("Numerical Index")
self.emit_text(
"The :doc:`numerical index </numerical>` contains "
"a table of all PEPs, ordered by number."
)
self.emit_newline()
# PEPs by category
self.emit_title("Index by Category")
meta, info, provisional, accepted, open_, finished, historical, deferred, dead = _classify_peps(peps)
pep_categories = [
("Process and Meta-PEPs", meta),
("Other Informational PEPs", info),
("Provisional PEPs (provisionally accepted; interface may still change)", provisional),
("Accepted PEPs (accepted; may not be implemented yet)", accepted),
("Open PEPs (under consideration)", open_),
("Finished PEPs (done, with a stable interface)", finished),
("Historical Meta-PEPs and Informational PEPs", historical),
("Deferred PEPs (postponed pending further research or updates)", deferred),
("Rejected, Superseded, and Withdrawn PEPs", dead),
]
for (category, peps_in_category) in pep_categories:
# For sub-indices, only emit categories with entries.
# For PEP 0, emit every category, but only with a table when it has entries.
if len(peps_in_category) > 0:
self.emit_pep_category(category, peps_in_category)
elif is_pep0:
# emit the category with no table
self.emit_subtitle(category)
self.emit_text("None.")
self.emit_newline()
self.emit_newline()
# Reserved PEP numbers
if is_pep0:
self.emit_title("Reserved PEP Numbers")
self.emit_column_headers(include_version=False)
for number, claimants in sorted(self.RESERVED.items()):
self.emit_pep_row(
shorthand="",
number=number,
title="RESERVED",
authors=claimants,
python_version=None,
)
self.emit_newline()
# PEP types key
self.emit_title("PEP Types Key")
for type_ in sorted(TYPE_VALUES):
self.emit_text(
f"* **{type_[0]}** --- *{type_}*: {ABBREVIATED_TYPES[type_]}"
)
self.emit_newline()
self.emit_text(":pep:`More info in PEP 1 <1#pep-types>`.")
self.emit_newline()
# PEP status key
self.emit_title("PEP Status Key")
for status in sorted(STATUS_VALUES):
# Draft PEPs have no status displayed, Active shares a key with Accepted
status_code = "<No letter>" if status == STATUS_DRAFT else status[0]
self.emit_text(
f"* **{status_code}** --- *{status}*: {ABBREVIATED_STATUSES[status]}"
)
self.emit_newline()
self.emit_text(":pep:`More info in PEP 1 <1#pep-review-resolution>`.")
self.emit_newline()
if is_pep0:
# PEP owners
authors_dict = _verify_email_addresses(peps)
max_name_len = max(len(author_name) for author_name in authors_dict)
self.emit_title("Authors/Owners")
self.emit_author_table_separator(max_name_len)
self.emit_text(f"{'Name':{max_name_len}} Email Address")
self.emit_author_table_separator(max_name_len)
for author_name in _sort_authors(authors_dict):
# Use the email from authors_dict instead of the one from "author" as
# the author instance may have an empty email.
self.emit_text(f"{author_name:{max_name_len}} {authors_dict[author_name]}")
self.emit_author_table_separator(max_name_len)
self.emit_newline()
self.emit_newline()
pep0_string = "\n".join(self.output)
return pep0_string
def _classify_peps(peps: list[PEP]) -> tuple[list[PEP], ...]:
"""Sort PEPs into meta, informational, accepted, open, finished,
and essentially dead."""
meta = []
info = []
provisional = []
accepted = []
open_ = []
finished = []
historical = []
deferred = []
dead = []
for pep in peps:
# Order of 'if' statement important. Key Status values take precedence
# over Type value, and vice-versa.
if pep.status == STATUS_DRAFT:
open_.append(pep)
elif pep.status == STATUS_DEFERRED:
deferred.append(pep)
elif pep.pep_type == TYPE_PROCESS:
if pep.status in {STATUS_ACCEPTED, STATUS_ACTIVE}:
meta.append(pep)
elif pep.status in {STATUS_WITHDRAWN, STATUS_REJECTED}:
dead.append(pep)
else:
historical.append(pep)
elif pep.status in DEAD_STATUSES:
dead.append(pep)
elif pep.pep_type == TYPE_INFO:
# Hack until the conflict between the use of "Final"
# for both API definition PEPs and other (actually
# obsolete) PEPs is addressed
if pep.status == STATUS_ACTIVE or "release schedule" not in pep.title.lower():
info.append(pep)
else:
historical.append(pep)
elif pep.status == STATUS_PROVISIONAL:
provisional.append(pep)
elif pep.status in {STATUS_ACCEPTED, STATUS_ACTIVE}:
accepted.append(pep)
elif pep.status == STATUS_FINAL:
finished.append(pep)
else:
raise PEPError(f"Unsorted ({pep.pep_type}/{pep.status})", pep.filename, pep.number)
return meta, info, provisional, accepted, open_, finished, historical, deferred, dead
def _verify_email_addresses(peps: list[PEP]) -> dict[str, str]:
authors_dict: dict[str, list[str]] = {}
for pep in peps:
for author in pep.authors:
# If this is the first time we have come across an author, add them.
if author.full_name not in authors_dict:
authors_dict[author.full_name] = []
# If the new email is an empty string, move on.
if not author.email:
continue
# If the email has not been seen, add it to the list.
emails = authors_dict[author.full_name]
if author.email not in emails:
emails.append(author.email)
# Combine multiple email addresses with commas. Since peps is
# sorted by PEP number, this should produce a deterministic
# output.
return {name: ', '.join(emails) for name, emails in authors_dict.items()}
def _sort_authors(authors_dict: dict[str, str]) -> list[str]:
return sorted(authors_dict, key=_author_sort_by)
def _author_sort_by(author_name: str) -> str:
"""Skip lower-cased words in surname when sorting."""
surname, *_ = author_name.split(",")
surname_parts = surname.split()
for i, part in enumerate(surname_parts):
if part[0].isupper():
base = " ".join(surname_parts[i:]).lower()
return unicodedata.normalize("NFKD", base)
# If no capitals, use the whole string
return unicodedata.normalize("NFKD", surname.lower())