PEP 676: Implementation updates (#2208)

This commit is contained in:
Adam Turner 2022-01-09 18:07:03 +00:00 committed by GitHub
parent 19684a0787
commit 3d60b84e35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 215 additions and 180 deletions

View File

@ -14,26 +14,16 @@ jobs:
with: with:
fetch-depth: 0 # fetch all history so that last modified date-times are accurate fetch-depth: 0 # fetch all history so that last modified date-times are accurate
- name: 🐍 Set up Python 3.9 - name: 🐍 Set up Python 3
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.9 python-version: 3
cache: "pip"
- name: 🧳 Cache pip
uses: actions/cache@v2
with:
# This path is specific to Ubuntu
path: ~/.cache/pip
# Look to see if there is a cache hit for the corresponding requirements file
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: 👷‍ Install dependencies - name: 👷‍ Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -r requirements.txt python -m pip install -r requirements.txt
- name: 🔧 Build PEPs - name: 🔧 Build PEPs
run: make pages -j$(nproc) run: make pages -j$(nproc)

View File

@ -62,7 +62,7 @@ SPHINX_BUILD=$(PYTHON) build.py -j $(SPHINX_JOBS)
# TODO replace `rss:` with this when merged & tested # TODO replace `rss:` with this when merged & tested
pep_rss: pep_rss:
$(PYTHON) pep_rss_gen.py $(PYTHON) generate_rss.py
pages: pep_rss pages: pep_rss
$(SPHINX_BUILD) --index-file $(SPHINX_BUILD) --index-file

View File

@ -70,8 +70,6 @@ if __name__ == "__main__":
warningiserror=args.fail_on_warning, warningiserror=args.fail_on_warning,
parallel=args.jobs, parallel=args.jobs,
) )
app.builder.copysource = False # Prevent unneeded source copying - we link direct to GitHub
app.builder.search = False # Disable search
app.build() app.build()
if args.index_file: if args.index_file:

View File

@ -43,16 +43,14 @@ exclude_patterns = [
# HTML output settings # HTML output settings
html_math_renderer = "maths_to_html" # Maths rendering html_math_renderer = "maths_to_html" # Maths rendering
html_show_copyright = False # Turn off miscellany
html_show_sphinx = False
html_title = "peps.python.org" # Set <title/> html_title = "peps.python.org" # Set <title/>
# Theme settings # Theme settings
html_theme_path = ["pep_sphinx_extensions"] html_theme_path = ["pep_sphinx_extensions"]
html_theme = "pep_theme" # The actual theme directory (child of html_theme_path) html_theme = "pep_theme" # The actual theme directory (child of html_theme_path)
html_use_index = False # Disable index (we use PEP 0) html_use_index = False # Disable index (we use PEP 0)
html_sourcelink_suffix = "" # Fix links to GitHub (don't append .txt)
html_style = "" # must be defined here or in theme.conf, but is unused html_style = "" # must be defined here or in theme.conf, but is unused
html_permalinks = False # handled in the PEPContents transform html_permalinks = False # handled in the PEPContents transform
gettext_auto_build = False # speed-ups
templates_path = ['pep_sphinx_extensions/pep_theme/templates'] # Theme template relative paths from `confdir` templates_path = ['pep_sphinx_extensions/pep_theme/templates'] # Theme template relative paths from `confdir`

View File

@ -1,13 +1,11 @@
import datetime import datetime
import email.utils import email.utils
from pathlib import Path from pathlib import Path
import re
from dateutil import parser from docutils import frontend
import docutils.frontend from docutils import nodes
import docutils.nodes from docutils import utils
import docutils.parsers.rst from docutils.parsers import rst
import docutils.utils
from feedgen import entry from feedgen import entry
from feedgen import feed from feedgen import feed
@ -44,37 +42,26 @@ def first_line_starting_with(full_path: Path, text: str) -> str:
def pep_creation(full_path: Path) -> datetime.datetime: def pep_creation(full_path: Path) -> datetime.datetime:
created_str = first_line_starting_with(full_path, "Created:") created_str = first_line_starting_with(full_path, "Created:")
# bleh, I was hoping to avoid re but some PEPs editorialize on the Created line if full_path.stem == "pep-0102":
# (note as of Aug 2020 only PEP 102 has additional content on the Created line) # remove additional content on the Created line
m = re.search(r"(\d+[- ][\w\d]+[- ]\d{2,4})", created_str) created_str = created_str.split(" ", 1)[0]
if not m: return datetime.datetime.strptime(created_str, "%d-%b-%Y")
# some older ones have an empty line, that's okay, if it's old we ipso facto don't care about it.
# "return None" would make the most sense but datetime objects refuse to compare with that. :-|
return datetime.datetime(1900, 1, 1)
created_str = m.group(1)
try:
return parser.parse(created_str, dayfirst=True)
except (ValueError, OverflowError):
return datetime.datetime(1900, 1, 1)
def parse_rst(text: str) -> docutils.nodes.document: def parse_rst(text: str) -> nodes.document:
rst_parser = docutils.parsers.rst.Parser() settings = frontend.OptionParser((rst.Parser,)).get_default_values()
components = (docutils.parsers.rst.Parser,) document = utils.new_document('<rst-doc>', settings=settings)
settings = docutils.frontend.OptionParser(components=components).get_default_values() rst.Parser().parse(text, document)
document = docutils.utils.new_document('<rst-doc>', settings=settings)
rst_parser.parse(text, document)
return document return document
def pep_abstract(full_path: Path) -> str: def pep_abstract(full_path: Path) -> str:
"""Return the first paragraph of the PEP abstract""" """Return the first paragraph of the PEP abstract"""
text = full_path.read_text(encoding="utf-8") text = full_path.read_text(encoding="utf-8")
for node in parse_rst(text): # TODO replace .traverse with .findall when Sphinx updates to docutils>=0.18.1
if "<title>Abstract</title>" in str(node): for node in parse_rst(text).traverse(nodes.section):
for child in node: if node.next_node(nodes.title).astext() == "Abstract":
if child.tagname == "paragraph": return node.next_node(nodes.paragraph).astext().strip().replace("\n", " ")
return child.astext().strip().replace("\n", " ")
return "" return ""
@ -119,7 +106,7 @@ def main():
Newest Python Enhancement Proposals (PEPs) - Information on new Newest Python Enhancement Proposals (PEPs) - Information on new
language features, and some meta-information like release language features, and some meta-information like release
procedure and schedules. procedure and schedules.
""".replace("\n ", " ").strip() """
# Setup feed generator # Setup feed generator
fg = feed.FeedGenerator() fg = feed.FeedGenerator()
@ -131,7 +118,7 @@ def main():
fg.title("Newest Python PEPs") fg.title("Newest Python PEPs")
fg.link(href="https://www.python.org/dev/peps") fg.link(href="https://www.python.org/dev/peps")
fg.link(href="https://www.python.org/dev/peps/peps.rss", rel="self") fg.link(href="https://www.python.org/dev/peps/peps.rss", rel="self")
fg.description(desc) fg.description(" ".join(desc.split()))
fg.lastBuildDate(datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc)) fg.lastBuildDate(datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc))
# Add PEP information (ordered by newest first) # Add PEP information (ordered by newest first)

View File

@ -529,7 +529,7 @@ not affect any application code.
The benchmarks were conducted on CPython default branch revision b08868fd5994 The benchmarks were conducted on CPython default branch revision b08868fd5994
and the PEP repository [pep-456-repos]_. All upstream changes were merged and the PEP repository [pep-456-repos]_. All upstream changes were merged
into the pep-456 branch. The "performance" CPU governor was configured and into the ``pep-456`` branch. The "performance" CPU governor was configured and
almost all programs were stopped so the benchmarks were able to utilize almost all programs were stopped so the benchmarks were able to utilize
TurboBoost and the CPU caches as much as possible. The raw benchmark results TurboBoost and the CPU caches as much as possible. The raw benchmark results
of multiple machines and platforms are made available at [benchmarks]_. of multiple machines and platforms are made available at [benchmarks]_.

View File

@ -4,11 +4,12 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from docutils import nodes
from docutils.parsers.rst import states
from docutils.writers.html5_polyglot import HTMLTranslator from docutils.writers.html5_polyglot import HTMLTranslator
from sphinx.environment import BuildEnvironment from sphinx import environment
from sphinx.environment import default_settings
from pep_sphinx_extensions import config from pep_sphinx_extensions.pep_processor.html import pep_html_builder
from pep_sphinx_extensions.pep_processor.html import pep_html_translator 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_parser
from pep_sphinx_extensions.pep_processor.parsing import pep_role from pep_sphinx_extensions.pep_processor.parsing import pep_role
@ -20,17 +21,16 @@ if TYPE_CHECKING:
# Monkeypatch sphinx.environment.default_settings as Sphinx doesn't allow custom settings or Readers # Monkeypatch sphinx.environment.default_settings as Sphinx doesn't allow custom settings or Readers
# These settings should go in docutils.conf, but are overridden here for now so as not to affect # These settings should go in docutils.conf, but are overridden here for now so as not to affect
# pep2html.py # pep2html.py
default_settings |= { environment.default_settings |= {
"pep_references": True, "pep_references": True,
"rfc_references": True, "rfc_references": True,
"pep_base_url": "", "pep_base_url": "",
"pep_file_url_template": "pep-%04d.html", "pep_file_url_template": "",
"_disable_config": True, # disable using docutils.conf whilst running both PEP generators "_disable_config": True, # disable using docutils.conf whilst running both PEP generators
} }
# Monkeypatch sphinx.environment.BuildEnvironment.collect_relations, as it takes a long time # TODO replace all inlined PEP and RFC strings with marked-up roles, disable pep_references and rfc_references and remove this monkey-patch
# and we don't use the parent/next/prev functionality states.Inliner.pep_reference = lambda s, m, _l: [nodes.reference("", m.group(0), refuri=s.document.settings.pep_url.format(int(m.group("pepnum2"))))]
BuildEnvironment.collect_relations = lambda self: {}
def _depart_maths(): def _depart_maths():
@ -39,14 +39,17 @@ def _depart_maths():
def _update_config_for_builder(app: Sphinx): def _update_config_for_builder(app: Sphinx):
if app.builder.name == "dirhtml": if app.builder.name == "dirhtml":
config.pep_url = f"../{config.pep_stem}" environment.default_settings["pep_url"] = "../pep-{:0>4}"
app.env.settings["pep_file_url_template"] = "../pep-%04d"
def setup(app: Sphinx) -> dict[str, bool]: def setup(app: Sphinx) -> dict[str, bool]:
"""Initialize Sphinx extension.""" """Initialize Sphinx extension."""
environment.default_settings["pep_url"] = "pep-{:0>4}.html"
# Register plugin logic # Register plugin logic
app.add_builder(pep_html_builder.FileBuilder, override=True)
app.add_builder(pep_html_builder.DirectoryBuilder, override=True)
app.add_source_parser(pep_parser.PEPParser) # Add PEP transforms 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.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 (html builder) app.set_translator("html", pep_html_translator.PEPTranslator) # Docutils Node Visitor overrides (html builder)

View File

@ -1,6 +0,0 @@
"""Miscellaneous configuration variables for the PEP Sphinx extensions."""
pep_stem = "pep-{:0>4}"
pep_url = f"{pep_stem}.html"
pep_vcs_url = "https://github.com/python/peps/blob/main/"
pep_commits_url = "https://github.com/python/peps/commits/main/"

View File

@ -0,0 +1,50 @@
from pathlib import Path
from docutils import nodes
from docutils.frontend import OptionParser
from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.writers.html import HTMLWriter
from sphinx.builders.dirhtml import DirectoryHTMLBuilder
class FileBuilder(StandaloneHTMLBuilder):
copysource = False # Prevent unneeded source copying - we link direct to GitHub
search = False # Disable search
# Things we don't use but that need to exist:
indexer = None
relations = {}
_script_files = _css_files = []
def prepare_writing(self, _doc_names: set[str]) -> None:
self.docwriter = HTMLWriter(self)
_opt_parser = OptionParser([self.docwriter], defaults=self.env.settings, read_config_files=True)
self.docsettings = _opt_parser.get_default_values()
self.globalcontext = {"docstitle": self.config.html_title, "script_files": [], "css_files": []}
def get_doc_context(self, docname: str, body: str, _metatags: str) -> dict:
"""Collect items for the template context of a page."""
try:
title = self.env.longtitles[docname].astext()
except KeyError:
title = ""
# source filename
file_is_rst = Path(self.env.srcdir, docname + ".rst").exists()
source_name = f"{docname}.rst" if file_is_rst else f"{docname}.txt"
# local table of contents
toc_tree = self.env.tocs[docname].deepcopy()
for node in toc_tree.traverse(nodes.reference):
node["refuri"] = node["anchorname"] or '#' # fix targets
toc = self.render_partial(toc_tree)["fragment"]
return {"title": title, "sourcename": source_name, "toc": toc, "body": body}
class DirectoryBuilder(FileBuilder):
# sync all overwritten things from DirectoryHTMLBuilder
name = DirectoryHTMLBuilder.name
get_target_uri = DirectoryHTMLBuilder.get_target_uri
get_outfilename = DirectoryHTMLBuilder.get_outfilename

View File

@ -57,26 +57,34 @@ class PEPTranslator(html5.HTML5Translator):
"""Add corresponding end tag from `visit_paragraph`.""" """Add corresponding end tag from `visit_paragraph`."""
self.body.append(self.context.pop()) self.body.append(self.context.pop())
def visit_footnote_reference(self, node):
self.body.append(self.starttag(node, "a", suffix="[",
CLASS=f"footnote-reference {self.settings.footnote_references}",
href=f"#{node['refid']}"
))
def depart_footnote_reference(self, node):
self.body.append(']</a>')
def visit_label(self, node):
# pass parent node to get id into starttag:
self.body.append(self.starttag(node.parent, "dt", suffix="[", CLASS="label"))
# footnote/citation backrefs:
back_refs = node.parent["backrefs"]
if self.settings.footnote_backlinks and len(back_refs) == 1:
self.body.append(f'<a href="#{back_refs[0]}">')
self.context.append(f"</a>]")
else:
self.context.append("]")
def depart_label(self, node) -> None: def depart_label(self, node) -> None:
"""PEP link/citation block cleanup with italicised backlinks.""" """PEP link/citation block cleanup with italicised backlinks."""
if not self.settings.footnote_backlinks: self.body.append(self.context.pop())
self.body.append("</span>") back_refs = node.parent["backrefs"]
self.body.append("</dt>\n<dd>") if self.settings.footnote_backlinks and len(back_refs) > 1:
return back_links = ", ".join(f"<a href='#{ref}'>{i}</a>" for i, ref in enumerate(back_refs, start=1))
self.body.append(f"<em> ({back_links}) </em>")
# If only one reference to this footnote
back_references = node.parent["backrefs"]
if len(back_references) == 1:
self.body.append("</a>")
# Close the tag
self.body.append("</span>")
# If more than one reference
if len(back_references) > 1:
back_links = [f"<a href='#{ref}'>{i}</a>" for i, ref in enumerate(back_references, start=1)]
back_links_str = ", ".join(back_links)
self.body.append(f"<span class='fn-backref''><em> ({back_links_str}) </em></span>")
# Close the def tags # Close the def tags
self.body.append("</dt>\n<dd>") self.body.append("</dt>\n<dd>")

View File

@ -1,15 +1,14 @@
from sphinx import roles from sphinx import roles
from pep_sphinx_extensions import config
class PEPRole(roles.PEP): class PEPRole(roles.PEP):
"""Override the :pep: role""" """Override the :pep: role"""
# TODO override the entire thing (internal should be True)
def build_uri(self) -> str: def build_uri(self) -> str:
"""Get PEP URI from role text.""" """Get PEP URI from role text."""
pep_str, _, fragment = self.target.partition("#") pep_str, _, fragment = self.target.partition("#")
pep_base = config.pep_url.format(int(pep_str)) pep_base = self.inliner.document.settings.pep_url.format(int(pep_str))
if fragment: if fragment:
return f"{pep_base}#{fragment}" return f"{pep_base}#{fragment}"
return pep_base return pep_base

View File

@ -5,8 +5,6 @@ import subprocess
from docutils import nodes from docutils import nodes
from docutils import transforms from docutils import transforms
from pep_sphinx_extensions import config
class PEPFooter(transforms.Transform): class PEPFooter(transforms.Transform):
"""Footer transforms for PEPs. """Footer transforms for PEPs.
@ -49,21 +47,49 @@ class PEPFooter(transforms.Transform):
def _add_source_link(pep_source_path: Path) -> nodes.paragraph: def _add_source_link(pep_source_path: Path) -> nodes.paragraph:
"""Add link to source text on VCS (GitHub)""" """Add link to source text on VCS (GitHub)"""
source_link = config.pep_vcs_url + pep_source_path.name source_link = f"https://github.com/python/peps/blob/main/{pep_source_path.name}"
link_node = nodes.reference("", source_link, refuri=source_link) link_node = nodes.reference("", source_link, refuri=source_link)
return nodes.paragraph("", "Source: ", link_node) return nodes.paragraph("", "Source: ", link_node)
def _add_commit_history_info(pep_source_path: Path) -> nodes.paragraph: def _add_commit_history_info(pep_source_path: Path) -> nodes.paragraph:
"""Use local git history to find last modified date.""" """Use local git history to find last modified date."""
args = ["git", "--no-pager", "log", "-1", "--format=%at", pep_source_path.name]
try: try:
file_modified = subprocess.check_output(args) since_epoch = LAST_MODIFIED_TIMES[pep_source_path.name]
since_epoch = file_modified.decode("utf-8").strip() except KeyError:
dt = datetime.datetime.utcfromtimestamp(float(since_epoch))
except (subprocess.CalledProcessError, ValueError):
return nodes.paragraph() return nodes.paragraph()
commit_link = config.pep_commits_url + pep_source_path.name iso_time = datetime.datetime.utcfromtimestamp(since_epoch).isoformat(sep=" ")
link_node = nodes.reference("", f"{dt.isoformat(sep=' ')} GMT", refuri=commit_link) commit_link = f"https://github.com/python/peps/commits/main/{pep_source_path.name}"
link_node = nodes.reference("", f"{iso_time} GMT", refuri=commit_link)
return nodes.paragraph("", "Last modified: ", link_node) return nodes.paragraph("", "Last modified: ", link_node)
def _get_last_modified_timestamps():
# get timestamps and changed files from all commits (without paging results)
args = ["git", "--no-pager", "log", "--format=#%at", "--name-only"]
with subprocess.Popen(args, stdout=subprocess.PIPE) as process:
all_modified = process.stdout.read().decode("utf-8")
process.stdout.close()
if process.wait(): # non-zero return code
return {}
# set up the dictionary with the *current* files
last_modified = {path.name: 0 for path in Path().glob("pep-*") if path.suffix in {".txt", ".rst"}}
# iterate through newest to oldest, updating per file timestamps
change_sets = all_modified.removeprefix("#").split("#")
for change_set in change_sets:
timestamp, files = change_set.split("\n", 1)
for file in files.strip().split("\n"):
if file.startswith("pep-") and file[-3:] in {"txt", "rst"}:
if last_modified.get(file) == 0:
try:
last_modified[file] = float(timestamp)
except ValueError:
pass # if float conversion fails
return last_modified
LAST_MODIFIED_TIMES = _get_last_modified_timestamps()

View File

@ -1,5 +1,3 @@
from __future__ import annotations
from pathlib import Path from pathlib import Path
import re import re
@ -7,8 +5,8 @@ from docutils import nodes
from docutils import transforms from docutils import transforms
from sphinx import errors from sphinx import errors
from pep_sphinx_extensions import config
from pep_sphinx_extensions.pep_processor.transforms import pep_zero from pep_sphinx_extensions.pep_processor.transforms import pep_zero
from pep_sphinx_extensions.pep_processor.transforms.pep_zero import _mask_email
class PEPParsingError(errors.SphinxError): class PEPParsingError(errors.SphinxError):
@ -79,9 +77,9 @@ class PEPHeaders(transforms.Transform):
elif name in {"replaces", "superseded-by", "requires"}: elif name in {"replaces", "superseded-by", "requires"}:
# replace PEP numbers with normalised list of links to PEPs # replace PEP numbers with normalised list of links to PEPs
new_body = [] new_body = []
for ref_pep in re.split(r",?\s+", body.astext()): for pep_str in re.split(r",?\s+", body.astext()):
new_body += [nodes.reference("", ref_pep, refuri=config.pep_url.format(int(ref_pep)))] target = self.document.settings.pep_url.format(int(pep_str))
new_body += [nodes.Text(", ")] new_body += [nodes.reference("", pep_str, refuri=target), nodes.Text(", ")]
para[:] = new_body[:-1] # drop trailing space para[:] = new_body[:-1] # drop trailing space
elif name in {"last-modified", "content-type", "version"}: elif name in {"last-modified", "content-type", "version"}:
# Mark unneeded fields # Mark unneeded fields
@ -90,25 +88,3 @@ class PEPHeaders(transforms.Transform):
# Remove unneeded fields # Remove unneeded fields
for field in fields_to_remove: for field in fields_to_remove:
field.parent.remove(field) field.parent.remove(field)
def _mask_email(ref: nodes.reference, pep_num: int | None = None) -> nodes.reference:
"""Mask the email address in `ref` and return a replacement node.
`ref` is returned unchanged if it contains no email address.
If given an email not explicitly whitelisted, process it such that
`user@host` -> `user at host`.
If given a PEP number `pep_num`, add a default email subject.
"""
if "refuri" not in ref or not ref["refuri"].startswith("mailto:"):
return ref
non_masked_addresses = {"peps@python.org", "python-list@python.org", "python-dev@python.org"}
if ref["refuri"].removeprefix("mailto:").strip() not in non_masked_addresses:
ref[0] = nodes.raw("", ref[0].replace("@", "&#32;&#97;t&#32;"), format="html")
if pep_num is None:
return ref[0] # return email text without mailto link
ref["refuri"] += f"?subject=PEP%20{pep_num}"
return ref

View File

@ -1,8 +1,7 @@
from __future__ import annotations
from docutils import nodes from docutils import nodes
from docutils import transforms from docutils import transforms
from docutils.transforms import peps
from pep_sphinx_extensions import config
class PEPZero(transforms.Transform): class PEPZero(transforms.Transform):
@ -38,7 +37,7 @@ class PEPZeroSpecial(nodes.SparseNodeVisitor):
@staticmethod @staticmethod
def visit_reference(node: nodes.reference) -> None: def visit_reference(node: nodes.reference) -> None:
"""Mask email addresses if present.""" """Mask email addresses if present."""
node.replace_self(peps.mask_email(node)) node.replace_self(_mask_email(node))
@staticmethod @staticmethod
def visit_field_list(node: nodes.field_list) -> None: def visit_field_list(node: nodes.field_list) -> None:
@ -68,7 +67,30 @@ class PEPZeroSpecial(nodes.SparseNodeVisitor):
if isinstance(para, nodes.paragraph) and len(para) == 1: if isinstance(para, nodes.paragraph) and len(para) == 1:
pep_str = para.astext() pep_str = para.astext()
try: try:
ref = config.pep_url.format(int(pep_str)) pep_num = int(pep_str)
para[0] = nodes.reference(pep_str, pep_str, refuri=ref)
except ValueError: except ValueError:
pass return
ref = self.document.settings.pep_url.format(pep_num)
para[0] = nodes.reference("", pep_str, refuri=ref)
def _mask_email(ref: nodes.reference, pep_num: int | None = None) -> nodes.reference:
"""Mask the email address in `ref` and return a replacement node.
`ref` is returned unchanged if it contains no email address.
If given an email not explicitly whitelisted, process it such that
`user@host` -> `user at host`.
If given a PEP number `pep_num`, add a default email subject.
"""
if "refuri" not in ref or not ref["refuri"].startswith("mailto:"):
return ref
non_masked_addresses = {"peps@python.org", "python-list@python.org", "python-dev@python.org"}
if ref["refuri"].removeprefix("mailto:").strip() not in non_masked_addresses:
ref[0] = nodes.raw("", ref[0].replace("@", "&#32;&#97;t&#32;"), format="html")
if pep_num is None:
return ref[0] # return email text without mailto link
ref["refuri"] += f"?subject=PEP%20{pep_num}"
return ref

View File

@ -1,5 +0,0 @@
/* JavaScript utilities for all documentation. */
// Footnote fixer
document.querySelectorAll("span.brackets").forEach(el => el.innerHTML = "[" + el.innerHTML + "]")
document.querySelectorAll("a.brackets").forEach(el => el.innerHTML = "[" + el.innerHTML + "]")

View File

@ -290,3 +290,6 @@ nav#pep-sidebar ul {
nav#pep-sidebar ul a { nav#pep-sidebar ul a {
text-decoration: none; text-decoration: none;
} }
#source {
padding-bottom: 2rem;
}

View File

@ -29,9 +29,8 @@
<h2>Contents</h2> <h2>Contents</h2>
{{ toc }} {{ toc }}
<br /> <br />
<strong><a href="https://github.com/python/peps/blob/main/{{sourcename}}">Page Source (GitHub)</a></strong> <strong id="source"><a href="https://github.com/python/peps/blob/main/{{sourcename}}">Page Source (GitHub)</a></strong>
</nav> </nav>
</section> </section>
<script src="{{ pathto('_static/doctools.js', resource=True) }}"></script>
</body> </body>
</html> </html>

View File

@ -49,16 +49,12 @@ Created: 13-Jul-2000
intro = """\ intro = """\
This PEP contains the index of all Python Enhancement Proposals, This PEP contains the index of all Python Enhancement Proposals,
known as PEPs. PEP numbers are assigned by the PEP editors, and known as PEPs. PEP numbers are :pep:`assigned <1#pep-editors>`
once assigned are never changed [1_]. The version control history [2_] of 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.
""" """
references = """\
.. [1] PEP 1: PEP Purpose and Guidelines
.. [2] View PEP history online: https://github.com/python/peps
"""
class PEPZeroWriter: class PEPZeroWriter:
# This is a list of reserved PEP numbers. Reservations are not to be used for # This is a list of reserved PEP numbers. Reservations are not to be used for
@ -100,17 +96,16 @@ class PEPZeroWriter:
self.emit_pep_row({"status": ".", "type": ".", "number": "PEP", "title": "PEP Title", "authors": "PEP Author(s)"}) self.emit_pep_row({"status": ".", "type": ".", "number": "PEP", "title": "PEP Title", "authors": "PEP Author(s)"})
self.emit_table_separator() self.emit_table_separator()
def emit_title(self, text: str, anchor: str, *, symbol: str = "=") -> None: def emit_title(self, text: str, *, symbol: str = "=") -> None:
self.output.append(f".. _{anchor}:\n")
self.output.append(text) self.output.append(text)
self.output.append(symbol * len(text)) self.output.append(symbol * len(text))
self.emit_newline() self.emit_newline()
def emit_subtitle(self, text: str, anchor: str) -> None: def emit_subtitle(self, text: str) -> None:
self.emit_title(text, anchor, symbol="-") self.emit_title(text, symbol="-")
def emit_pep_category(self, category: str, anchor: str, peps: list[PEP]) -> None: def emit_pep_category(self, category: str, peps: list[PEP]) -> None:
self.emit_subtitle(category, anchor) self.emit_subtitle(category)
self.emit_column_headers() self.emit_column_headers()
for pep in peps: for pep in peps:
self.output.append(column_format(**pep.details(title_length=title_length))) self.output.append(column_format(**pep.details(title_length=title_length)))
@ -124,44 +119,40 @@ class PEPZeroWriter:
self.emit_newline() self.emit_newline()
# Introduction # Introduction
self.emit_title("Introduction", "intro") self.emit_title("Introduction")
self.emit_text(intro) self.emit_text(intro)
self.emit_newline() self.emit_newline()
# PEPs by category # PEPs by category
self.emit_title("Index by Category", "by-category") self.emit_title("Index by Category")
meta, info, provisional, accepted, open_, finished, historical, deferred, dead = _classify_peps(peps) meta, info, provisional, accepted, open_, finished, historical, deferred, dead = _classify_peps(peps)
pep_categories = [ pep_categories = [
("Meta-PEPs (PEPs about PEPs or Processes)", "by-category-meta", meta), ("Meta-PEPs (PEPs about PEPs or Processes)", meta),
("Other Informational PEPs", "by-category-other-info", info), ("Other Informational PEPs", info),
("Provisional PEPs (provisionally accepted; interface may still change)", "by-category-provisional", provisional), ("Provisional PEPs (provisionally accepted; interface may still change)", provisional),
("Accepted PEPs (accepted; may not be implemented yet)", "by-category-accepted", accepted), ("Accepted PEPs (accepted; may not be implemented yet)", accepted),
("Open PEPs (under consideration)", "by-category-open", open_), ("Open PEPs (under consideration)", open_),
("Finished PEPs (done, with a stable interface)", "by-category-finished", finished), ("Finished PEPs (done, with a stable interface)", finished),
("Historical Meta-PEPs and Informational PEPs", "by-category-historical", historical), ("Historical Meta-PEPs and Informational PEPs", historical),
("Deferred PEPs (postponed pending further research or updates)", "by-category-deferred", deferred), ("Deferred PEPs (postponed pending further research or updates)", deferred),
("Abandoned, Withdrawn, and Rejected PEPs", "by-category-abandoned", dead), ("Abandoned, Withdrawn, and Rejected PEPs", dead),
] ]
for (category, anchor, peps_in_category) in pep_categories: for (category, peps_in_category) in pep_categories:
self.emit_pep_category(category, anchor, peps_in_category) self.emit_pep_category(category, peps_in_category)
self.emit_newline() self.emit_newline()
# PEPs by number # PEPs by number
self.emit_title("Numerical Index", "by-pep-number") self.emit_title("Numerical Index")
self.emit_column_headers() self.emit_column_headers()
prev_pep = 0
for pep in peps: for pep in peps:
if pep.number - prev_pep > 1:
self.emit_newline()
self.emit_pep_row(pep.details(title_length=title_length)) self.emit_pep_row(pep.details(title_length=title_length))
prev_pep = pep.number
self.emit_table_separator() self.emit_table_separator()
self.emit_newline() self.emit_newline()
# Reserved PEP numbers # Reserved PEP numbers
self.emit_title("Reserved PEP Numbers", "reserved") self.emit_title("Reserved PEP Numbers")
self.emit_column_headers() self.emit_column_headers()
for number, claimants in sorted(self.RESERVED.items()): for number, claimants in sorted(self.RESERVED.items()):
self.emit_pep_row({"type": ".", "status": ".", "number": number, "title": "RESERVED", "authors": claimants}) self.emit_pep_row({"type": ".", "status": ".", "number": number, "title": "RESERVED", "authors": claimants})
@ -170,7 +161,7 @@ class PEPZeroWriter:
self.emit_newline() self.emit_newline()
# PEP types key # PEP types key
self.emit_title("PEP Types Key", "type-key") self.emit_title("PEP Types Key")
for type_ in sorted(TYPE_VALUES): for type_ in sorted(TYPE_VALUES):
self.emit_text(f" {type_[0]} - {type_} PEP") self.emit_text(f" {type_[0]} - {type_} PEP")
self.emit_newline() self.emit_newline()
@ -178,7 +169,7 @@ class PEPZeroWriter:
self.emit_newline() self.emit_newline()
# PEP status key # PEP status key
self.emit_title("PEP Status Key", "status-key") self.emit_title("PEP Status Key")
for status in sorted(STATUS_VALUES): for status in sorted(STATUS_VALUES):
# Draft PEPs have no status displayed, Active shares a key with Accepted # Draft PEPs have no status displayed, Active shares a key with Accepted
if status in HIDE_STATUS: if status in HIDE_STATUS:
@ -195,7 +186,7 @@ class PEPZeroWriter:
# PEP owners # PEP owners
authors_dict = _verify_email_addresses(peps) authors_dict = _verify_email_addresses(peps)
max_name_len = max(len(author_name) for author_name in authors_dict) max_name_len = max(len(author_name) for author_name in authors_dict)
self.emit_title("Authors/Owners", "authors") self.emit_title("Authors/Owners")
self.emit_author_table_separator(max_name_len) self.emit_author_table_separator(max_name_len)
self.emit_text(f"{'Name':{max_name_len}} Email Address") self.emit_text(f"{'Name':{max_name_len}} Email Address")
self.emit_author_table_separator(max_name_len) self.emit_author_table_separator(max_name_len)
@ -207,10 +198,6 @@ class PEPZeroWriter:
self.emit_newline() self.emit_newline()
self.emit_newline() self.emit_newline()
# References for introduction footnotes
self.emit_title("References", "references")
self.emit_text(references)
pep0_string = "\n".join([str(s) for s in self.output]) pep0_string = "\n".join([str(s) for s in self.output])
return pep0_string return pep0_string