Skip to content

Commit

Permalink
Add support for topic indices (#2579)
Browse files Browse the repository at this point in the history
Co-authored-by: Łukasz Langa <[email protected]>
  • Loading branch information
AA-Turner and ambv authored Jun 18, 2022
1 parent da13103 commit a2f2d6c
Show file tree
Hide file tree
Showing 11 changed files with 157 additions and 59 deletions.
7 changes: 7 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,13 @@ repos:
files: '^pep-\d+\.(rst|txt)$'
types: [text]

- id: validate-topic
name: "'Topic' must be for a valid sub-index"
language: pygrep
entry: '^Topic:(?:(?! +(Packaging|Typing|Packaging, Typing)$))'
files: '^pep-\d+\.(rst|txt)$'
types: [text]

- id: validate-content-type
name: "'Content-Type' must be 'text/x-rst'"
language: pygrep
Expand Down
1 change: 1 addition & 0 deletions contents.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ This is an internal Sphinx page; please go to the :doc:`PEP Index <pep-0000>`.

docs/*
pep-*
topic/*
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def get_doc_context(self, docname: str, body: str, _metatags: str) -> dict:

# local table of contents
toc_tree = self.env.tocs[docname].deepcopy()
if len(toc_tree[0]) > 1:
if len(toc_tree) and len(toc_tree[0]) > 1:
toc_tree = toc_tree[0][1] # don't include document title
del toc_tree[0] # remove contents node
for node in toc_tree.findall(nodes.reference):
Expand Down
14 changes: 14 additions & 0 deletions pep_sphinx_extensions/pep_zero_generator/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,17 @@
TYPE_VALUES = {TYPE_STANDARDS, TYPE_INFO, TYPE_PROCESS}
# Active PEPs can only be for Informational or Process PEPs.
ACTIVE_ALLOWED = {TYPE_PROCESS, TYPE_INFO}

# map of topic -> additional description
SUBINDICES_BY_TOPIC = {
"packaging": """\
The canonical, up-to-date packaging specifications can be found on the
`Python Packaging Authority`_ (PyPA) `specifications`_ page.
Packaging PEPs follow the `PyPA specification update process`_.
They are used to propose major additions or changes to the PyPA specifications.
.. _Python Packaging Authority: https://www.pypa.io/
.. _specifications: https://packaging.python.org/en/latest/specifications/
.. _PyPA specification update process: https://www.pypa.io/en/latest/specifications/#specification-update-process
""",
}
18 changes: 16 additions & 2 deletions pep_sphinx_extensions/pep_zero_generator/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import csv
from email.parser import HeaderParser
from pathlib import Path
import re
Expand All @@ -22,6 +23,14 @@
from pep_sphinx_extensions.pep_zero_generator.author import Author


# AUTHOR_OVERRIDES.csv is an exception file for PEP 0 name parsing
AUTHOR_OVERRIDES: dict[str, dict[str, str]] = {}
with open("AUTHOR_OVERRIDES.csv", "r", encoding="utf-8") as f:
for line in csv.DictReader(f):
full_name = line.pop("Overridden Name")
AUTHOR_OVERRIDES[full_name] = line


class PEP:
"""Representation of PEPs.
Expand All @@ -37,7 +46,7 @@ class PEP:
# The required RFC 822 headers for all PEPs.
required_headers = {"PEP", "Title", "Author", "Status", "Type", "Created"}

def __init__(self, filename: Path, authors_overrides: dict):
def __init__(self, filename: Path):
"""Init object from an open PEP file object.
pep_file is full text of the PEP file, filename is path of the PEP file, author_lookup is author exceptions file
Expand Down Expand Up @@ -88,7 +97,11 @@ def __init__(self, filename: Path, authors_overrides: dict):
self.status: str = status

# Parse PEP authors
self.authors: list[Author] = _parse_authors(self, metadata["Author"], authors_overrides)
self.authors: list[Author] = _parse_authors(self, metadata["Author"], AUTHOR_OVERRIDES)

# Topic (for sub-indices)
_topic = metadata.get("Topic", "").lower().split(",")
self.topic: set[str] = {topic for topic_raw in _topic if (topic := topic_raw.strip())}

# Other headers
self.created = metadata["Created"]
Expand Down Expand Up @@ -136,6 +149,7 @@ def full_details(self) -> dict[str, str]:
"discussions_to": self.discussions_to,
"status": self.status,
"type": self.pep_type,
"topic": ", ".join(sorted(self.topic)),
"created": self.created,
"python_version": self.python_version,
"post_history": self.post_history,
Expand Down
45 changes: 17 additions & 28 deletions pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,60 +17,49 @@
"""
from __future__ import annotations

import csv
import json
from pathlib import Path
import re
from typing import TYPE_CHECKING

from pep_sphinx_extensions.pep_zero_generator.constants import SUBINDICES_BY_TOPIC
from pep_sphinx_extensions.pep_zero_generator import parser
from pep_sphinx_extensions.pep_zero_generator import subindices
from pep_sphinx_extensions.pep_zero_generator import writer

if TYPE_CHECKING:
from sphinx.application import Sphinx
from sphinx.environment import BuildEnvironment


def create_pep_json(peps: list[parser.PEP]) -> str:
return json.dumps({pep.number: pep.full_details for pep in peps}, indent=1)


def create_pep_zero(app: Sphinx, env: BuildEnvironment, docnames: list[str]) -> None:
def _parse_peps() -> list[parser.PEP]:
# Read from root directory
path = Path(".")

pep_zero_filename = "pep-0000"
peps: list[parser.PEP] = []
pep_pat = re.compile(r"pep-\d{4}") # Path.match() doesn't support regular expressions

# AUTHOR_OVERRIDES.csv is an exception file for PEP0 name parsing
with open("AUTHOR_OVERRIDES.csv", "r", encoding="utf-8") as f:
authors_overrides = {}
for line in csv.DictReader(f):
full_name = line.pop("Overridden Name")
authors_overrides[full_name] = line

for file_path in path.iterdir():
if not file_path.is_file():
continue # Skip directories etc.
if file_path.match("pep-0000*"):
continue # Skip pre-existing PEP 0 files
if pep_pat.match(str(file_path)) and file_path.suffix in {".txt", ".rst"}:
pep = parser.PEP(path.joinpath(file_path).absolute(), authors_overrides)
if file_path.match("pep-????.???") and file_path.suffix in {".txt", ".rst"}:
pep = parser.PEP(path.joinpath(file_path).absolute())
peps.append(pep)

peps = sorted(peps)
return sorted(peps)

pep0_text = writer.PEPZeroWriter().write_pep0(peps)
pep0_path = Path(f"{pep_zero_filename}.rst")
pep0_path.write_text(pep0_text, encoding="utf-8")

peps.append(parser.PEP(pep0_path, authors_overrides))
def create_pep_json(peps: list[parser.PEP]) -> str:
return json.dumps({pep.number: pep.full_details for pep in peps}, indent=1)


def create_pep_zero(app: Sphinx, env: BuildEnvironment, docnames: list[str]) -> None:
peps = _parse_peps()

pep0_text = writer.PEPZeroWriter().write_pep0(peps)
pep0_path = subindices.update_sphinx("pep-0000", pep0_text, docnames, env)
peps.append(parser.PEP(pep0_path))

# Add to files for builder
docnames.insert(1, pep_zero_filename)
# Add to files for writer
env.found_docs.add(pep_zero_filename)
subindices.generate_subindices(SUBINDICES_BY_TOPIC, peps, docnames, env)

# Create peps.json
json_path = Path(app.outdir, "api", "peps.json").resolve()
Expand Down
71 changes: 71 additions & 0 deletions pep_sphinx_extensions/pep_zero_generator/subindices.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Utilities to support sub-indices for PEPs."""

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING

from pep_sphinx_extensions.pep_zero_generator import writer

if TYPE_CHECKING:
from sphinx.environment import BuildEnvironment

from pep_sphinx_extensions.pep_zero_generator.parser import PEP


def update_sphinx(filename: str, text: str, docnames: list[str], env: BuildEnvironment) -> Path:
file_path = Path(f"{filename}.rst").resolve()
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(text, encoding="utf-8")

# Add to files for builder
docnames.append(filename)
# Add to files for writer
env.found_docs.add(filename)

return file_path


def generate_subindices(
subindices: dict[str, str],
peps: list[PEP],
docnames: list[str],
env: BuildEnvironment,
) -> None:
# Create sub index page
generate_topic_contents(docnames, env)

for subindex, additional_description in subindices.items():
header_text = f"{subindex.title()} PEPs"
header_line = "#" * len(header_text)
header = header_text + "\n" + header_line + "\n"

topic = subindex.lower()
filtered_peps = [pep for pep in peps if topic in pep.topic]
subindex_intro = f"""\
This is the index of all Python Enhancement Proposals (PEPs) labelled
under the '{subindex.title()}' topic. This is a sub-index of :pep:`0`,
the PEP index.
{additional_description}
"""
subindex_text = writer.PEPZeroWriter().write_pep0(
filtered_peps, header, subindex_intro, is_pep0=False,
)
update_sphinx(f"topic/{subindex}", subindex_text, docnames, env)


def generate_topic_contents(docnames: list[str], env: BuildEnvironment):
update_sphinx(f"topic/index", """\
Topic Index
***********
PEPs are indexed by topic on the pages below:
.. toctree::
:maxdepth: 1
:titlesonly:
:glob:
*
""", docnames, env)
28 changes: 18 additions & 10 deletions pep_sphinx_extensions/pep_zero_generator/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
if TYPE_CHECKING:
from pep_sphinx_extensions.pep_zero_generator.parser import PEP

header = f"""\
HEADER = f"""\
PEP: 0
Title: Index of Python Enhancement Proposals (PEPs)
Last-Modified: {datetime.date.today()}
Expand All @@ -36,12 +36,13 @@
Created: 13-Jul-2000
"""

intro = """\
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.
the PEP texts represent their historical record. The PEPs are
:doc:`indexed by topic <topic/index>` for specialist subjects.
"""


Expand Down Expand Up @@ -112,7 +113,9 @@ def emit_pep_category(self, category: str, peps: list[PEP]) -> None:
self.emit_text(" -")
self.emit_newline()

def write_pep0(self, peps: list[PEP]):
def write_pep0(self, peps: list[PEP], header: str = HEADER, intro: str = INTRO, is_pep0: bool = True):
if len(peps) == 0:
return ""

# PEP metadata
self.emit_text(header)
Expand All @@ -138,7 +141,10 @@ def write_pep0(self, peps: list[PEP]):
("Abandoned, Withdrawn, and Rejected PEPs", dead),
]
for (category, peps_in_category) in pep_categories:
self.emit_pep_category(category, peps_in_category)
# For sub-indices, only emit categories with entries.
# For PEP 0, emit every category
if is_pep0 or len(peps_in_category) > 0:
self.emit_pep_category(category, peps_in_category)

self.emit_newline()

Expand All @@ -151,12 +157,14 @@ def write_pep0(self, peps: list[PEP]):
self.emit_newline()

# Reserved PEP numbers
self.emit_title("Reserved PEP Numbers")
self.emit_column_headers()
for number, claimants in sorted(self.RESERVED.items()):
self.emit_pep_row(type="", status="", number=number, title="RESERVED", authors=claimants)
if is_pep0:
self.emit_title("Reserved PEP Numbers")
self.emit_column_headers()
for number, claimants in sorted(self.RESERVED.items()):
self.emit_pep_row(type="", status="", number=number, title="RESERVED", authors=claimants)

self.emit_newline()

self.emit_newline()

# PEP types key
self.emit_title("PEP Types Key")
Expand Down
20 changes: 10 additions & 10 deletions pep_sphinx_extensions/tests/pep_zero_generator/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,27 @@


def test_pep_repr():
pep8 = parser.PEP(Path("pep-0008.txt"), AUTHORS_OVERRIDES)
pep8 = parser.PEP(Path("pep-0008.txt"))

assert repr(pep8) == "<PEP 0008 - Style Guide for Python Code>"


def test_pep_less_than():
pep8 = parser.PEP(Path("pep-0008.txt"), AUTHORS_OVERRIDES)
pep3333 = parser.PEP(Path("pep-3333.txt"), AUTHORS_OVERRIDES)
pep8 = parser.PEP(Path("pep-0008.txt"))
pep3333 = parser.PEP(Path("pep-3333.txt"))

assert pep8 < pep3333


def test_pep_equal():
pep_a = parser.PEP(Path("pep-0008.txt"), AUTHORS_OVERRIDES)
pep_b = parser.PEP(Path("pep-0008.txt"), AUTHORS_OVERRIDES)
pep_a = parser.PEP(Path("pep-0008.txt"))
pep_b = parser.PEP(Path("pep-0008.txt"))

assert pep_a == pep_b


def test_pep_details():
pep8 = parser.PEP(Path("pep-0008.txt"), AUTHORS_OVERRIDES)
def test_pep_details(monkeypatch):
pep8 = parser.PEP(Path("pep-0008.txt"))

assert pep8.details == {
"authors": "GvR, Warsaw, Coghlan",
Expand Down Expand Up @@ -64,18 +64,18 @@ def test_pep_details():
)
def test_parse_authors(test_input, expected):
# Arrange
pep = parser.PEP(Path("pep-0160.txt"), AUTHORS_OVERRIDES)
dummy_object = parser.PEP(Path("pep-0160.txt"))

# Act
out = parser._parse_authors(pep, test_input, AUTHORS_OVERRIDES)
out = parser._parse_authors(dummy_object, test_input, AUTHORS_OVERRIDES)

# Assert
assert out == expected


def test_parse_authors_invalid():

pep = parser.PEP(Path("pep-0008.txt"), AUTHORS_OVERRIDES)
pep = parser.PEP(Path("pep-0008.txt"))

with pytest.raises(PEPError, match="no authors found"):
parser._parse_authors(pep, "", AUTHORS_OVERRIDES)
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from pathlib import Path

from pep_sphinx_extensions.pep_zero_generator import parser, pep_index_generator
from pep_sphinx_extensions.tests.utils import AUTHORS_OVERRIDES


def test_create_pep_json():
peps = [parser.PEP(Path("pep-0008.txt"), AUTHORS_OVERRIDES)]
peps = [parser.PEP(Path("pep-0008.txt"))]

out = pep_index_generator.create_pep_json(peps)

Expand Down
Loading

0 comments on commit a2f2d6c

Please sign in to comment.