Skip to content

Commit

Permalink
Sphinx support: add PEP 0 generation extension (1932)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
AA-Turner authored Jun 12, 2021
1 parent 0f27839 commit 749c2d9
Show file tree
Hide file tree
Showing 12 changed files with 717 additions and 6 deletions.
11 changes: 11 additions & 0 deletions AUTHOR_OVERRIDES.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Overridden Name,Surname First,Name Reference
The Python core team and community,The Python core team and community,python-dev
Ernest W. Durbin III,"Durbin, Ernest W., III",Durbin
Greg Ewing,"Ewing, Gregory",Ewing
Guido van Rossum,"van Rossum, Guido (GvR)",GvR
Inada Naoki,"Inada, Naoki",Inada
Jim Jewett,"Jewett, Jim J.",Jewett
Just van Rossum,"van Rossum, Just (JvR)",JvR
Martin v. Löwis,"von Löwis, Martin",von Löwis
Nathaniel Smith,"Smith, Nathaniel J.",Smith
P.J. Eby,"Eby, Phillip J.",Eby
11 changes: 11 additions & 0 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import argparse
from pathlib import Path
import shutil

from sphinx.application import Sphinx

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


def create_index_file(html_root: Path):
"""Copies PEP 0 to the root index.html so that /peps/ works."""
pep_zero_path = html_root / "pep-0000" / "index.html"
if pep_zero_path.is_file():
shutil.copy(pep_zero_path, html_root / "index.html")


if __name__ == "__main__":
args = create_parser()

Expand Down Expand Up @@ -60,3 +68,6 @@ def create_parser():
)
app.builder.copysource = False # Prevent unneeded source copying - we link direct to GitHub
app.build()

if args.index_file:
create_index_file(build_directory)
4 changes: 3 additions & 1 deletion pep_sphinx_extensions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@

from typing import TYPE_CHECKING

from sphinx.environment import default_settings
from docutils.writers.html5_polyglot import HTMLTranslator
from sphinx.environment import default_settings

from pep_sphinx_extensions.pep_processor.html import pep_html_translator
from pep_sphinx_extensions.pep_processor.parsing import pep_parser
from pep_sphinx_extensions.pep_processor.parsing import pep_role
from pep_sphinx_extensions.pep_zero_generator.pep_index_generator import create_pep_zero

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

# Mathematics rendering
inline_maths = HTMLTranslator.visit_math, _depart_maths
Expand Down
4 changes: 2 additions & 2 deletions pep_sphinx_extensions/pep_processor/parsing/pep_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

from sphinx import parsers

from pep_sphinx_extensions.pep_processor.transforms import pep_headers
from pep_sphinx_extensions.pep_processor.transforms import pep_title
from pep_sphinx_extensions.pep_processor.transforms import pep_contents
from pep_sphinx_extensions.pep_processor.transforms import pep_footer
from pep_sphinx_extensions.pep_processor.transforms import pep_headers
from pep_sphinx_extensions.pep_processor.transforms import pep_title

if TYPE_CHECKING:
from docutils import transforms
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import datetime
import subprocess
from pathlib import Path
import subprocess

from docutils import nodes
from docutils import transforms
Expand Down
4 changes: 2 additions & 2 deletions pep_sphinx_extensions/pep_processor/transforms/pep_headers.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import re
from pathlib import Path
import re

from docutils import nodes
from docutils import transforms
from docutils.transforms import peps
from sphinx import errors

from pep_sphinx_extensions.pep_processor.transforms import pep_zero
from pep_sphinx_extensions.config import pep_url
from pep_sphinx_extensions.pep_processor.transforms import pep_zero


class PEPParsingError(errors.SphinxError):
Expand Down
93 changes: 93 additions & 0 deletions pep_sphinx_extensions/pep_zero_generator/author.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from __future__ import annotations

from typing import NamedTuple


class _Name(NamedTuple):
mononym: str = None
forename: str = None
surname: str = None
suffix: str = None


class Author(NamedTuple):
"""Represent PEP authors."""
last_first: str # The author's name in Surname, Forename, Suffix order.
nick: str # Author's nickname for PEP tables. Defaults to surname.
email: str # The author's email address.


def parse_author_email(author_email_tuple: tuple[str, str], authors_overrides: dict[str, dict[str, str]]) -> Author:
"""Parse the name and email address of an author."""
name, email = author_email_tuple
_first_last = name.strip()
email = email.lower()

if _first_last in authors_overrides:
name_dict = authors_overrides[_first_last]
last_first = name_dict["Surname First"]
nick = name_dict["Name Reference"]
return Author(last_first, nick, email)

name_parts = _parse_name(_first_last)
if name_parts.mononym is not None:
return Author(name_parts.mononym, name_parts.mononym, email)

if name_parts.surname[1] == ".":
# Add an escape to avoid docutils turning `v.` into `22.`.
name_parts.surname = f"\\{name_parts.surname}"

if name_parts.suffix:
last_first = f"{name_parts.surname}, {name_parts.forename}, {name_parts.suffix}"
return Author(last_first, name_parts.surname, email)

last_first = f"{name_parts.surname}, {name_parts.forename}"
return Author(last_first, name_parts.surname, email)


def _parse_name(full_name: str) -> _Name:
"""Decompose a full name into parts.
If a mononym (e.g, 'Aahz') then return the full name. If there are
suffixes in the name (e.g. ', Jr.' or 'II'), then find and extract
them. If there is a middle initial followed by a full stop, then
combine the following words into a surname (e.g. N. Vander Weele). If
there is a leading, lowercase portion to the last name (e.g. 'van' or
'von') then include it in the surname.
"""
possible_suffixes = {"Jr", "Jr.", "II", "III"}

pre_suffix, _, raw_suffix = full_name.partition(",")
name_parts = pre_suffix.strip().split(" ")
num_parts = len(name_parts)
suffix = raw_suffix.strip()

if num_parts == 0:
raise ValueError("Name is empty!")
elif num_parts == 1:
return _Name(mononym=name_parts[0], suffix=suffix)
elif num_parts == 2:
return _Name(forename=name_parts[0].strip(), surname=name_parts[1], suffix=suffix)

# handles rogue uncaught suffixes
if name_parts[-1] in possible_suffixes:
suffix = f"{name_parts.pop(-1)} {suffix}".strip()

# handles von, van, v. etc.
if name_parts[-2].islower():
forename = " ".join(name_parts[:-2]).strip()
surname = " ".join(name_parts[-2:])
return _Name(forename=forename, surname=surname, suffix=suffix)

# handles double surnames after a middle initial (e.g. N. Vander Weele)
elif any(s.endswith(".") for s in name_parts):
split_position = [i for i, x in enumerate(name_parts) if x.endswith(".")][-1] + 1
forename = " ".join(name_parts[:split_position]).strip()
surname = " ".join(name_parts[split_position:])
return _Name(forename=forename, surname=surname, suffix=suffix)

# default to using the last item as the surname
else:
forename = " ".join(name_parts[:-1]).strip()
return _Name(forename=forename, surname=name_parts[-1], suffix=suffix)
34 changes: 34 additions & 0 deletions pep_sphinx_extensions/pep_zero_generator/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Holds type and status constants for PEP 0 generation."""

STATUS_ACCEPTED = "Accepted"
STATUS_ACTIVE = "Active"
STATUS_DEFERRED = "Deferred"
STATUS_DRAFT = "Draft"
STATUS_FINAL = "Final"
STATUS_PROVISIONAL = "Provisional"
STATUS_REJECTED = "Rejected"
STATUS_SUPERSEDED = "Superseded"
STATUS_WITHDRAWN = "Withdrawn"

# Valid values for the Status header.
STATUS_VALUES = {
STATUS_ACCEPTED, STATUS_PROVISIONAL, STATUS_REJECTED, STATUS_WITHDRAWN,
STATUS_DEFERRED, STATUS_FINAL, STATUS_ACTIVE, STATUS_DRAFT, STATUS_SUPERSEDED,
}
# Map of invalid/special statuses to their valid counterparts
SPECIAL_STATUSES = {
"April Fool!": STATUS_REJECTED, # See PEP 401 :)
}
# Draft PEPs have no status displayed, Active shares a key with Accepted
HIDE_STATUS = {STATUS_DRAFT, STATUS_ACTIVE}
# Dead PEP statuses
DEAD_STATUSES = {STATUS_REJECTED, STATUS_WITHDRAWN, STATUS_SUPERSEDED}

TYPE_INFO = "Informational"
TYPE_PROCESS = "Process"
TYPE_STANDARDS = "Standards Track"

# Valid values for the Type header.
TYPE_VALUES = {TYPE_STANDARDS, TYPE_INFO, TYPE_PROCESS}
# Active PEPs can only be for Informational or Process PEPs.
ACTIVE_ALLOWED = {TYPE_PROCESS, TYPE_INFO}
16 changes: 16 additions & 0 deletions pep_sphinx_extensions/pep_zero_generator/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from __future__ import annotations

from pathlib import Path


class PEPError(Exception):
def __init__(self, error: str, pep_file: Path, pep_number: int | None = None):
super().__init__(error)
self.filename = pep_file
self.number = pep_number

def __str__(self):
error_msg = super(PEPError, self).__str__()
error_msg = f"({self.filename}): {error_msg}"
pep_str = f"PEP {self.number}"
return f"{pep_str} {error_msg}" if self.number is not None else error_msg
Loading

0 comments on commit 749c2d9

Please sign in to comment.