Skip to content

Commit

Permalink
Sphinx support: theming (python#1933)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
AA-Turner authored Jun 30, 2021
1 parent 7d72700 commit 0d93abf
Show file tree
Hide file tree
Showing 19 changed files with 591 additions and 129 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/deploy-gh-pages.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ jobs:
- name: 🔧 Build PEPs
run: make pages -j$(nproc)

# remove the .doctrees folder when building for deployment as it takes two thirds of disk space
- name: 🔥 Clean up files
run: rm -r build/.doctrees/

- name: 🚀 Deploy to GitHub pages
uses: JamesIves/[email protected]
with:
Expand Down
17 changes: 11 additions & 6 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import argparse
from pathlib import Path
import shutil

from sphinx.application import Sphinx

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


def create_index_file(html_root: Path):
def create_index_file(html_root: Path, builder: str) -> None:
"""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")
pep_zero_file = "pep-0000.html" if builder == "html" else "pep-0000/index.html"
try:
pep_zero_text = html_root.joinpath(pep_zero_file).read_text(encoding="utf-8")
except FileNotFoundError:
return None
if builder == "dirhtml":
pep_zero_text = pep_zero_text.replace('="../', '="') # remove relative directory links
html_root.joinpath("index.html").write_text(pep_zero_text, encoding="utf-8")


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

if args.index_file:
create_index_file(build_directory)
create_index_file(build_directory, sphinx_builder)
12 changes: 11 additions & 1 deletion conf.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Configuration for building PEPs using Sphinx."""

import sys
from pathlib import Path
import sys

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

Expand Down Expand Up @@ -44,3 +44,13 @@
html_show_copyright = False # Turn off miscellany
html_show_sphinx = False
html_title = "peps.python.org" # Set <title/>

# Theme settings
html_theme_path = ["pep_sphinx_extensions"]
html_theme = "pep_theme" # The actual theme directory (child of html_theme_path)
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_permalinks = False # handled in the PEPContents transform

templates_path = ['pep_sphinx_extensions/pep_theme/templates'] # Theme template relative paths from `confdir`
9 changes: 3 additions & 6 deletions pep-0310.txt
Original file line number Diff line number Diff line change
Expand Up @@ -238,12 +238,9 @@ could be mentioned here.
https://mail.python.org/pipermail/python-dev/2003-August/037795.html

.. [3] Thread on python-dev with subject

.. [Python-Dev] pre-PEP: Resource-Release Support for Generators

starting at

https://mail.python.org/pipermail/python-dev/2003-August/037803.html
`[Python-Dev] pre-PEP: Resource-Release Support for Generators`
starting at
https://mail.python.org/pipermail/python-dev/2003-August/037803.html

Copyright
=========
Expand Down
4 changes: 2 additions & 2 deletions pep-0439.txt
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,11 @@ The "pip3" command will support two new command-line options that are used
in the boostrapping, and otherwise ignored. They control where the pip
implementation is installed:

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

--bootstrap-to-system
``--bootstrap-to-system``
Install to the system site-packages directory.

These command-line options will also need to be implemented, but otherwise
Expand Down
18 changes: 0 additions & 18 deletions pep-3143.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,6 @@ any daemon regardless of what else the program may need to do.
This PEP introduces a package to the Python standard library that
provides a simple interface to the task of becoming a daemon process.


.. contents::
..
Table of Contents:
Abstract
Specification
Example usage
Interface
``DaemonContext`` objects
Motivation
Rationale
Correct daemon behaviour
A daemon is not a service
Reference Implementation
Other daemon implementations
References
Copyright
============
PEP Deferral
============
Expand Down
16 changes: 15 additions & 1 deletion pep_sphinx_extensions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
from typing import TYPE_CHECKING

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

from pep_sphinx_extensions import config
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
Expand All @@ -26,19 +28,31 @@
"_disable_config": True, # disable using docutils.conf whilst running both PEP generators
}

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


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


def _update_config_for_builder(app: Sphinx):
if app.builder.name == "dirhtml":
config.pep_url = f"../{config.pep_stem}"
app.env.settings["pep_file_url_template"] = "../pep-%04d"


def setup(app: Sphinx) -> dict[str, bool]:
"""Initialize Sphinx extension."""

# Register plugin logic
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.set_translator("html", pep_html_translator.PEPTranslator) # Docutils Node Visitor overrides (html builder)
app.set_translator("dirhtml", pep_html_translator.PEPTranslator) # Docutils Node Visitor overrides (dirhtml builder)
app.connect("env-before-read-docs", create_pep_zero) # PEP 0 hook
app.connect("builder-inited", _update_config_for_builder) # Update configuration values for builder used

# Mathematics rendering
inline_maths = HTMLTranslator.visit_math, _depart_maths
Expand Down
7 changes: 3 additions & 4 deletions pep_sphinx_extensions/pep_processor/parsing/pep_role.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
from sphinx import roles

from pep_sphinx_extensions.config import pep_url
from pep_sphinx_extensions import config


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

def build_uri(self) -> str:
"""Get PEP URI from role text."""
base_url = self.inliner.document.settings.pep_base_url
pep_num, _, fragment = self.target.partition("#")
pep_base = base_url + pep_url.format(int(pep_num))
pep_str, _, fragment = self.target.partition("#")
pep_base = config.pep_url.format(int(pep_str))
if fragment:
return f"{pep_base}#{fragment}"
return pep_base
62 changes: 39 additions & 23 deletions pep_sphinx_extensions/pep_processor/transforms/pep_contents.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from pathlib import Path

from docutils import nodes
Expand All @@ -14,21 +16,20 @@ class PEPContents(transforms.Transform):
def apply(self) -> None:
if not Path(self.document["source"]).match("pep-*"):
return # not a PEP file, exit early

# Create the contents placeholder section
title = nodes.title("", "Contents")
contents_topic = nodes.topic("", title, classes=["contents"])
title = nodes.title("", "", nodes.Text("Contents"))
contents_section = nodes.section("", title)
if not self.document.has_name("contents"):
contents_topic["names"].append("contents")
self.document.note_implicit_target(contents_topic)
contents_section["names"].append("contents")
self.document.note_implicit_target(contents_section)

# Add a table of contents builder
pending = nodes.pending(Contents)
contents_topic += pending
contents_section += pending
self.document.note_pending(pending)

# Insert the toc after title and PEP headers
self.document.children[0].insert(2, contents_topic)
self.document.children[0].insert(2, contents_section)

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

class Contents(parts.Contents):
"""Build Table of Contents from document."""
def __init__(self, document, startnode=None):
def __init__(self, document: nodes.document, startnode: nodes.Node | None = None):
super().__init__(document, startnode)

# used in parts.Contents.build_contents
self.toc_id = None
self.backlinks = None

def apply(self) -> None:
# used in parts.Contents.build_contents
self.toc_id = self.startnode.parent["ids"][0]
self.backlinks = self.document.settings.toc_backlinks

# let the writer (or output software) build the contents list?
if getattr(self.document.settings, "use_latex_toc", False):
# move customisation settings to the parent node
self.startnode.parent.attributes.update(self.startnode.details)
self.startnode.parent.remove(self.startnode)
contents = self.build_contents(self.document[0][4:]) # skip PEP title, headers, <hr/>, and contents
if contents:
self.startnode.replace_self(contents)
else:
contents = self.build_contents(self.document[0])
if contents:
self.startnode.replace_self(contents)
else:
# if no contents, remove the empty placeholder
self.startnode.parent.parent.remove(self.startnode.parent)
# if no contents, remove the empty placeholder
self.startnode.parent.parent.remove(self.startnode.parent)

def build_contents(self, node: nodes.Node | list[nodes.Node], _level: None = None):
entries = []
children = getattr(node, "children", node)

for section in children:
if not isinstance(section, nodes.section):
continue

title = section[0]

# remove all pre-existing hyperlinks in the title (e.g. PEP references)
while (link_node := title.next_node(nodes.reference)) is not None:
link_node.replace_self(link_node[0])
ref_id = section['ids'][0]
title["refid"] = ref_id # Add a link to self
entry_text = self.copy_and_filter(title)
reference = nodes.reference("", "", refid=ref_id, *entry_text)
item = nodes.list_item("", nodes.paragraph("", "", reference))

item += self.build_contents(section) # recurse to add sub-sections
entries.append(item)
if entries:
return nodes.bullet_list('', *entries)
return []
72 changes: 36 additions & 36 deletions pep_sphinx_extensions/pep_processor/transforms/pep_footer.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,43 +69,43 @@ def apply(self) -> None:

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

# Add link to source text and last modified date
self.add_source_link(pep_source_path)
self.add_commit_history_info(pep_source_path)

@staticmethod
def cleanup_callback(pending: nodes.pending) -> None:
"""Remove an empty "References" section.
Called after the `references.TargetNotes` transform is complete.
"""
if len(pending.parent) == 2: # <title> and <pending>
pending.parent.parent.remove(pending.parent)

def add_source_link(self, pep_source_path: Path) -> None:
"""Add link to source text on VCS (GitHub)"""
source_link = config.pep_vcs_url + pep_source_path.name
link_node = nodes.reference("", source_link, refuri=source_link)
span_node = nodes.inline("", "Source: ", link_node)
self.document.append(span_node)

def add_commit_history_info(self, pep_source_path: Path) -> None:
"""Use local git history to find last modified date."""
args = ["git", "--no-pager", "log", "-1", "--format=%at", pep_source_path.name]
try:
file_modified = subprocess.check_output(args)
since_epoch = file_modified.decode("utf-8").strip()
dt = datetime.datetime.utcfromtimestamp(float(since_epoch))
except (subprocess.CalledProcessError, ValueError):
return None

commit_link = config.pep_commits_url + pep_source_path.name
link_node = nodes.reference("", f"{dt.isoformat()}Z", refuri=commit_link)
span_node = nodes.inline("", "Last modified: ", link_node)
self.document.append(nodes.line("", "", classes=["zero-height"]))
self.document.append(span_node)
if pep_source_path.stem != "pep-0000":
self.document += _add_source_link(pep_source_path)
self.document += _add_commit_history_info(pep_source_path)


def _cleanup_callback(pending: nodes.pending) -> None:
"""Remove an empty "References" section.
Called after the `references.TargetNotes` transform is complete.
"""
if len(pending.parent) == 2: # <title> and <pending>
pending.parent.parent.remove(pending.parent)


def _add_source_link(pep_source_path: Path) -> nodes.paragraph:
"""Add link to source text on VCS (GitHub)"""
source_link = config.pep_vcs_url + pep_source_path.name
link_node = nodes.reference("", source_link, refuri=source_link)
return nodes.paragraph("", "Source: ", link_node)


def _add_commit_history_info(pep_source_path: Path) -> nodes.paragraph:
"""Use local git history to find last modified date."""
args = ["git", "--no-pager", "log", "-1", "--format=%at", pep_source_path.name]
try:
file_modified = subprocess.check_output(args)
since_epoch = file_modified.decode("utf-8").strip()
dt = datetime.datetime.utcfromtimestamp(float(since_epoch))
except (subprocess.CalledProcessError, ValueError):
return nodes.paragraph()

commit_link = config.pep_commits_url + pep_source_path.name
link_node = nodes.reference("", f"{dt.isoformat(sep=' ')} GMT", refuri=commit_link)
return nodes.paragraph("", "Last modified: ", link_node)
Loading

0 comments on commit 0d93abf

Please sign in to comment.