Skip to content

Commit 0d93abf

Browse files
authored
Sphinx support: theming (python#1933)
See #2, python#1385 for context. Superseeds python#1568. This is the Sphinx-theming part, building on PR python#1930. ### Stylesheets: - `style.css` - styles - `mq.css` - media queries ### Jinja2 Templates: - `page.html` - overarching template ### Javascript: - `doctools.js` - fixes footnote brackets ### Theme miscellany - `theme.conf` - sets pygments styles, theme internals
1 parent 7d72700 commit 0d93abf

19 files changed

+591
-129
lines changed

.github/workflows/deploy-gh-pages.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ jobs:
3636
- name: 🔧 Build PEPs
3737
run: make pages -j$(nproc)
3838

39+
# remove the .doctrees folder when building for deployment as it takes two thirds of disk space
40+
- name: 🔥 Clean up files
41+
run: rm -r build/.doctrees/
42+
3943
- name: 🚀 Deploy to GitHub pages
4044
uses: JamesIves/[email protected]
4145
with:

build.py

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

33
import argparse
44
from pathlib import Path
5-
import shutil
65

76
from sphinx.application import Sphinx
87

@@ -25,11 +24,16 @@ def create_parser():
2524
return parser.parse_args()
2625

2726

28-
def create_index_file(html_root: Path):
27+
def create_index_file(html_root: Path, builder: str) -> None:
2928
"""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")
29+
pep_zero_file = "pep-0000.html" if builder == "html" else "pep-0000/index.html"
30+
try:
31+
pep_zero_text = html_root.joinpath(pep_zero_file).read_text(encoding="utf-8")
32+
except FileNotFoundError:
33+
return None
34+
if builder == "dirhtml":
35+
pep_zero_text = pep_zero_text.replace('="../', '="') # remove relative directory links
36+
html_root.joinpath("index.html").write_text(pep_zero_text, encoding="utf-8")
3337

3438

3539
if __name__ == "__main__":
@@ -67,7 +71,8 @@ def create_index_file(html_root: Path):
6771
parallel=args.jobs,
6872
)
6973
app.builder.copysource = False # Prevent unneeded source copying - we link direct to GitHub
74+
app.builder.search = False # Disable search
7075
app.build()
7176

7277
if args.index_file:
73-
create_index_file(build_directory)
78+
create_index_file(build_directory, sphinx_builder)

conf.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Configuration for building PEPs using Sphinx."""
22

3-
import sys
43
from pathlib import Path
4+
import sys
55

66
sys.path.append(str(Path("pep_sphinx_extensions").absolute()))
77

@@ -44,3 +44,13 @@
4444
html_show_copyright = False # Turn off miscellany
4545
html_show_sphinx = False
4646
html_title = "peps.python.org" # Set <title/>
47+
48+
# Theme settings
49+
html_theme_path = ["pep_sphinx_extensions"]
50+
html_theme = "pep_theme" # The actual theme directory (child of html_theme_path)
51+
html_use_index = False # Disable index (we use PEP 0)
52+
html_sourcelink_suffix = "" # Fix links to GitHub (don't append .txt)
53+
html_style = "" # must be defined here or in theme.conf, but is unused
54+
html_permalinks = False # handled in the PEPContents transform
55+
56+
templates_path = ['pep_sphinx_extensions/pep_theme/templates'] # Theme template relative paths from `confdir`

pep-0310.txt

+3-6
Original file line numberDiff line numberDiff line change
@@ -238,12 +238,9 @@ could be mentioned here.
238238
https://mail.python.org/pipermail/python-dev/2003-August/037795.html
239239

240240
.. [3] Thread on python-dev with subject
241-
242-
.. [Python-Dev] pre-PEP: Resource-Release Support for Generators
243-
244-
starting at
245-
246-
https://mail.python.org/pipermail/python-dev/2003-August/037803.html
241+
`[Python-Dev] pre-PEP: Resource-Release Support for Generators`
242+
starting at
243+
https://mail.python.org/pipermail/python-dev/2003-August/037803.html
247244

248245
Copyright
249246
=========

pep-0439.txt

+2-2
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,11 @@ The "pip3" command will support two new command-line options that are used
147147
in the boostrapping, and otherwise ignored. They control where the pip
148148
implementation is installed:
149149

150-
--bootstrap
150+
``--bootstrap``
151151
Install to the user's packages directory. The name of this option is chosen
152152
to promote it as the preferred installation option.
153153

154-
--bootstrap-to-system
154+
``--bootstrap-to-system``
155155
Install to the system site-packages directory.
156156

157157
These command-line options will also need to be implemented, but otherwise

pep-3143.txt

-18
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,6 @@ any daemon regardless of what else the program may need to do.
2222
This PEP introduces a package to the Python standard library that
2323
provides a simple interface to the task of becoming a daemon process.
2424

25-
26-
.. contents::
27-
..
28-
Table of Contents:
29-
Abstract
30-
Specification
31-
Example usage
32-
Interface
33-
``DaemonContext`` objects
34-
Motivation
35-
Rationale
36-
Correct daemon behaviour
37-
A daemon is not a service
38-
Reference Implementation
39-
Other daemon implementations
40-
References
41-
Copyright
42-
4325
============
4426
PEP Deferral
4527
============

pep_sphinx_extensions/__init__.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
from typing import TYPE_CHECKING
66

77
from docutils.writers.html5_polyglot import HTMLTranslator
8+
from sphinx.environment import BuildEnvironment
89
from sphinx.environment import default_settings
910

11+
from pep_sphinx_extensions import config
1012
from pep_sphinx_extensions.pep_processor.html import pep_html_translator
1113
from pep_sphinx_extensions.pep_processor.parsing import pep_parser
1214
from pep_sphinx_extensions.pep_processor.parsing import pep_role
@@ -26,19 +28,31 @@
2628
"_disable_config": True, # disable using docutils.conf whilst running both PEP generators
2729
}
2830

31+
# Monkeypatch sphinx.environment.BuildEnvironment.collect_relations, as it takes a long time
32+
# and we don't use the parent/next/prev functionality
33+
BuildEnvironment.collect_relations = lambda self: {}
34+
2935

3036
def _depart_maths():
3137
pass # No-op callable for the type checker
3238

3339

40+
def _update_config_for_builder(app: Sphinx):
41+
if app.builder.name == "dirhtml":
42+
config.pep_url = f"../{config.pep_stem}"
43+
app.env.settings["pep_file_url_template"] = "../pep-%04d"
44+
45+
3446
def setup(app: Sphinx) -> dict[str, bool]:
3547
"""Initialize Sphinx extension."""
3648

3749
# Register plugin logic
3850
app.add_source_parser(pep_parser.PEPParser) # Add PEP transforms
3951
app.add_role("pep", pep_role.PEPRole(), override=True) # Transform PEP references to links
40-
app.set_translator("html", pep_html_translator.PEPTranslator) # Docutils Node Visitor overrides
52+
app.set_translator("html", pep_html_translator.PEPTranslator) # Docutils Node Visitor overrides (html builder)
53+
app.set_translator("dirhtml", pep_html_translator.PEPTranslator) # Docutils Node Visitor overrides (dirhtml builder)
4154
app.connect("env-before-read-docs", create_pep_zero) # PEP 0 hook
55+
app.connect("builder-inited", _update_config_for_builder) # Update configuration values for builder used
4256

4357
# Mathematics rendering
4458
inline_maths = HTMLTranslator.visit_math, _depart_maths
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
from sphinx import roles
22

3-
from pep_sphinx_extensions.config import pep_url
3+
from pep_sphinx_extensions import config
44

55

66
class PEPRole(roles.PEP):
77
"""Override the :pep: role"""
88

99
def build_uri(self) -> str:
1010
"""Get PEP URI from role text."""
11-
base_url = self.inliner.document.settings.pep_base_url
12-
pep_num, _, fragment = self.target.partition("#")
13-
pep_base = base_url + pep_url.format(int(pep_num))
11+
pep_str, _, fragment = self.target.partition("#")
12+
pep_base = config.pep_url.format(int(pep_str))
1413
if fragment:
1514
return f"{pep_base}#{fragment}"
1615
return pep_base
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
from pathlib import Path
24

35
from docutils import nodes
@@ -14,21 +16,20 @@ class PEPContents(transforms.Transform):
1416
def apply(self) -> None:
1517
if not Path(self.document["source"]).match("pep-*"):
1618
return # not a PEP file, exit early
17-
1819
# Create the contents placeholder section
19-
title = nodes.title("", "Contents")
20-
contents_topic = nodes.topic("", title, classes=["contents"])
20+
title = nodes.title("", "", nodes.Text("Contents"))
21+
contents_section = nodes.section("", title)
2122
if not self.document.has_name("contents"):
22-
contents_topic["names"].append("contents")
23-
self.document.note_implicit_target(contents_topic)
23+
contents_section["names"].append("contents")
24+
self.document.note_implicit_target(contents_section)
2425

2526
# Add a table of contents builder
2627
pending = nodes.pending(Contents)
27-
contents_topic += pending
28+
contents_section += pending
2829
self.document.note_pending(pending)
2930

3031
# Insert the toc after title and PEP headers
31-
self.document.children[0].insert(2, contents_topic)
32+
self.document.children[0].insert(2, contents_section)
3233

3334
# Add a horizontal rule before contents
3435
transition = nodes.transition()
@@ -37,27 +38,42 @@ def apply(self) -> None:
3738

3839
class Contents(parts.Contents):
3940
"""Build Table of Contents from document."""
40-
def __init__(self, document, startnode=None):
41+
def __init__(self, document: nodes.document, startnode: nodes.Node | None = None):
4142
super().__init__(document, startnode)
4243

4344
# used in parts.Contents.build_contents
4445
self.toc_id = None
4546
self.backlinks = None
4647

4748
def apply(self) -> None:
48-
# used in parts.Contents.build_contents
49-
self.toc_id = self.startnode.parent["ids"][0]
50-
self.backlinks = self.document.settings.toc_backlinks
51-
52-
# let the writer (or output software) build the contents list?
53-
if getattr(self.document.settings, "use_latex_toc", False):
54-
# move customisation settings to the parent node
55-
self.startnode.parent.attributes.update(self.startnode.details)
56-
self.startnode.parent.remove(self.startnode)
49+
contents = self.build_contents(self.document[0][4:]) # skip PEP title, headers, <hr/>, and contents
50+
if contents:
51+
self.startnode.replace_self(contents)
5752
else:
58-
contents = self.build_contents(self.document[0])
59-
if contents:
60-
self.startnode.replace_self(contents)
61-
else:
62-
# if no contents, remove the empty placeholder
63-
self.startnode.parent.parent.remove(self.startnode.parent)
53+
# if no contents, remove the empty placeholder
54+
self.startnode.parent.parent.remove(self.startnode.parent)
55+
56+
def build_contents(self, node: nodes.Node | list[nodes.Node], _level: None = None):
57+
entries = []
58+
children = getattr(node, "children", node)
59+
60+
for section in children:
61+
if not isinstance(section, nodes.section):
62+
continue
63+
64+
title = section[0]
65+
66+
# remove all pre-existing hyperlinks in the title (e.g. PEP references)
67+
while (link_node := title.next_node(nodes.reference)) is not None:
68+
link_node.replace_self(link_node[0])
69+
ref_id = section['ids'][0]
70+
title["refid"] = ref_id # Add a link to self
71+
entry_text = self.copy_and_filter(title)
72+
reference = nodes.reference("", "", refid=ref_id, *entry_text)
73+
item = nodes.list_item("", nodes.paragraph("", "", reference))
74+
75+
item += self.build_contents(section) # recurse to add sub-sections
76+
entries.append(item)
77+
if entries:
78+
return nodes.bullet_list('', *entries)
79+
return []

pep_sphinx_extensions/pep_processor/transforms/pep_footer.py

+36-36
Original file line numberDiff line numberDiff line change
@@ -69,43 +69,43 @@ def apply(self) -> None:
6969

7070
# If there are no references after TargetNotes has finished, remove the
7171
# references section
72-
pending = nodes.pending(misc.CallBack, details={"callback": self.cleanup_callback})
72+
pending = nodes.pending(misc.CallBack, details={"callback": _cleanup_callback})
7373
reference_section.append(pending)
7474
self.document.note_pending(pending, priority=1)
7575

7676
# Add link to source text and last modified date
77-
self.add_source_link(pep_source_path)
78-
self.add_commit_history_info(pep_source_path)
79-
80-
@staticmethod
81-
def cleanup_callback(pending: nodes.pending) -> None:
82-
"""Remove an empty "References" section.
83-
84-
Called after the `references.TargetNotes` transform is complete.
85-
86-
"""
87-
if len(pending.parent) == 2: # <title> and <pending>
88-
pending.parent.parent.remove(pending.parent)
89-
90-
def add_source_link(self, pep_source_path: Path) -> None:
91-
"""Add link to source text on VCS (GitHub)"""
92-
source_link = config.pep_vcs_url + pep_source_path.name
93-
link_node = nodes.reference("", source_link, refuri=source_link)
94-
span_node = nodes.inline("", "Source: ", link_node)
95-
self.document.append(span_node)
96-
97-
def add_commit_history_info(self, pep_source_path: Path) -> None:
98-
"""Use local git history to find last modified date."""
99-
args = ["git", "--no-pager", "log", "-1", "--format=%at", pep_source_path.name]
100-
try:
101-
file_modified = subprocess.check_output(args)
102-
since_epoch = file_modified.decode("utf-8").strip()
103-
dt = datetime.datetime.utcfromtimestamp(float(since_epoch))
104-
except (subprocess.CalledProcessError, ValueError):
105-
return None
106-
107-
commit_link = config.pep_commits_url + pep_source_path.name
108-
link_node = nodes.reference("", f"{dt.isoformat()}Z", refuri=commit_link)
109-
span_node = nodes.inline("", "Last modified: ", link_node)
110-
self.document.append(nodes.line("", "", classes=["zero-height"]))
111-
self.document.append(span_node)
77+
if pep_source_path.stem != "pep-0000":
78+
self.document += _add_source_link(pep_source_path)
79+
self.document += _add_commit_history_info(pep_source_path)
80+
81+
82+
def _cleanup_callback(pending: nodes.pending) -> None:
83+
"""Remove an empty "References" section.
84+
85+
Called after the `references.TargetNotes` transform is complete.
86+
87+
"""
88+
if len(pending.parent) == 2: # <title> and <pending>
89+
pending.parent.parent.remove(pending.parent)
90+
91+
92+
def _add_source_link(pep_source_path: Path) -> nodes.paragraph:
93+
"""Add link to source text on VCS (GitHub)"""
94+
source_link = config.pep_vcs_url + pep_source_path.name
95+
link_node = nodes.reference("", source_link, refuri=source_link)
96+
return nodes.paragraph("", "Source: ", link_node)
97+
98+
99+
def _add_commit_history_info(pep_source_path: Path) -> nodes.paragraph:
100+
"""Use local git history to find last modified date."""
101+
args = ["git", "--no-pager", "log", "-1", "--format=%at", pep_source_path.name]
102+
try:
103+
file_modified = subprocess.check_output(args)
104+
since_epoch = file_modified.decode("utf-8").strip()
105+
dt = datetime.datetime.utcfromtimestamp(float(since_epoch))
106+
except (subprocess.CalledProcessError, ValueError):
107+
return nodes.paragraph()
108+
109+
commit_link = config.pep_commits_url + pep_source_path.name
110+
link_node = nodes.reference("", f"{dt.isoformat(sep=' ')} GMT", refuri=commit_link)
111+
return nodes.paragraph("", "Last modified: ", link_node)

0 commit comments

Comments
 (0)