Skip to content

Commit 749c2d9

Browse files
authored
Sphinx support: add PEP 0 generation extension (1932)
* Add PEP 0 parser * Add PEP 0 writer * Add PEP 0 generator and authors override * Add/update build and run * Simplify `create_index_file` * Special status handling * Add constants for PEP related magic strings * Prefer checking on class * Add PEP.hide_status, use constants * Remove comment from 2008 (current method works fine) * Clarify intent of for-else loop * Hook in to Sphinx (oops, missed when splitting out this PR) * Rename AUTHORS.csv for clarity * Sort and strip spaces * Prefer `authors_overrides` name * Add pep_0_errors.py * Move author_sort_by to writer * PEP init misc * Split out Author * Drop pep_0 prefix * Pass title length as an argument * Add constants.py to hold global type / status values * Capitalise constants * Capitalise constants * Update PEP classification algorithm * Extract static methods to module level * Add emit_text, emit_pep_row * Use constants in writer.py * Sort imports * Sort constants * Fix sorting in historical and dead PEPs * Extract static methods to module level * Extract static methods to module level (parser.py * Make Author a NamedTuple * Fix author duplication bug with NamedTuples * Revert to old PEP classification algorithm * Define PEP equality
1 parent 0f27839 commit 749c2d9

File tree

12 files changed

+717
-6
lines changed

12 files changed

+717
-6
lines changed

AUTHOR_OVERRIDES.csv

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Overridden Name,Surname First,Name Reference
2+
The Python core team and community,The Python core team and community,python-dev
3+
Ernest W. Durbin III,"Durbin, Ernest W., III",Durbin
4+
Greg Ewing,"Ewing, Gregory",Ewing
5+
Guido van Rossum,"van Rossum, Guido (GvR)",GvR
6+
Inada Naoki,"Inada, Naoki",Inada
7+
Jim Jewett,"Jewett, Jim J.",Jewett
8+
Just van Rossum,"van Rossum, Just (JvR)",JvR
9+
Martin v. Löwis,"von Löwis, Martin",von Löwis
10+
Nathaniel Smith,"Smith, Nathaniel J.",Smith
11+
P.J. Eby,"Eby, Phillip J.",Eby

build.py

+11
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import argparse
44
from pathlib import Path
5+
import shutil
56

67
from sphinx.application import Sphinx
78

@@ -24,6 +25,13 @@ def create_parser():
2425
return parser.parse_args()
2526

2627

28+
def create_index_file(html_root: Path):
29+
"""Copies PEP 0 to the root index.html so that /peps/ works."""
30+
pep_zero_path = html_root / "pep-0000" / "index.html"
31+
if pep_zero_path.is_file():
32+
shutil.copy(pep_zero_path, html_root / "index.html")
33+
34+
2735
if __name__ == "__main__":
2836
args = create_parser()
2937

@@ -60,3 +68,6 @@ def create_parser():
6068
)
6169
app.builder.copysource = False # Prevent unneeded source copying - we link direct to GitHub
6270
app.build()
71+
72+
if args.index_file:
73+
create_index_file(build_directory)

pep_sphinx_extensions/__init__.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44

55
from typing import TYPE_CHECKING
66

7-
from sphinx.environment import default_settings
87
from docutils.writers.html5_polyglot import HTMLTranslator
8+
from sphinx.environment import default_settings
99

1010
from pep_sphinx_extensions.pep_processor.html import pep_html_translator
1111
from pep_sphinx_extensions.pep_processor.parsing import pep_parser
1212
from pep_sphinx_extensions.pep_processor.parsing import pep_role
13+
from pep_sphinx_extensions.pep_zero_generator.pep_index_generator import create_pep_zero
1314

1415
if TYPE_CHECKING:
1516
from sphinx.application import Sphinx
@@ -37,6 +38,7 @@ def setup(app: Sphinx) -> dict[str, bool]:
3738
app.add_source_parser(pep_parser.PEPParser) # Add PEP transforms
3839
app.add_role("pep", pep_role.PEPRole(), override=True) # Transform PEP references to links
3940
app.set_translator("html", pep_html_translator.PEPTranslator) # Docutils Node Visitor overrides
41+
app.connect("env-before-read-docs", create_pep_zero) # PEP 0 hook
4042

4143
# Mathematics rendering
4244
inline_maths = HTMLTranslator.visit_math, _depart_maths

pep_sphinx_extensions/pep_processor/parsing/pep_parser.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
from sphinx import parsers
66

7-
from pep_sphinx_extensions.pep_processor.transforms import pep_headers
8-
from pep_sphinx_extensions.pep_processor.transforms import pep_title
97
from pep_sphinx_extensions.pep_processor.transforms import pep_contents
108
from pep_sphinx_extensions.pep_processor.transforms import pep_footer
9+
from pep_sphinx_extensions.pep_processor.transforms import pep_headers
10+
from pep_sphinx_extensions.pep_processor.transforms import pep_title
1111

1212
if TYPE_CHECKING:
1313
from docutils import transforms

pep_sphinx_extensions/pep_processor/transforms/pep_footer.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import datetime
2-
import subprocess
32
from pathlib import Path
3+
import subprocess
44

55
from docutils import nodes
66
from docutils import transforms

pep_sphinx_extensions/pep_processor/transforms/pep_headers.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import re
21
from pathlib import Path
2+
import re
33

44
from docutils import nodes
55
from docutils import transforms
66
from docutils.transforms import peps
77
from sphinx import errors
88

9-
from pep_sphinx_extensions.pep_processor.transforms import pep_zero
109
from pep_sphinx_extensions.config import pep_url
10+
from pep_sphinx_extensions.pep_processor.transforms import pep_zero
1111

1212

1313
class PEPParsingError(errors.SphinxError):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from __future__ import annotations
2+
3+
from typing import NamedTuple
4+
5+
6+
class _Name(NamedTuple):
7+
mononym: str = None
8+
forename: str = None
9+
surname: str = None
10+
suffix: str = None
11+
12+
13+
class Author(NamedTuple):
14+
"""Represent PEP authors."""
15+
last_first: str # The author's name in Surname, Forename, Suffix order.
16+
nick: str # Author's nickname for PEP tables. Defaults to surname.
17+
email: str # The author's email address.
18+
19+
20+
def parse_author_email(author_email_tuple: tuple[str, str], authors_overrides: dict[str, dict[str, str]]) -> Author:
21+
"""Parse the name and email address of an author."""
22+
name, email = author_email_tuple
23+
_first_last = name.strip()
24+
email = email.lower()
25+
26+
if _first_last in authors_overrides:
27+
name_dict = authors_overrides[_first_last]
28+
last_first = name_dict["Surname First"]
29+
nick = name_dict["Name Reference"]
30+
return Author(last_first, nick, email)
31+
32+
name_parts = _parse_name(_first_last)
33+
if name_parts.mononym is not None:
34+
return Author(name_parts.mononym, name_parts.mononym, email)
35+
36+
if name_parts.surname[1] == ".":
37+
# Add an escape to avoid docutils turning `v.` into `22.`.
38+
name_parts.surname = f"\\{name_parts.surname}"
39+
40+
if name_parts.suffix:
41+
last_first = f"{name_parts.surname}, {name_parts.forename}, {name_parts.suffix}"
42+
return Author(last_first, name_parts.surname, email)
43+
44+
last_first = f"{name_parts.surname}, {name_parts.forename}"
45+
return Author(last_first, name_parts.surname, email)
46+
47+
48+
def _parse_name(full_name: str) -> _Name:
49+
"""Decompose a full name into parts.
50+
51+
If a mononym (e.g, 'Aahz') then return the full name. If there are
52+
suffixes in the name (e.g. ', Jr.' or 'II'), then find and extract
53+
them. If there is a middle initial followed by a full stop, then
54+
combine the following words into a surname (e.g. N. Vander Weele). If
55+
there is a leading, lowercase portion to the last name (e.g. 'van' or
56+
'von') then include it in the surname.
57+
58+
"""
59+
possible_suffixes = {"Jr", "Jr.", "II", "III"}
60+
61+
pre_suffix, _, raw_suffix = full_name.partition(",")
62+
name_parts = pre_suffix.strip().split(" ")
63+
num_parts = len(name_parts)
64+
suffix = raw_suffix.strip()
65+
66+
if num_parts == 0:
67+
raise ValueError("Name is empty!")
68+
elif num_parts == 1:
69+
return _Name(mononym=name_parts[0], suffix=suffix)
70+
elif num_parts == 2:
71+
return _Name(forename=name_parts[0].strip(), surname=name_parts[1], suffix=suffix)
72+
73+
# handles rogue uncaught suffixes
74+
if name_parts[-1] in possible_suffixes:
75+
suffix = f"{name_parts.pop(-1)} {suffix}".strip()
76+
77+
# handles von, van, v. etc.
78+
if name_parts[-2].islower():
79+
forename = " ".join(name_parts[:-2]).strip()
80+
surname = " ".join(name_parts[-2:])
81+
return _Name(forename=forename, surname=surname, suffix=suffix)
82+
83+
# handles double surnames after a middle initial (e.g. N. Vander Weele)
84+
elif any(s.endswith(".") for s in name_parts):
85+
split_position = [i for i, x in enumerate(name_parts) if x.endswith(".")][-1] + 1
86+
forename = " ".join(name_parts[:split_position]).strip()
87+
surname = " ".join(name_parts[split_position:])
88+
return _Name(forename=forename, surname=surname, suffix=suffix)
89+
90+
# default to using the last item as the surname
91+
else:
92+
forename = " ".join(name_parts[:-1]).strip()
93+
return _Name(forename=forename, surname=name_parts[-1], suffix=suffix)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""Holds type and status constants for PEP 0 generation."""
2+
3+
STATUS_ACCEPTED = "Accepted"
4+
STATUS_ACTIVE = "Active"
5+
STATUS_DEFERRED = "Deferred"
6+
STATUS_DRAFT = "Draft"
7+
STATUS_FINAL = "Final"
8+
STATUS_PROVISIONAL = "Provisional"
9+
STATUS_REJECTED = "Rejected"
10+
STATUS_SUPERSEDED = "Superseded"
11+
STATUS_WITHDRAWN = "Withdrawn"
12+
13+
# Valid values for the Status header.
14+
STATUS_VALUES = {
15+
STATUS_ACCEPTED, STATUS_PROVISIONAL, STATUS_REJECTED, STATUS_WITHDRAWN,
16+
STATUS_DEFERRED, STATUS_FINAL, STATUS_ACTIVE, STATUS_DRAFT, STATUS_SUPERSEDED,
17+
}
18+
# Map of invalid/special statuses to their valid counterparts
19+
SPECIAL_STATUSES = {
20+
"April Fool!": STATUS_REJECTED, # See PEP 401 :)
21+
}
22+
# Draft PEPs have no status displayed, Active shares a key with Accepted
23+
HIDE_STATUS = {STATUS_DRAFT, STATUS_ACTIVE}
24+
# Dead PEP statuses
25+
DEAD_STATUSES = {STATUS_REJECTED, STATUS_WITHDRAWN, STATUS_SUPERSEDED}
26+
27+
TYPE_INFO = "Informational"
28+
TYPE_PROCESS = "Process"
29+
TYPE_STANDARDS = "Standards Track"
30+
31+
# Valid values for the Type header.
32+
TYPE_VALUES = {TYPE_STANDARDS, TYPE_INFO, TYPE_PROCESS}
33+
# Active PEPs can only be for Informational or Process PEPs.
34+
ACTIVE_ALLOWED = {TYPE_PROCESS, TYPE_INFO}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
5+
6+
class PEPError(Exception):
7+
def __init__(self, error: str, pep_file: Path, pep_number: int | None = None):
8+
super().__init__(error)
9+
self.filename = pep_file
10+
self.number = pep_number
11+
12+
def __str__(self):
13+
error_msg = super(PEPError, self).__str__()
14+
error_msg = f"({self.filename}): {error_msg}"
15+
pep_str = f"PEP {self.number}"
16+
return f"{pep_str} {error_msg}" if self.number is not None else error_msg

0 commit comments

Comments
 (0)