Skip to content

Commit a2f2d6c

Browse files
AA-Turnerambv
andauthored
Add support for topic indices (#2579)
Co-authored-by: Łukasz Langa <[email protected]>
1 parent da13103 commit a2f2d6c

File tree

11 files changed

+157
-59
lines changed

11 files changed

+157
-59
lines changed

.pre-commit-config.yaml

+7
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,13 @@ repos:
176176
files: '^pep-\d+\.(rst|txt)$'
177177
types: [text]
178178

179+
- id: validate-topic
180+
name: "'Topic' must be for a valid sub-index"
181+
language: pygrep
182+
entry: '^Topic:(?:(?! +(Packaging|Typing|Packaging, Typing)$))'
183+
files: '^pep-\d+\.(rst|txt)$'
184+
types: [text]
185+
179186
- id: validate-content-type
180187
name: "'Content-Type' must be 'text/x-rst'"
181188
language: pygrep

contents.rst

+1
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ This is an internal Sphinx page; please go to the :doc:`PEP Index <pep-0000>`.
1616

1717
docs/*
1818
pep-*
19+
topic/*

pep_sphinx_extensions/pep_processor/html/pep_html_builder.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def get_doc_context(self, docname: str, body: str, _metatags: str) -> dict:
3636

3737
# local table of contents
3838
toc_tree = self.env.tocs[docname].deepcopy()
39-
if len(toc_tree[0]) > 1:
39+
if len(toc_tree) and len(toc_tree[0]) > 1:
4040
toc_tree = toc_tree[0][1] # don't include document title
4141
del toc_tree[0] # remove contents node
4242
for node in toc_tree.findall(nodes.reference):

pep_sphinx_extensions/pep_zero_generator/constants.py

+14
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,17 @@
3232
TYPE_VALUES = {TYPE_STANDARDS, TYPE_INFO, TYPE_PROCESS}
3333
# Active PEPs can only be for Informational or Process PEPs.
3434
ACTIVE_ALLOWED = {TYPE_PROCESS, TYPE_INFO}
35+
36+
# map of topic -> additional description
37+
SUBINDICES_BY_TOPIC = {
38+
"packaging": """\
39+
The canonical, up-to-date packaging specifications can be found on the
40+
`Python Packaging Authority`_ (PyPA) `specifications`_ page.
41+
Packaging PEPs follow the `PyPA specification update process`_.
42+
They are used to propose major additions or changes to the PyPA specifications.
43+
44+
.. _Python Packaging Authority: https://www.pypa.io/
45+
.. _specifications: https://packaging.python.org/en/latest/specifications/
46+
.. _PyPA specification update process: https://www.pypa.io/en/latest/specifications/#specification-update-process
47+
""",
48+
}

pep_sphinx_extensions/pep_zero_generator/parser.py

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

33
from __future__ import annotations
44

5+
import csv
56
from email.parser import HeaderParser
67
from pathlib import Path
78
import re
@@ -22,6 +23,14 @@
2223
from pep_sphinx_extensions.pep_zero_generator.author import Author
2324

2425

26+
# AUTHOR_OVERRIDES.csv is an exception file for PEP 0 name parsing
27+
AUTHOR_OVERRIDES: dict[str, dict[str, str]] = {}
28+
with open("AUTHOR_OVERRIDES.csv", "r", encoding="utf-8") as f:
29+
for line in csv.DictReader(f):
30+
full_name = line.pop("Overridden Name")
31+
AUTHOR_OVERRIDES[full_name] = line
32+
33+
2534
class PEP:
2635
"""Representation of PEPs.
2736
@@ -37,7 +46,7 @@ class PEP:
3746
# The required RFC 822 headers for all PEPs.
3847
required_headers = {"PEP", "Title", "Author", "Status", "Type", "Created"}
3948

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

9099
# Parse PEP authors
91-
self.authors: list[Author] = _parse_authors(self, metadata["Author"], authors_overrides)
100+
self.authors: list[Author] = _parse_authors(self, metadata["Author"], AUTHOR_OVERRIDES)
101+
102+
# Topic (for sub-indices)
103+
_topic = metadata.get("Topic", "").lower().split(",")
104+
self.topic: set[str] = {topic for topic_raw in _topic if (topic := topic_raw.strip())}
92105

93106
# Other headers
94107
self.created = metadata["Created"]
@@ -136,6 +149,7 @@ def full_details(self) -> dict[str, str]:
136149
"discussions_to": self.discussions_to,
137150
"status": self.status,
138151
"type": self.pep_type,
152+
"topic": ", ".join(sorted(self.topic)),
139153
"created": self.created,
140154
"python_version": self.python_version,
141155
"post_history": self.post_history,

pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py

+17-28
Original file line numberDiff line numberDiff line change
@@ -17,60 +17,49 @@
1717
"""
1818
from __future__ import annotations
1919

20-
import csv
2120
import json
2221
from pathlib import Path
23-
import re
2422
from typing import TYPE_CHECKING
2523

24+
from pep_sphinx_extensions.pep_zero_generator.constants import SUBINDICES_BY_TOPIC
2625
from pep_sphinx_extensions.pep_zero_generator import parser
26+
from pep_sphinx_extensions.pep_zero_generator import subindices
2727
from pep_sphinx_extensions.pep_zero_generator import writer
2828

2929
if TYPE_CHECKING:
3030
from sphinx.application import Sphinx
3131
from sphinx.environment import BuildEnvironment
3232

3333

34-
def create_pep_json(peps: list[parser.PEP]) -> str:
35-
return json.dumps({pep.number: pep.full_details for pep in peps}, indent=1)
36-
37-
38-
def create_pep_zero(app: Sphinx, env: BuildEnvironment, docnames: list[str]) -> None:
34+
def _parse_peps() -> list[parser.PEP]:
3935
# Read from root directory
4036
path = Path(".")
41-
42-
pep_zero_filename = "pep-0000"
4337
peps: list[parser.PEP] = []
44-
pep_pat = re.compile(r"pep-\d{4}") # Path.match() doesn't support regular expressions
45-
46-
# AUTHOR_OVERRIDES.csv is an exception file for PEP0 name parsing
47-
with open("AUTHOR_OVERRIDES.csv", "r", encoding="utf-8") as f:
48-
authors_overrides = {}
49-
for line in csv.DictReader(f):
50-
full_name = line.pop("Overridden Name")
51-
authors_overrides[full_name] = line
5238

5339
for file_path in path.iterdir():
5440
if not file_path.is_file():
5541
continue # Skip directories etc.
5642
if file_path.match("pep-0000*"):
5743
continue # Skip pre-existing PEP 0 files
58-
if pep_pat.match(str(file_path)) and file_path.suffix in {".txt", ".rst"}:
59-
pep = parser.PEP(path.joinpath(file_path).absolute(), authors_overrides)
44+
if file_path.match("pep-????.???") and file_path.suffix in {".txt", ".rst"}:
45+
pep = parser.PEP(path.joinpath(file_path).absolute())
6046
peps.append(pep)
6147

62-
peps = sorted(peps)
48+
return sorted(peps)
6349

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

68-
peps.append(parser.PEP(pep0_path, authors_overrides))
51+
def create_pep_json(peps: list[parser.PEP]) -> str:
52+
return json.dumps({pep.number: pep.full_details for pep in peps}, indent=1)
53+
54+
55+
def create_pep_zero(app: Sphinx, env: BuildEnvironment, docnames: list[str]) -> None:
56+
peps = _parse_peps()
57+
58+
pep0_text = writer.PEPZeroWriter().write_pep0(peps)
59+
pep0_path = subindices.update_sphinx("pep-0000", pep0_text, docnames, env)
60+
peps.append(parser.PEP(pep0_path))
6961

70-
# Add to files for builder
71-
docnames.insert(1, pep_zero_filename)
72-
# Add to files for writer
73-
env.found_docs.add(pep_zero_filename)
62+
subindices.generate_subindices(SUBINDICES_BY_TOPIC, peps, docnames, env)
7463

7564
# Create peps.json
7665
json_path = Path(app.outdir, "api", "peps.json").resolve()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""Utilities to support sub-indices for PEPs."""
2+
3+
from __future__ import annotations
4+
5+
from pathlib import Path
6+
from typing import TYPE_CHECKING
7+
8+
from pep_sphinx_extensions.pep_zero_generator import writer
9+
10+
if TYPE_CHECKING:
11+
from sphinx.environment import BuildEnvironment
12+
13+
from pep_sphinx_extensions.pep_zero_generator.parser import PEP
14+
15+
16+
def update_sphinx(filename: str, text: str, docnames: list[str], env: BuildEnvironment) -> Path:
17+
file_path = Path(f"{filename}.rst").resolve()
18+
file_path.parent.mkdir(parents=True, exist_ok=True)
19+
file_path.write_text(text, encoding="utf-8")
20+
21+
# Add to files for builder
22+
docnames.append(filename)
23+
# Add to files for writer
24+
env.found_docs.add(filename)
25+
26+
return file_path
27+
28+
29+
def generate_subindices(
30+
subindices: dict[str, str],
31+
peps: list[PEP],
32+
docnames: list[str],
33+
env: BuildEnvironment,
34+
) -> None:
35+
# Create sub index page
36+
generate_topic_contents(docnames, env)
37+
38+
for subindex, additional_description in subindices.items():
39+
header_text = f"{subindex.title()} PEPs"
40+
header_line = "#" * len(header_text)
41+
header = header_text + "\n" + header_line + "\n"
42+
43+
topic = subindex.lower()
44+
filtered_peps = [pep for pep in peps if topic in pep.topic]
45+
subindex_intro = f"""\
46+
This is the index of all Python Enhancement Proposals (PEPs) labelled
47+
under the '{subindex.title()}' topic. This is a sub-index of :pep:`0`,
48+
the PEP index.
49+
50+
{additional_description}
51+
"""
52+
subindex_text = writer.PEPZeroWriter().write_pep0(
53+
filtered_peps, header, subindex_intro, is_pep0=False,
54+
)
55+
update_sphinx(f"topic/{subindex}", subindex_text, docnames, env)
56+
57+
58+
def generate_topic_contents(docnames: list[str], env: BuildEnvironment):
59+
update_sphinx(f"topic/index", """\
60+
Topic Index
61+
***********
62+
63+
PEPs are indexed by topic on the pages below:
64+
65+
.. toctree::
66+
:maxdepth: 1
67+
:titlesonly:
68+
:glob:
69+
70+
*
71+
""", docnames, env)

pep_sphinx_extensions/pep_zero_generator/writer.py

+18-10
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
if TYPE_CHECKING:
2626
from pep_sphinx_extensions.pep_zero_generator.parser import PEP
2727

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

39-
intro = """\
39+
INTRO = """\
4040
This PEP contains the index of all Python Enhancement Proposals,
4141
known as PEPs. PEP numbers are :pep:`assigned <1#pep-editors>`
4242
by the PEP editors, and once assigned are never changed. The
4343
`version control history <https://github.com/python/peps>`_ of
44-
the PEP texts represent their historical record.
44+
the PEP texts represent their historical record. The PEPs are
45+
:doc:`indexed by topic <topic/index>` for specialist subjects.
4546
"""
4647

4748

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

115-
def write_pep0(self, peps: list[PEP]):
116+
def write_pep0(self, peps: list[PEP], header: str = HEADER, intro: str = INTRO, is_pep0: bool = True):
117+
if len(peps) == 0:
118+
return ""
116119

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

143149
self.emit_newline()
144150

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

153159
# Reserved PEP numbers
154-
self.emit_title("Reserved PEP Numbers")
155-
self.emit_column_headers()
156-
for number, claimants in sorted(self.RESERVED.items()):
157-
self.emit_pep_row(type="", status="", number=number, title="RESERVED", authors=claimants)
160+
if is_pep0:
161+
self.emit_title("Reserved PEP Numbers")
162+
self.emit_column_headers()
163+
for number, claimants in sorted(self.RESERVED.items()):
164+
self.emit_pep_row(type="", status="", number=number, title="RESERVED", authors=claimants)
158165

159-
self.emit_newline()
166+
167+
self.emit_newline()
160168

161169
# PEP types key
162170
self.emit_title("PEP Types Key")

pep_sphinx_extensions/tests/pep_zero_generator/test_parser.py

+10-10
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,27 @@
99

1010

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

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

1616

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

2121
assert pep8 < pep3333
2222

2323

2424
def test_pep_equal():
25-
pep_a = parser.PEP(Path("pep-0008.txt"), AUTHORS_OVERRIDES)
26-
pep_b = parser.PEP(Path("pep-0008.txt"), AUTHORS_OVERRIDES)
25+
pep_a = parser.PEP(Path("pep-0008.txt"))
26+
pep_b = parser.PEP(Path("pep-0008.txt"))
2727

2828
assert pep_a == pep_b
2929

3030

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

3434
assert pep8.details == {
3535
"authors": "GvR, Warsaw, Coghlan",
@@ -64,18 +64,18 @@ def test_pep_details():
6464
)
6565
def test_parse_authors(test_input, expected):
6666
# Arrange
67-
pep = parser.PEP(Path("pep-0160.txt"), AUTHORS_OVERRIDES)
67+
dummy_object = parser.PEP(Path("pep-0160.txt"))
6868

6969
# Act
70-
out = parser._parse_authors(pep, test_input, AUTHORS_OVERRIDES)
70+
out = parser._parse_authors(dummy_object, test_input, AUTHORS_OVERRIDES)
7171

7272
# Assert
7373
assert out == expected
7474

7575

7676
def test_parse_authors_invalid():
7777

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

8080
with pytest.raises(PEPError, match="no authors found"):
8181
parser._parse_authors(pep, "", AUTHORS_OVERRIDES)

pep_sphinx_extensions/tests/pep_zero_generator/test_pep_index_generator.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
from pathlib import Path
22

33
from pep_sphinx_extensions.pep_zero_generator import parser, pep_index_generator
4-
from pep_sphinx_extensions.tests.utils import AUTHORS_OVERRIDES
54

65

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

109
out = pep_index_generator.create_pep_json(peps)
1110

0 commit comments

Comments
 (0)