diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 892c098f911..c0a3a207949 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -54,27 +54,23 @@ repos:
name: "Format with Black"
args:
- '--target-version=py311'
- files: '^(peps/conf\.py|pep_sphinx_extensions/tests/.*)$'
+ exclude: '^peps/'
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.15.21
+ rev: v0.16.6
hooks:
- id: ruff-check
name: "Lint with Ruff"
args:
- '--exit-non-zero-on-fix'
- files: '^pep_sphinx_extensions/tests/'
- - id: ruff-format
- name: "Format with Ruff"
- args:
- - '--check'
- files: '^release_management/'
+ exclude: '^peps/'
- repo: https://github.com/python-jsonschema/check-jsonschema
- rev: 0.37.4
+ rev: 0.38.0
hooks:
- id: check-dependabot
- id: check-github-workflows
+ - id: check-readthedocs
- repo: https://github.com/rhysd/actionlint
rev: v1.7.12
@@ -82,12 +78,22 @@ repos:
- id: actionlint
- repo: https://github.com/zizmorcore/zizmor-pre-commit
- rev: v1.26.1
+ rev: v1.30.1
hooks:
- id: zizmor
+ - repo: https://github.com/tox-dev/pyproject-fmt
+ rev: v2.29.4
+ hooks:
+ - id: pyproject-fmt
+
+ - repo: https://github.com/abravalheri/validate-pyproject
+ rev: '0.26'
+ hooks:
+ - id: validate-pyproject
+
- repo: https://github.com/tox-dev/tox-ini-fmt
- rev: 1.7.2
+ rev: 1.9.0
hooks:
- id: tox-ini-fmt
name: "Format tox.ini"
@@ -114,7 +120,7 @@ repos:
# Manual codespell check
- repo: https://github.com/codespell-project/codespell
- rev: v2.4.2
+ rev: v2.4.3
hooks:
- id: codespell
name: "Check for common misspellings in text files"
diff --git a/.ruff.toml b/.ruff.toml
deleted file mode 100644
index 5e0a0fa17f3..00000000000
--- a/.ruff.toml
+++ /dev/null
@@ -1,15 +0,0 @@
-output-format = "full"
-target-version = "py311"
-
-[lint]
-ignore = [
- "E501", # Line too long
-]
-
-select = [
- "E", # pycodestyle errors
- "F", # pyflakes
- "I", # isort
- "PT", # flake8-pytest-style
- "W", # pycodestyle warnings
-]
diff --git a/build.py b/build.py
index 04f0b5fdb05..269e616dee4 100755
--- a/build.py
+++ b/build.py
@@ -15,18 +15,33 @@ def create_parser():
parser = argparse.ArgumentParser(description="Build PEP documents")
# alternative builders:
builders = parser.add_mutually_exclusive_group()
- builders.add_argument("-l", "--check-links", action="store_const",
- dest="builder", const="linkcheck",
- help='Check validity of links within PEP sources. '
- 'Cannot be used with "-f" or "-d".')
- builders.add_argument("-f", "--build-files", action="store_const",
- dest="builder", const="html",
- help='Render PEPs to "pep-NNNN.html" files (default). '
- 'Cannot be used with "-d" or "-l".')
- builders.add_argument("-d", "--build-dirs", action="store_const",
- dest="builder", const="dirhtml",
- help='Render PEPs to "index.html" files within "pep-NNNN" directories. '
- 'Cannot be used with "-f" or "-l".')
+ builders.add_argument(
+ "-l",
+ "--check-links",
+ action="store_const",
+ dest="builder",
+ const="linkcheck",
+ help="Check validity of links within PEP sources. "
+ 'Cannot be used with "-f" or "-d".',
+ )
+ builders.add_argument(
+ "-f",
+ "--build-files",
+ action="store_const",
+ dest="builder",
+ const="html",
+ help='Render PEPs to "pep-NNNN.html" files (default). '
+ 'Cannot be used with "-d" or "-l".',
+ )
+ builders.add_argument(
+ "-d",
+ "--build-dirs",
+ action="store_const",
+ dest="builder",
+ const="dirhtml",
+ help='Render PEPs to "index.html" files within "pep-NNNN" directories. '
+ 'Cannot be used with "-f" or "-l".',
+ )
parser.add_argument(
"-o",
@@ -46,7 +61,8 @@ def create_index_file(html_root: Path, builder: str) -> None:
except FileNotFoundError:
return None
if builder == "dirhtml":
- pep_zero_text = pep_zero_text.replace('="../', '="') # remove relative directory links
+ # remove relative directory links
+ pep_zero_text = pep_zero_text.replace('="../', '="')
html_root.joinpath("index.html").write_text(pep_zero_text, encoding="utf-8")
diff --git a/check-peps.py b/check-peps.py
index 75898a13aab..20931c85a7e 100755
--- a/check-peps.py
+++ b/check-peps.py
@@ -41,7 +41,8 @@
"Title",
"Author",
"Sponsor",
- "BDFL-Delegate", "PEP-Delegate",
+ "BDFL-Delegate",
+ "PEP-Delegate",
"Discussions-To",
"Status",
"Type",
@@ -57,18 +58,20 @@
REQUIRED_HEADERS = frozenset({"PEP", "Title", "Author", "Status", "Type", "Created"})
# See PEP 1 for the full list
-ALL_STATUSES = frozenset({
- "Accepted",
- "Active",
- "April Fool!",
- "Deferred",
- "Draft",
- "Final",
- "Provisional",
- "Rejected",
- "Superseded",
- "Withdrawn",
-})
+ALL_STATUSES = frozenset(
+ {
+ "Accepted",
+ "Active",
+ "April Fool!",
+ "Deferred",
+ "Draft",
+ "Final",
+ "Provisional",
+ "Rejected",
+ "Superseded",
+ "Withdrawn",
+ }
+)
# PEPs that are allowed to link directly to PEPs
SKIP_DIRECT_PEP_LINK_CHECK = frozenset({"0009", "0287", "0676", "0684", "8001"})
@@ -86,8 +89,12 @@
DISCOURSE_POST_PATTERN = re.compile(r"([\w\-]+/)?\d+(/\d+)?", DEFAULT_FLAGS)
MAILMAN_2_PATTERN = re.compile(r"[\w\-]+/\d{4}-[a-z]+/\d+\.html", DEFAULT_FLAGS)
-MAILMAN_3_THREAD_PATTERN = re.compile(r"[\w\-]+@python\.org/thread/[a-z0-9]+/?", DEFAULT_FLAGS)
-MAILMAN_3_MESSAGE_PATTERN = re.compile(r"[\w\-]+@python\.org/message/[a-z0-9]+/?(#[a-z0-9]+)?", DEFAULT_FLAGS)
+MAILMAN_3_THREAD_PATTERN = re.compile(
+ r"[\w\-]+@python\.org/thread/[a-z0-9]+/?", DEFAULT_FLAGS
+)
+MAILMAN_3_MESSAGE_PATTERN = re.compile(
+ r"[\w\-]+@python\.org/message/[a-z0-9]+/?(#[a-z0-9]+)?", DEFAULT_FLAGS
+)
# Controlled by the "--detailed" flag
DETAILED_ERRORS = False
@@ -203,7 +210,9 @@ def check_direct_links(line_num: int, line: str) -> MessageIterator:
yield line_num, "Use the :rfc:`NNN` role to refer to RFCs"
-def _output_error(filename: Path, lines: Sequence[str], errors: Iterable[Message]) -> int:
+def _output_error(
+ filename: Path, lines: Sequence[str], errors: Iterable[Message]
+) -> int:
relative_filename = filename.relative_to(ROOT_DIR)
err_count = 0
for line_num, msg in errors:
@@ -501,7 +510,14 @@ def _invalid_domain(domain_part: str) -> bool:
return not root.isalnum() or not root.isascii()
-def _thread(line_num: int, url: str, prefix: str, *, allow_message: bool = False, discussions_to: bool = False) -> MessageIterator:
+def _thread(
+ line_num: int,
+ url: str,
+ prefix: str,
+ *,
+ allow_message: bool = False,
+ discussions_to: bool = False,
+) -> MessageIterator:
if allow_message and discussions_to:
msg = "allow_message and discussions_to cannot both be True"
raise ValueError(msg)
@@ -523,7 +539,7 @@ def _thread(line_num: int, url: str, prefix: str, *, allow_message: bool = False
# We use ``str.rpartition`` as the topic name is optional
topic_name, _, topic_id = remainder.rpartition("/")
- if topic_name == '' and _is_digits(topic_id):
+ if topic_name == "" and _is_digits(topic_id):
return
topic_name = topic_name.replace("-", "0").replace("_", "0")
# the topic name must not be entirely numeric
diff --git a/pep_sphinx_extensions/__init__.py b/pep_sphinx_extensions/__init__.py
index 109c09d7890..1c3383dfd81 100644
--- a/pep_sphinx_extensions/__init__.py
+++ b/pep_sphinx_extensions/__init__.py
@@ -12,7 +12,6 @@
from pep_sphinx_extensions.generate_rss import (
create_rss_feed,
get_from_doctree,
- pep_abstract,
)
from pep_sphinx_extensions.pep_processor.html import (
pep_html_builder,
@@ -23,8 +22,7 @@
pep_parser,
pep_role,
)
-from pep_sphinx_extensions.pep_processor.transforms import pep_footer
-from pep_sphinx_extensions.pep_processor.transforms import pep_references
+from pep_sphinx_extensions.pep_processor.transforms import pep_footer, pep_references
from pep_sphinx_extensions.pep_zero_generator.pep_index_generator import create_pep_zero
if TYPE_CHECKING:
@@ -85,35 +83,45 @@ def setup(app: Sphinx) -> dict[str, bool]:
app.add_source_parser(pep_parser.PEPParser) # Add PEP transforms
- 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.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.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.add_post_transform(pep_references.PEPReferenceRoleTitleText)
# Register custom directives
+ app.add_directive("pep-banner", pep_banner_directive.PEPBanner)
+ app.add_directive("canonical-doc", pep_banner_directive.CanonicalDocBanner)
app.add_directive(
- "pep-banner", pep_banner_directive.PEPBanner)
+ "canonical-pypa-spec", pep_banner_directive.CanonicalPyPASpecBanner
+ )
app.add_directive(
- "canonical-doc", pep_banner_directive.CanonicalDocBanner)
- app.add_directive(
- "canonical-pypa-spec", pep_banner_directive.CanonicalPyPASpecBanner)
- app.add_directive(
- "canonical-typing-spec", pep_banner_directive.CanonicalTypingSpecBanner)
+ "canonical-typing-spec", pep_banner_directive.CanonicalTypingSpecBanner
+ )
app.add_directive("rejected", pep_banner_directive.RejectedBanner)
app.add_directive("superseded", pep_banner_directive.SupersededBanner)
app.add_directive("withdrawn", pep_banner_directive.WithdrawnBanner)
# Register event callbacks
- app.connect("builder-inited", _update_config_for_builder) # Update configuration values for builder used
+ app.connect(
+ "builder-inited", _update_config_for_builder
+ ) # Update configuration values for builder used
app.connect("env-before-read-docs", create_pep_zero) # PEP 0 hook
- app.connect('html-page-context', set_description)
+ app.connect("html-page-context", set_description)
# Mathematics rendering
inline_maths = HTMLTranslator.visit_math, None
block_maths = HTMLTranslator.visit_math_block, None
- app.add_html_math_renderer("maths_to_html", inline_maths, block_maths) # Render maths to HTML
+ app.add_html_math_renderer(
+ "maths_to_html", inline_maths, block_maths
+ ) # Render maths to HTML
# Parallel safety: https://www.sphinx-doc.org/en/master/extdev/index.html#extension-metadata
return {"parallel_read_safe": True, "parallel_write_safe": True}
diff --git a/pep_sphinx_extensions/generate_rss.py b/pep_sphinx_extensions/generate_rss.py
index 296bb033e0e..3b8b95f73da 100644
--- a/pep_sphinx_extensions/generate_rss.py
+++ b/pep_sphinx_extensions/generate_rss.py
@@ -19,7 +19,7 @@
def _format_rfc_2822(datetime: dt.datetime) -> str:
- datetime = datetime.replace(tzinfo=dt.timezone.utc)
+ datetime = datetime.replace(tzinfo=dt.UTC)
return format_datetime(datetime, usegmt=True)
@@ -64,14 +64,18 @@ def pep_abstract(document: nodes.document) -> str:
return para_node.astext().strip().replace("\n", " ")
return ""
if title_node.astext() == "Introduction":
- introduction = node.next_node(nodes.paragraph).astext().strip().replace("\n", " ")
+ introduction = (
+ node.next_node(nodes.paragraph).astext().strip().replace("\n", " ")
+ )
return introduction
def _generate_items(doctree_dir: Path):
# get list of peps with creation time (from "Created:" string in pep source)
- peps_with_dt = sorted((pep_creation(path), path) for path in doctree_dir.glob("pep-????.doctree"))
+ peps_with_dt = sorted(
+ (pep_creation(path), path) for path in doctree_dir.glob("pep-????.doctree")
+ )
# generate rss items for 10 most recent peps (in reverse order)
for datetime, full_path in reversed(peps_with_dt[-10:]):
@@ -86,7 +90,9 @@ def _generate_items(doctree_dir: Path):
author = get_from_doctree(full_path, "Author")
if "@" in author or " at " in author:
parsed_authors = getaddresses([author])
- joined_authors = ", ".join(f"{name} ({email_address})" for name, email_address in parsed_authors)
+ joined_authors = ", ".join(
+ f"{name} ({email_address})" for name, email_address in parsed_authors
+ )
else:
joined_authors = author
@@ -104,7 +110,7 @@ def _generate_items(doctree_dir: Path):
def create_rss_feed(doctree_dir: Path, output_dir: Path):
# The rss envelope
- last_build_date = _format_rfc_2822(dt.datetime.now(dt.timezone.utc))
+ last_build_date = _format_rfc_2822(dt.datetime.now(dt.UTC))
items = "\n".join(_generate_items(Path(doctree_dir)))
output = f"""\
diff --git a/pep_sphinx_extensions/pep_processor/html/pep_html_builder.py b/pep_sphinx_extensions/pep_processor/html/pep_html_builder.py
index 467dd82670c..4790836f53a 100644
--- a/pep_sphinx_extensions/pep_processor/html/pep_html_builder.py
+++ b/pep_sphinx_extensions/pep_processor/html/pep_html_builder.py
@@ -1,10 +1,9 @@
from docutils import nodes
from docutils.frontend import OptionParser
+from sphinx.builders.dirhtml import DirectoryHTMLBuilder
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
@@ -18,7 +17,9 @@ class FileBuilder(StandaloneHTMLBuilder):
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)
+ _opt_parser = OptionParser(
+ [self.docwriter], defaults=self.env.settings, read_config_files=True
+ )
self.docsettings = _opt_parser.get_default_values()
self._orig_css_files = self._orig_js_files = []
@@ -36,7 +37,7 @@ def get_doc_context(self, docname: str, body: str, _metatags: str) -> dict:
if docname.startswith("pep-"):
del toc_tree[0] # remove contents node from PEPs
for node in toc_tree.findall(nodes.reference):
- node["refuri"] = node["anchorname"] or '#' # fix targets
+ node["refuri"] = node["anchorname"] or "#" # fix targets
toc = self.render_partial(toc_tree)["fragment"]
else:
toc = "" # PEPs with no sections -- 9, 210
diff --git a/pep_sphinx_extensions/pep_processor/html/pep_html_translator.py b/pep_sphinx_extensions/pep_processor/html/pep_html_translator.py
index e8893790ef6..c65e2a1ad95 100644
--- a/pep_sphinx_extensions/pep_processor/html/pep_html_translator.py
+++ b/pep_sphinx_extensions/pep_processor/html/pep_html_translator.py
@@ -2,8 +2,8 @@
from typing import TYPE_CHECKING
-from docutils import nodes
import sphinx.writers.html5 as html5
+from docutils import nodes
if TYPE_CHECKING:
from sphinx.builders import html
@@ -38,7 +38,11 @@ def should_be_compact_paragraph(node: nodes.paragraph) -> bool:
# Only first paragraph can be compact (ignoring initial label & invisible nodes)
first = isinstance(node.parent[0], nodes.label)
- visible_siblings = [child for child in node.parent.children[first:] if not isinstance(child, nodes.Invisible)]
+ visible_siblings = [
+ child
+ for child in node.parent.children[first:]
+ if not isinstance(child, nodes.Invisible)
+ ]
if visible_siblings[0] is not node:
return False
@@ -58,13 +62,18 @@ def depart_paragraph(self, _: nodes.paragraph) -> None:
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']}"
- ))
+ 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(']')
+ self.body.append("]")
def visit_label(self, node):
# pass parent node to get id into starttag:
@@ -83,21 +92,29 @@ def depart_label(self, node) -> None:
self.body.append(self.context.pop())
back_refs = node.parent["backrefs"]
if self.settings.footnote_backlinks and len(back_refs) > 1:
- back_links = ", ".join(f"{i}" for i, ref in enumerate(back_refs, start=1))
+ back_links = ", ".join(
+ f"{i}" for i, ref in enumerate(back_refs, start=1)
+ )
self.body.append(f" ({back_links}) ")
# Close the def tags
self.body.append("\n
")
def visit_bullet_list(self, node):
- if isinstance(node.parent, nodes.section) and "contents" in node.parent["names"]:
+ if (
+ isinstance(node.parent, nodes.section)
+ and "contents" in node.parent["names"]
+ ):
self.body.append("Table of Contents
")
self.context.append(" ")
super().visit_bullet_list(node)
def depart_bullet_list(self, node):
super().depart_bullet_list(node)
- if isinstance(node.parent, nodes.section) and "contents" in node.parent["names"]:
+ if (
+ isinstance(node.parent, nodes.section)
+ and "contents" in node.parent["names"]
+ ):
self.body.append(self.context.pop())
def unknown_visit(self, node: nodes.Node) -> None:
diff --git a/pep_sphinx_extensions/pep_processor/parsing/pep_banner_directive.py b/pep_sphinx_extensions/pep_processor/parsing/pep_banner_directive.py
index 5584719dd39..12a62102692 100644
--- a/pep_sphinx_extensions/pep_processor/parsing/pep_banner_directive.py
+++ b/pep_sphinx_extensions/pep_processor/parsing/pep_banner_directive.py
@@ -29,13 +29,14 @@ def run(self) -> list[nodes.admonition]:
if self.arguments:
link_content = self.arguments[0]
- pre_text = self.admonition_pre_template.format(
- link_content=link_content)
+ pre_text = self.admonition_pre_template.format(link_content=link_content)
else:
pre_text = self.admonition_pre_text
- close_button_node = nodes.paragraph('', '', nodes.Text('×'), classes=['close-button'])
- close_button_node['classes'].append('close-button')
+ close_button_node = nodes.paragraph(
+ "", "", nodes.Text("×"), classes=["close-button"]
+ )
+ close_button_node["classes"].append("close-button")
pre_text_node = nodes.paragraph(pre_text)
pre_text_node.line = self.lineno
@@ -50,13 +51,13 @@ def run(self) -> list[nodes.admonition]:
source_lines = [pre_text] + list(self.content or []) + [post_text]
admonition_node = self.admonition_class(
- "\n".join(source_lines), classes=["pep-banner"] + self.css_classes)
+ "\n".join(source_lines), classes=["pep-banner"] + self.css_classes
+ )
admonition_node.append(pre_text_node)
admonition_node.append(close_button_node)
if self.content:
- self.state.nested_parse(
- self.content, self.content_offset, admonition_node)
+ self.state.nested_parse(self.content, self.content_offset, admonition_node)
admonition_node.append(post_text_node)
return [admonition_node]
@@ -74,9 +75,7 @@ class CanonicalDocBanner(PEPBanner):
"This PEP is a historical document. "
"The up-to-date, canonical documentation can now be found elsewhere."
)
- admonition_post_text = (
- "See :pep:`1` for how to propose changes."
- )
+ admonition_post_text = "See :pep:`1` for how to propose changes."
css_classes = ["canonical-doc", "sticky-banner"]
diff --git a/pep_sphinx_extensions/pep_processor/parsing/pep_parser.py b/pep_sphinx_extensions/pep_processor/parsing/pep_parser.py
index 2ccbd6cb857..af64a53f98b 100644
--- a/pep_sphinx_extensions/pep_processor/parsing/pep_parser.py
+++ b/pep_sphinx_extensions/pep_processor/parsing/pep_parser.py
@@ -4,10 +4,12 @@
from sphinx import parsers
-from pep_sphinx_extensions.pep_processor.transforms import pep_contents
-from pep_sphinx_extensions.pep_processor.transforms import pep_footer
-from pep_sphinx_extensions.pep_processor.transforms import pep_headers
-from pep_sphinx_extensions.pep_processor.transforms import pep_title
+from pep_sphinx_extensions.pep_processor.transforms import (
+ pep_contents,
+ pep_footer,
+ pep_headers,
+ pep_title,
+)
if TYPE_CHECKING:
from docutils import transforms
diff --git a/pep_sphinx_extensions/pep_processor/parsing/pep_role.py b/pep_sphinx_extensions/pep_processor/parsing/pep_role.py
index be1683a33ac..1760e284630 100644
--- a/pep_sphinx_extensions/pep_processor/parsing/pep_role.py
+++ b/pep_sphinx_extensions/pep_processor/parsing/pep_role.py
@@ -11,7 +11,9 @@ def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]:
try:
pep_num = int(pep_str)
except ValueError:
- msg = self.inliner.reporter.error(f'invalid PEP number {self.target}', line=self.lineno)
+ msg = self.inliner.reporter.error(
+ f"invalid PEP number {self.target}", line=self.lineno
+ )
prb = self.inliner.problematic(self.rawtext, self.rawtext, msg)
return [prb], [msg]
pep_base = self.inliner.document.settings.pep_url.format(pep_num)
@@ -30,10 +32,11 @@ def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]:
return [
nodes.reference(
- "", title,
+ "",
+ title,
internal=True,
refuri=ref_uri,
classes=["pep"],
- _title_tuple=(pep_num, fragment)
+ _title_tuple=(pep_num, fragment),
)
], []
diff --git a/pep_sphinx_extensions/pep_processor/transforms/pep_contents.py b/pep_sphinx_extensions/pep_processor/transforms/pep_contents.py
index 3810429ac78..4cd262f906f 100644
--- a/pep_sphinx_extensions/pep_processor/transforms/pep_contents.py
+++ b/pep_sphinx_extensions/pep_processor/transforms/pep_contents.py
@@ -2,8 +2,7 @@
from pathlib import Path
-from docutils import nodes
-from docutils import transforms
+from docutils import nodes, transforms
from docutils.transforms import parts
@@ -37,6 +36,7 @@ def apply(self) -> None:
class Contents(parts.Contents):
"""Build Table of Contents from document."""
+
def __init__(self, document: nodes.document, startnode: nodes.Node | None = None):
super().__init__(document, startnode)
@@ -45,7 +45,8 @@ def __init__(self, document: nodes.document, startnode: nodes.Node | None = None
self.backlinks = None
def apply(self) -> None:
- contents = self.build_contents(self.document[0][4:]) # skip PEP title, headers,
, and contents
+ # skip PEP title, headers,
, and contents
+ contents = self.build_contents(self.document[0][4:])
if contents:
self.startnode.replace_self(contents)
else:
@@ -65,7 +66,7 @@ def build_contents(self, node: nodes.Node | list[nodes.Node], _level: None = Non
# 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]
+ 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)
@@ -74,5 +75,5 @@ def build_contents(self, node: nodes.Node | list[nodes.Node], _level: None = Non
item += self.build_contents(section) # recurse to add sub-sections
entries.append(item)
if entries:
- return nodes.bullet_list('', *entries)
+ return nodes.bullet_list("", *entries)
return []
diff --git a/pep_sphinx_extensions/pep_processor/transforms/pep_footer.py b/pep_sphinx_extensions/pep_processor/transforms/pep_footer.py
index df96405479a..fbc3d6ace03 100644
--- a/pep_sphinx_extensions/pep_processor/transforms/pep_footer.py
+++ b/pep_sphinx_extensions/pep_processor/transforms/pep_footer.py
@@ -1,15 +1,14 @@
+import subprocess
import time
from pathlib import Path
-import subprocess
-from docutils import nodes
-from docutils import transforms
+from docutils import nodes, transforms
class PEPFooter(transforms.Transform):
"""Footer transforms for PEPs.
- - Remove the References/Footnotes section if it is empty when rendered.
+ - Remove the References/Footnotes section if it is empty when rendered.
"""
# Uses same priority as docutils.transforms.TargetNotes
diff --git a/pep_sphinx_extensions/pep_processor/transforms/pep_headers.py b/pep_sphinx_extensions/pep_processor/transforms/pep_headers.py
index 34ae7cfc623..fb76e4f58a4 100644
--- a/pep_sphinx_extensions/pep_processor/transforms/pep_headers.py
+++ b/pep_sphinx_extensions/pep_processor/transforms/pep_headers.py
@@ -1,8 +1,7 @@
-from pathlib import Path
import re
+from pathlib import Path
-from docutils import nodes
-from docutils import transforms
+from docutils import nodes, transforms
from sphinx import errors
from pep_sphinx_extensions.pep_processor.transforms import pep_zero
@@ -44,6 +43,7 @@
"community process, workflow or governance",
}
+
class PEPParsingError(errors.SphinxError):
pass
@@ -63,8 +63,13 @@ def apply(self) -> None:
raise PEPParsingError("Document tree is empty.")
header = self.document[0]
- if not isinstance(header, nodes.field_list) or "rfc2822" not in header["classes"]:
- raise PEPParsingError("Document does not begin with an RFC-2822 header; it is not a PEP.")
+ if (
+ not isinstance(header, nodes.field_list)
+ or "rfc2822" not in header["classes"]
+ ):
+ raise PEPParsingError(
+ "Document does not begin with an RFC-2822 header; it is not a PEP."
+ )
# PEP number should be the first field
pep_field = header[0]
@@ -76,7 +81,9 @@ def apply(self) -> None:
try:
pep_num = int(pep_num_str)
except ValueError:
- raise PEPParsingError(f"PEP header must contain an integer. '{pep_num_str}' is invalid!")
+ raise PEPParsingError(
+ f"PEP header must contain an integer. '{pep_num_str}' is invalid!"
+ )
# Special processing for PEP 0.
if pep_num == 0:
@@ -116,8 +123,7 @@ def apply(self) -> None:
elif name in {"discussions-to", "resolution", "post-history"}:
# Prettify mailing list and Discourse links
for node in para:
- if (not isinstance(node, nodes.reference)
- or not node["refuri"]):
+ if not isinstance(node, nodes.reference) or not node["refuri"]:
continue
# If the Resolution header is already a link, don't prettify it
if name == "resolution" and node["refuri"] != node[0]:
@@ -140,7 +146,10 @@ def apply(self) -> None:
target = self.document.settings.pep_url.format(int(pep_str))
if self.document.settings.builder == "dirhtml":
target = f"../{target}"
- new_body += [nodes.reference("", pep_str, refuri=target), nodes.Text(", ")]
+ new_body += [
+ nodes.reference("", pep_str, refuri=target),
+ nodes.Text(", "),
+ ]
para[:] = new_body[:-1] # drop trailing space
elif name == "topic":
new_body = []
@@ -216,16 +225,14 @@ def _process_list_url(parts: list[str]) -> tuple[str, str]:
# HyperKitty (Mailman3) archive structure is
# https://mail.python.org/archives/list//thread/
if "archives" in parts:
- list_name = (
- parts[parts.index("archives") + 2].removesuffix("@python.org"))
+ list_name = parts[parts.index("archives") + 2].removesuffix("@python.org")
if len(parts) > 6 and parts[6] in {"message", "thread"}:
item_type = parts[6]
# Mailman3 list info structure is
# https://mail.python.org/mailman3/lists/.python.org/
elif "mailman3" in parts:
- list_name = (
- parts[parts.index("mailman3") + 2].removesuffix(".python.org"))
+ list_name = parts[parts.index("mailman3") + 2].removesuffix(".python.org")
# Pipermail (Mailman) archive structure is
# https://mail.python.org/pipermail//-/
@@ -240,8 +247,7 @@ def _process_list_url(parts: list[str]) -> tuple[str, str]:
# Not a link to a mailing list, message or thread
else:
- raise ValueError(
- f"{'/'.join(parts)} not a link to a list, message or thread")
+ raise ValueError(f"{'/'.join(parts)} not a link to a list, message or thread")
return list_name, item_type
@@ -251,7 +257,8 @@ def _process_discourse_url(parts: list[str]) -> tuple[str, str]:
if len(parts) < 5 or ("t" not in parts and "c" not in parts):
raise ValueError(
- f"{'/'.join(parts)} not a link to a Discourse thread or category")
+ f"{'/'.join(parts)} not a link to a Discourse thread or category"
+ )
first_subpart = parts[4]
has_title = not first_subpart.isnumeric()
@@ -279,7 +286,8 @@ def _process_pretty_url(url: str) -> tuple[str, str]:
item_name, item_type = LINK_PRETTIFIERS[parts[2]](parts)
except KeyError as error:
raise ValueError(
- f"{url} not a link to a recognized domain to prettify") from error
+ f"{url} not a link to a recognized domain to prettify"
+ ) from error
item_name = item_name.title().replace("Sig", "SIG").replace("Pep", "PEP")
return item_name, item_type
diff --git a/pep_sphinx_extensions/pep_processor/transforms/pep_references.py b/pep_sphinx_extensions/pep_processor/transforms/pep_references.py
index 1e00e84cb20..cc2840e9208 100644
--- a/pep_sphinx_extensions/pep_processor/transforms/pep_references.py
+++ b/pep_sphinx_extensions/pep_processor/transforms/pep_references.py
@@ -1,7 +1,6 @@
from pathlib import Path
-from docutils import nodes
-from docutils import transforms
+from docutils import nodes, transforms
class PEPReferenceRoleTitleText(transforms.Transform):
diff --git a/pep_sphinx_extensions/pep_processor/transforms/pep_title.py b/pep_sphinx_extensions/pep_processor/transforms/pep_title.py
index de3ce4466a1..8665175d404 100644
--- a/pep_sphinx_extensions/pep_processor/transforms/pep_title.py
+++ b/pep_sphinx_extensions/pep_processor/transforms/pep_title.py
@@ -1,10 +1,7 @@
from pathlib import Path
-from docutils import nodes
-from docutils import transforms
-from docutils import utils
-from docutils.parsers.rst import roles
-from docutils.parsers.rst import states
+from docutils import nodes, transforms, utils
+from docutils.parsers.rst import roles, states
class PEPTitle(transforms.Transform):
@@ -27,7 +24,9 @@ def apply(self) -> None:
for field in self.document[0]:
# Hold details of the attribute's tag against its details
row_attributes = {sub.tagname: sub.rawsource for sub in field}
- pep_header_details[row_attributes["field_name"]] = row_attributes["field_body"]
+ pep_header_details[row_attributes["field_name"]] = row_attributes[
+ "field_body"
+ ]
# Store the redundant fields in the table for removal
if row_attributes["field_name"] in desired_fields:
@@ -40,11 +39,17 @@ def apply(self) -> None:
# Create the title string for the PEP
pep_number = int(pep_header_details["PEP"])
pep_title = pep_header_details["Title"]
- pep_title_string = f"PEP {pep_number} -- {pep_title}" # double hyphen for en dash
+ pep_title_string = (
+ f"PEP {pep_number} -- {pep_title}" # double hyphen for en dash
+ )
# Generate the title section node and its properties
title_nodes = _line_to_nodes(pep_title_string)
- pep_title_node = nodes.section("", nodes.title("", "", *title_nodes, classes=["page-title"]), names=["pep-content"])
+ pep_title_node = nodes.section(
+ "",
+ nodes.title("", "", *title_nodes, classes=["page-title"]),
+ names=["pep-content"],
+ )
# Insert the title node as the root element, move children down
document_children = self.document.children
@@ -60,7 +65,12 @@ def apply(self) -> None:
def _line_to_nodes(text: str) -> list[nodes.Node]:
"""Parse RST string to nodes."""
document = utils.new_document("")
- document.settings.pep_references = document.settings.rfc_references = False # patch settings
- states.RSTStateMachine(state_classes=states.state_classes, initial_state="Body").run([text], document) # do parsing
- roles._roles.pop("", None) # restore the "default" default role after parsing a document
+ # patch settings
+ document.settings.pep_references = document.settings.rfc_references = False
+ # do parsing
+ states.RSTStateMachine(
+ state_classes=states.state_classes, initial_state="Body"
+ ).run([text], document)
+ # restore the "default" default role after parsing a document
+ roles._roles.pop("", None)
return document[0].children
diff --git a/pep_sphinx_extensions/pep_processor/transforms/pep_zero.py b/pep_sphinx_extensions/pep_processor/transforms/pep_zero.py
index 18a34b03090..824067f2f32 100644
--- a/pep_sphinx_extensions/pep_processor/transforms/pep_zero.py
+++ b/pep_sphinx_extensions/pep_processor/transforms/pep_zero.py
@@ -1,7 +1,6 @@
from __future__ import annotations
-from docutils import nodes
-from docutils import transforms
+from docutils import nodes, transforms
class PEPZero(transforms.Transform):
diff --git a/pep_sphinx_extensions/pep_zero_generator/constants.py b/pep_sphinx_extensions/pep_zero_generator/constants.py
index 9ce1c4af555..17062e33703 100644
--- a/pep_sphinx_extensions/pep_zero_generator/constants.py
+++ b/pep_sphinx_extensions/pep_zero_generator/constants.py
@@ -12,8 +12,15 @@
# Valid values for the Status header.
STATUS_VALUES = {
- STATUS_ACCEPTED, STATUS_PROVISIONAL, STATUS_REJECTED, STATUS_WITHDRAWN,
- STATUS_DEFERRED, STATUS_FINAL, STATUS_ACTIVE, STATUS_DRAFT, STATUS_SUPERSEDED,
+ STATUS_ACCEPTED,
+ STATUS_PROVISIONAL,
+ STATUS_REJECTED,
+ STATUS_WITHDRAWN,
+ STATUS_DEFERRED,
+ STATUS_FINAL,
+ STATUS_ACTIVE,
+ STATUS_DRAFT,
+ STATUS_SUPERSEDED,
}
# Map of invalid/special statuses to their valid counterparts
SPECIAL_STATUSES = {
diff --git a/pep_sphinx_extensions/pep_zero_generator/errors.py b/pep_sphinx_extensions/pep_zero_generator/errors.py
index deb12021ac9..a0b8bc291a3 100644
--- a/pep_sphinx_extensions/pep_zero_generator/errors.py
+++ b/pep_sphinx_extensions/pep_zero_generator/errors.py
@@ -10,7 +10,7 @@ def __init__(self, error: str, pep_file: Path, pep_number: int | None = None):
self.number = pep_number
def __str__(self):
- error_msg = super(PEPError, self).__str__()
+ error_msg = super().__str__()
error_msg = f"({self.filename}): {error_msg}"
pep_str = f"PEP {self.number}"
return f"{pep_str} {error_msg}" if self.number is not None else error_msg
diff --git a/pep_sphinx_extensions/pep_zero_generator/parser.py b/pep_sphinx_extensions/pep_zero_generator/parser.py
index 6877d680606..f36b57fcb12 100644
--- a/pep_sphinx_extensions/pep_zero_generator/parser.py
+++ b/pep_sphinx_extensions/pep_zero_generator/parser.py
@@ -7,20 +7,23 @@
from email.parser import HeaderParser
from pathlib import Path
-from pep_sphinx_extensions.pep_zero_generator.constants import ACTIVE_ALLOWED
-from pep_sphinx_extensions.pep_zero_generator.constants import HIDE_STATUS
-from pep_sphinx_extensions.pep_zero_generator.constants import SPECIAL_STATUSES
-from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_ACTIVE
-from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_PROVISIONAL
-from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_VALUES
-from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_STANDARDS
-from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_VALUES
+from pep_sphinx_extensions.pep_zero_generator.constants import (
+ ACTIVE_ALLOWED,
+ HIDE_STATUS,
+ SPECIAL_STATUSES,
+ STATUS_ACTIVE,
+ STATUS_PROVISIONAL,
+ STATUS_VALUES,
+ TYPE_STANDARDS,
+ TYPE_VALUES,
+)
from pep_sphinx_extensions.pep_zero_generator.errors import PEPError
@dataclasses.dataclass(order=True, frozen=True)
class _Author:
"""Represent PEP authors."""
+
full_name: str # The author's name.
email: str # The author's email address.
@@ -53,7 +56,9 @@ def __init__(self, filename: Path):
metadata = HeaderParser().parsestr(pep_text)
required_header_misses = PEP.required_headers - set(metadata.keys())
if required_header_misses:
- _raise_pep_error(self, f"PEP is missing required headers {required_header_misses}")
+ _raise_pep_error(
+ self, f"PEP is missing required headers {required_header_misses}"
+ )
try:
self.number = int(metadata["PEP"])
@@ -62,7 +67,9 @@ def __init__(self, filename: Path):
# Check PEP number matches filename
if self.number != int(filename.stem[4:]):
- _raise_pep_error(self, f"PEP number does not match file name ({filename})", pep_num=True)
+ _raise_pep_error(
+ self, f"PEP number does not match file name ({filename})", pep_num=True
+ )
# Title
self.title: str = metadata["Title"]
@@ -70,14 +77,18 @@ def __init__(self, filename: Path):
# Type
self.pep_type: str = metadata["Type"]
if self.pep_type not in TYPE_VALUES:
- _raise_pep_error(self, f"{self.pep_type} is not a valid Type value", pep_num=True)
+ _raise_pep_error(
+ self, f"{self.pep_type} is not a valid Type value", pep_num=True
+ )
# Status
status = metadata["Status"]
if status in SPECIAL_STATUSES:
status = SPECIAL_STATUSES[status]
if status not in STATUS_VALUES:
- _raise_pep_error(self, f"{status} is not a valid Status value", pep_num=True)
+ _raise_pep_error(
+ self, f"{status} is not a valid Status value", pep_num=True
+ )
# Special case for Active PEPs.
if status == STATUS_ACTIVE and self.pep_type not in ACTIVE_ALLOWED:
@@ -97,7 +108,9 @@ def __init__(self, filename: Path):
# 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())}
+ self.topic: set[str] = {
+ topic for topic_raw in _topic if (topic := topic_raw.strip())
+ }
# Other headers
self.created = metadata["Created"]
@@ -187,11 +200,14 @@ def _parse_author(data: str) -> list[_Author]:
"""Return a list of author names and emails."""
author_list = []
- data = (data.replace("\n", " ")
- .replace(", Jr", jr_placeholder)
- .rstrip().removesuffix(","))
+ data = (
+ data.replace("\n", " ")
+ .replace(", Jr", jr_placeholder)
+ .rstrip()
+ .removesuffix(",")
+ )
for author_email in data.split(", "):
- if ' <' in author_email:
+ if " <" in author_email:
author, email = author_email.removesuffix(">").split(" <")
else:
author, email = author_email, ""
diff --git a/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py b/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py
index a9f21d11940..8b70b10055d 100644
--- a/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py
+++ b/pep_sphinx_extensions/pep_zero_generator/pep_index_generator.py
@@ -15,6 +15,7 @@
to allow it to be processed as normal.
"""
+
from __future__ import annotations
import json
@@ -22,11 +23,13 @@
from pathlib import Path
from typing import TYPE_CHECKING
-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
+from pep_sphinx_extensions.pep_zero_generator import parser, subindices, writer
from pep_sphinx_extensions.pep_zero_generator.constants import SUBINDICES_BY_TOPIC
-from release_management.serialize import create_release_cycle, create_release_schedule_calendar, create_release_json
+from release_management.serialize import (
+ create_release_cycle,
+ create_release_json,
+ create_release_schedule_calendar,
+)
if TYPE_CHECKING:
from sphinx.application import Sphinx
@@ -81,14 +84,14 @@ def create_pep_zero(app: Sphinx, env: BuildEnvironment, docnames: list[str]) ->
release_peps = build_release_peps(peps)
- numerical_index_text = writer.PEPZeroWriter(
- release_peps
- ).write_numerical_index(peps)
+ numerical_index_text = writer.PEPZeroWriter(release_peps).write_numerical_index(
+ peps
+ )
subindices.update_sphinx("numerical", numerical_index_text, docnames, env)
- pep0_text = writer.PEPZeroWriter(
- release_peps
- ).write_pep0(peps, builder=env.settings["builder"])
+ pep0_text = writer.PEPZeroWriter(release_peps).write_pep0(
+ peps, builder=env.settings["builder"]
+ )
pep0_path = subindices.update_sphinx("pep-0000", pep0_text, docnames, env)
peps.append(parser.PEP(pep0_path))
@@ -103,10 +106,16 @@ def create_pep_zero(app: Sphinx, env: BuildEnvironment, docnames: list[str]) ->
write_peps_json(peps, Path(app.outdir))
release_cycle = create_release_cycle()
- app.outdir.joinpath('api/release-cycle.json').write_text(release_cycle, encoding="utf-8")
+ app.outdir.joinpath("api/release-cycle.json").write_text(
+ release_cycle, encoding="utf-8"
+ )
release_json = create_release_json()
- app.outdir.joinpath('api/python-releases.json').write_text(release_json, encoding="utf-8")
+ app.outdir.joinpath("api/python-releases.json").write_text(
+ release_json, encoding="utf-8"
+ )
release_ical = create_release_schedule_calendar()
- app.outdir.joinpath('release-schedule.ics').write_text(release_ical, encoding="utf-8")
+ app.outdir.joinpath("release-schedule.ics").write_text(
+ release_ical, encoding="utf-8"
+ )
diff --git a/pep_sphinx_extensions/pep_zero_generator/subindices.py b/pep_sphinx_extensions/pep_zero_generator/subindices.py
index fdf167d6049..b6d7c9d9eca 100644
--- a/pep_sphinx_extensions/pep_zero_generator/subindices.py
+++ b/pep_sphinx_extensions/pep_zero_generator/subindices.py
@@ -14,7 +14,9 @@
from pep_sphinx_extensions.pep_zero_generator.parser import PEP
-def update_sphinx(filename: str, text: str, docnames: list[str], env: BuildEnvironment) -> Path:
+def update_sphinx(
+ filename: str, text: str, docnames: list[str], env: BuildEnvironment
+) -> Path:
file_path = Path(env.srcdir, f"{filename}.rst")
# Only write and schedule for rebuild if content actually changed
try:
@@ -69,7 +71,9 @@ def generate_subindices(
def generate_topic_contents(docnames: list[str], env: BuildEnvironment):
- update_sphinx("topic/index", """\
+ update_sphinx(
+ "topic/index",
+ """\
.. _topic-index:
Topic Index
@@ -83,4 +87,7 @@ def generate_topic_contents(docnames: list[str], env: BuildEnvironment):
:glob:
*
-""", docnames, env)
+""",
+ docnames,
+ env,
+ )
diff --git a/pep_sphinx_extensions/pep_zero_generator/writer.py b/pep_sphinx_extensions/pep_zero_generator/writer.py
index a5033149bb5..037ab7d878e 100644
--- a/pep_sphinx_extensions/pep_zero_generator/writer.py
+++ b/pep_sphinx_extensions/pep_zero_generator/writer.py
@@ -2,25 +2,29 @@
from __future__ import annotations
-from typing import TYPE_CHECKING
import unicodedata
+from typing import TYPE_CHECKING
-from pep_sphinx_extensions.pep_processor.transforms.pep_headers import ABBREVIATED_STATUSES
-from pep_sphinx_extensions.pep_processor.transforms.pep_headers import ABBREVIATED_TYPES
-from pep_sphinx_extensions.pep_zero_generator.constants import DEAD_STATUSES
-from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_ACCEPTED
-from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_ACTIVE
-from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_DEFERRED
-from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_DRAFT
-from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_FINAL
-from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_PROVISIONAL
-from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_REJECTED
-from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_VALUES
-from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_WITHDRAWN
-from pep_sphinx_extensions.pep_zero_generator.constants import SUBINDICES_BY_TOPIC
-from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_INFO
-from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_PROCESS
-from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_VALUES
+from pep_sphinx_extensions.pep_processor.transforms.pep_headers import (
+ ABBREVIATED_STATUSES,
+ ABBREVIATED_TYPES,
+)
+from pep_sphinx_extensions.pep_zero_generator.constants import (
+ DEAD_STATUSES,
+ STATUS_ACCEPTED,
+ STATUS_ACTIVE,
+ STATUS_DEFERRED,
+ STATUS_DRAFT,
+ STATUS_FINAL,
+ STATUS_PROVISIONAL,
+ STATUS_REJECTED,
+ STATUS_VALUES,
+ STATUS_WITHDRAWN,
+ SUBINDICES_BY_TOPIC,
+ TYPE_INFO,
+ TYPE_PROCESS,
+ TYPE_VALUES,
+)
from pep_sphinx_extensions.pep_zero_generator.errors import PEPError
if TYPE_CHECKING:
@@ -210,11 +214,24 @@ def write_pep0(
# PEPs 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 = [
("Process and Meta-PEPs", meta),
("Other Informational PEPs", info),
- ("Provisional PEPs (provisionally accepted; interface may still change)", provisional),
+ (
+ "Provisional PEPs (provisionally accepted; interface may still change)",
+ provisional,
+ ),
("Accepted PEPs (accepted; may not be implemented yet)", accepted),
("Open PEPs (under consideration)", open_),
("Finished PEPs (done, with a stable interface)", finished),
@@ -222,7 +239,7 @@ def write_pep0(
("Deferred PEPs (postponed pending further research or updates)", deferred),
("Rejected, Superseded, and Withdrawn PEPs", dead),
]
- for (category, peps_in_category) in pep_categories:
+ for category, peps_in_category in pep_categories:
# For sub-indices, only emit categories with entries.
# For PEP 0, emit every category, but only with a table when it has entries.
if len(peps_in_category) > 0:
@@ -285,7 +302,9 @@ def write_pep0(
for author_name in _sort_authors(authors_dict):
# Use the email from authors_dict instead of the one from "author" as
# the author instance may have an empty email.
- self.emit_text(f"{author_name:{max_name_len}} {authors_dict[author_name]}")
+ self.emit_text(
+ f"{author_name:{max_name_len}} {authors_dict[author_name]}"
+ )
self.emit_author_table_separator(max_name_len)
self.emit_newline()
self.emit_newline()
@@ -326,7 +345,10 @@ def _classify_peps(peps: list[PEP]) -> tuple[list[PEP], ...]:
# Hack until the conflict between the use of "Final"
# for both API definition PEPs and other (actually
# obsolete) PEPs is addressed
- if pep.status == STATUS_ACTIVE or "release schedule" not in pep.title.lower():
+ if (
+ pep.status == STATUS_ACTIVE
+ or "release schedule" not in pep.title.lower()
+ ):
info.append(pep)
else:
historical.append(pep)
@@ -337,8 +359,20 @@ def _classify_peps(peps: list[PEP]) -> tuple[list[PEP], ...]:
elif pep.status == STATUS_FINAL:
finished.append(pep)
else:
- raise PEPError(f"Unsorted ({pep.pep_type}/{pep.status})", pep.filename, pep.number)
- return meta, info, provisional, accepted, open_, finished, historical, deferred, dead
+ raise PEPError(
+ f"Unsorted ({pep.pep_type}/{pep.status})", pep.filename, pep.number
+ )
+ return (
+ meta,
+ info,
+ provisional,
+ accepted,
+ open_,
+ finished,
+ historical,
+ deferred,
+ dead,
+ )
def _verify_email_addresses(peps: list[PEP]) -> dict[str, str]:
@@ -360,7 +394,7 @@ def _verify_email_addresses(peps: list[PEP]) -> dict[str, str]:
# Combine multiple email addresses with commas. Since peps is
# sorted by PEP number, this should produce a deterministic
# output.
- return {name: ', '.join(emails) for name, emails in authors_dict.items()}
+ return {name: ", ".join(emails) for name, emails in authors_dict.items()}
def _sort_authors(authors_dict: dict[str, str]) -> list[str]:
diff --git a/peps/conf.py b/peps/conf.py
index 6daec648954..a8e986869ab 100644
--- a/peps/conf.py
+++ b/peps/conf.py
@@ -4,8 +4,8 @@
"""Configuration for building PEPs using Sphinx."""
import os
-from pathlib import Path
import sys
+from pathlib import Path
_ROOT = Path(__file__).resolve().parent.parent
sys.path.append(os.fspath(_ROOT))
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000000..eafd3ef7f2c
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,16 @@
+[tool.ruff]
+target-version = "py311"
+fix = true
+output-format = "full"
+lint.select = [
+ "E", # pycodestyle errors
+ "F", # pyflakes
+ "I", # isort
+ "PT", # flake8-pytest-style
+ "UP", # pyupgrade
+ "W", # pycodestyle warnings
+ "YTT", # flake8-2020
+]
+lint.ignore = [
+ "E501", # Line too long
+]
diff --git a/release_management/.ruff.toml b/release_management/.ruff.toml
deleted file mode 100644
index 2aeb6ec8468..00000000000
--- a/release_management/.ruff.toml
+++ /dev/null
@@ -1,5 +0,0 @@
-extend = "../.ruff.toml" # Inherit the project-wide settings
-
-[format]
-preview = true
-quote-style = "single"
diff --git a/release_management/__init__.py b/release_management/__init__.py
index 9846219fdc0..84e89493131 100644
--- a/release_management/__init__.py
+++ b/release_management/__init__.py
@@ -14,17 +14,17 @@
import datetime as dt
from typing import Literal, TypeAlias
- ReleaseState: TypeAlias = Literal['actual', 'expected']
- ReleaseSchedules: TypeAlias = dict[tuple[str, ReleaseState], list['ReleaseInfo']]
+ ReleaseState: TypeAlias = Literal["actual", "expected"]
+ ReleaseSchedules: TypeAlias = dict[tuple[str, ReleaseState], list["ReleaseInfo"]]
VersionStatus: TypeAlias = Literal[
- 'feature', 'prerelease', 'bugfix', 'security', 'end-of-life'
+ "feature", "prerelease", "bugfix", "security", "end-of-life"
]
RELEASE_DIR = Path(__file__).resolve().parent
ROOT_DIR = RELEASE_DIR.parent
-PEP_ROOT = ROOT_DIR / 'peps'
+PEP_ROOT = ROOT_DIR / "peps"
-dc_kw = {'kw_only': True, 'slots': True} if sys.version_info[:2] >= (3, 10) else {}
+dc_kw = {"kw_only": True, "slots": True} if sys.version_info[:2] >= (3, 10) else {}
@dataclass(frozen=True, **dc_kw)
@@ -49,7 +49,7 @@ class VersionMetadata:
@classmethod
def from_toml(cls, data: dict[str, int | str | dt.date]):
- return cls(**{k.replace('-', '_'): v for k, v in data.items()})
+ return cls(**{k.replace("-", "_"): v for k, v in data.items()})
@dataclass(frozen=True, **dc_kw)
@@ -59,23 +59,23 @@ class ReleaseInfo:
stage: str
state: ReleaseState
date: dt.date
- note: str = '' # optional note / comment, displayed in the schedule
+ note: str = "" # optional note / comment, displayed in the schedule
@property
def schedule_bullet(self):
"""Return a formatted bullet point for the schedule list."""
- return f'- {self.stage}: {self.date:%A, %Y-%m-%d}'
+ return f"- {self.stage}: {self.date:%A, %Y-%m-%d}"
def load_python_releases() -> PythonReleases:
- with open(RELEASE_DIR / 'python-releases.toml', 'rb') as f:
+ with open(RELEASE_DIR / "python-releases.toml", "rb") as f:
python_releases = tomllib.load(f)
all_metadata = {
v: VersionMetadata.from_toml(metadata)
- for v, metadata in python_releases['metadata'].items()
+ for v, metadata in python_releases["metadata"].items()
}
all_releases = {
v: [ReleaseInfo(**r) for r in releases]
- for v, releases in python_releases['release'].items()
+ for v, releases in python_releases["release"].items()
}
return PythonReleases(metadata=all_metadata, releases=all_releases)
diff --git a/release_management/__main__.py b/release_management/__main__.py
index 458a00ef90d..55e3fe2193f 100644
--- a/release_management/__main__.py
+++ b/release_management/__main__.py
@@ -3,13 +3,13 @@
import argparse
commands = (
- CMD_FULL_JSON := 'full-json',
- CMD_UPDATE_PEPS := 'update-peps',
- CMD_RELEASE_CYCLE := 'release-cycle',
- CMD_CALENDAR := 'calendar',
+ CMD_FULL_JSON := "full-json",
+ CMD_UPDATE_PEPS := "update-peps",
+ CMD_RELEASE_CYCLE := "release-cycle",
+ CMD_CALENDAR := "calendar",
)
parser = argparse.ArgumentParser(allow_abbrev=False)
-parser.add_argument('COMMAND', choices=commands)
+parser.add_argument("COMMAND", choices=commands)
args = parser.parse_args()
if args.COMMAND == CMD_UPDATE_PEPS:
@@ -21,22 +21,22 @@
from release_management import ROOT_DIR
from release_management.serialize import create_release_json
- json_path = ROOT_DIR / 'python-releases.json'
- json_path.write_text(create_release_json(), encoding='utf-8')
+ json_path = ROOT_DIR / "python-releases.json"
+ json_path.write_text(create_release_json(), encoding="utf-8")
raise SystemExit(0)
if args.COMMAND == CMD_RELEASE_CYCLE:
from release_management import ROOT_DIR
from release_management.serialize import create_release_cycle
- json_path = ROOT_DIR / 'release-cycle.json'
- json_path.write_text(create_release_cycle(), encoding='utf-8')
+ json_path = ROOT_DIR / "release-cycle.json"
+ json_path.write_text(create_release_cycle(), encoding="utf-8")
raise SystemExit(0)
if args.COMMAND == CMD_CALENDAR:
from release_management import ROOT_DIR
from release_management.serialize import create_release_schedule_calendar
- calendar_path = ROOT_DIR / 'release-schedule.ics'
- calendar_path.write_text(create_release_schedule_calendar(), encoding='utf-8')
+ calendar_path = ROOT_DIR / "release-schedule.ics"
+ calendar_path.write_text(create_release_schedule_calendar(), encoding="utf-8")
raise SystemExit(0)
diff --git a/release_management/serialize.py b/release_management/serialize.py
index 051ed77c3ec..950fef02679 100644
--- a/release_management/serialize.py
+++ b/release_management/serialize.py
@@ -1,10 +1,10 @@
from __future__ import annotations
-import datetime as dt
import dataclasses
+import datetime as dt
import json
-from release_management import ROOT_DIR, load_python_releases
+from release_management import load_python_releases
TYPE_CHECKING = False
if TYPE_CHECKING:
@@ -15,12 +15,14 @@
SEVEN_YEARS_AGO = TODAY.replace(year=TODAY.year - 7)
# https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.11
-CALENDAR_ESCAPE_TEXT = str.maketrans({
- '\\': r'\\',
- ';': r'\;',
- ',': r'\,',
- '\n': r'\n',
-})
+CALENDAR_ESCAPE_TEXT = str.maketrans(
+ {
+ "\\": r"\\",
+ ";": r"\;",
+ ",": r"\,",
+ "\n": r"\n",
+ }
+)
def create_release_json() -> str:
@@ -42,24 +44,24 @@ def create_release_cycle() -> str:
versions = [v for _date, v in all_versions if version_to_tuple(v) >= (2, 6)]
release_cycle = {version: version_info(metadata[version]) for version in versions}
rc_json = json.dumps(release_cycle, indent=2, sort_keys=False, ensure_ascii=False)
- return f'{rc_json}\n'
+ return f"{rc_json}\n"
def version_to_tuple(version: str, /) -> tuple[int, ...]:
- return tuple(map(int, version.split('.')))
+ return tuple(map(int, version.split(".")))
def version_info(metadata: VersionMetadata, /) -> dict[str, str | int]:
end_of_life = metadata.end_of_life.isoformat()
- if metadata.status != 'end-of-life':
- end_of_life = end_of_life.removesuffix('-01')
+ if metadata.status != "end-of-life":
+ end_of_life = end_of_life.removesuffix("-01")
return {
- 'branch': metadata.branch,
- 'pep': metadata.pep,
- 'status': metadata.status,
- 'first_release': metadata.first_release.isoformat(),
- 'end_of_life': end_of_life,
- 'release_manager': metadata.release_manager,
+ "branch": metadata.branch,
+ "pep": metadata.pep,
+ "status": metadata.status,
+ "first_release": metadata.first_release.isoformat(),
+ "end_of_life": end_of_life,
+ "release_manager": metadata.release_manager,
}
@@ -75,41 +77,41 @@ def create_release_schedule_calendar() -> str:
releases.append((pep_number, release))
releases.sort(key=lambda r: r[1].date)
lines = release_schedule_calendar_lines(releases)
- return '\r\n'.join(lines)
+ return "\r\n".join(lines)
def release_schedule_calendar_lines(
releases: list[tuple[int, ReleaseInfo]], /
) -> list[str]:
- dtstamp = dt.datetime.now(dt.timezone.utc).strftime('%Y%m%dT%H%M%SZ')
+ dtstamp = dt.datetime.now(dt.UTC).strftime("%Y%m%dT%H%M%SZ")
lines = [
- 'BEGIN:VCALENDAR',
- 'VERSION:2.0',
- 'PRODID:-//Python Software Foundation//Python release schedule//EN',
- 'X-WR-CALDESC:Python releases schedule from https://peps.python.org',
- 'X-WR-CALNAME:Python releases schedule',
+ "BEGIN:VCALENDAR",
+ "VERSION:2.0",
+ "PRODID:-//Python Software Foundation//Python release schedule//EN",
+ "X-WR-CALDESC:Python releases schedule from https://peps.python.org",
+ "X-WR-CALNAME:Python releases schedule",
]
for pep_number, release in releases:
- normalised_stage = release.stage.replace(' ', '')
+ normalised_stage = release.stage.replace(" ", "")
normalised_stage = normalised_stage.translate(CALENDAR_ESCAPE_TEXT)
if release.note:
normalised_note = release.note.translate(CALENDAR_ESCAPE_TEXT)
- note = (f'DESCRIPTION:Note: {normalised_note}',)
+ note = (f"DESCRIPTION:Note: {normalised_note}",)
else:
note = ()
lines += (
- 'BEGIN:VEVENT',
- f'DTSTAMP:{dtstamp}',
- f'UID:python-{normalised_stage}@releases.python.org',
+ "BEGIN:VEVENT",
+ f"DTSTAMP:{dtstamp}",
+ f"UID:python-{normalised_stage}@releases.python.org",
f'DTSTART;VALUE=DATE:{release.date.strftime("%Y%m%d")}',
- f'SUMMARY:Python {release.stage}',
+ f"SUMMARY:Python {release.stage}",
*note,
- f'URL:https://peps.python.org/pep-{pep_number:04d}/',
- 'END:VEVENT',
+ f"URL:https://peps.python.org/pep-{pep_number:04d}/",
+ "END:VEVENT",
)
lines += (
- 'END:VCALENDAR',
- '',
+ "END:VCALENDAR",
+ "",
)
return lines
diff --git a/release_management/tests/test_release_schedule_calendar.py b/release_management/tests/test_release_schedule_calendar.py
index 0d1fcff61bf..7d8a3b0abb1 100644
--- a/release_management/tests/test_release_schedule_calendar.py
+++ b/release_management/tests/test_release_schedule_calendar.py
@@ -3,30 +3,30 @@
from release_management import ReleaseInfo, serialize
FAKE_RELEASE = ReleaseInfo(
- stage='X.Y.Z final',
- state='actual',
+ stage="X.Y.Z final",
+ state="actual",
date=dt.date(2000, 1, 1),
- note='These characters need escaping: \\ , ; \n',
+ note="These characters need escaping: \\ , ; \n",
)
def test_create_release_calendar_has_calendar_metadata() -> None:
# Act
- cal_lines = serialize.create_release_schedule_calendar().split('\r\n')
+ cal_lines = serialize.create_release_schedule_calendar().split("\r\n")
# Assert
# Check calendar metadata
assert cal_lines[:5] == [
- 'BEGIN:VCALENDAR',
- 'VERSION:2.0',
- 'PRODID:-//Python Software Foundation//Python release schedule//EN',
- 'X-WR-CALDESC:Python releases schedule from https://peps.python.org',
- 'X-WR-CALNAME:Python releases schedule',
+ "BEGIN:VCALENDAR",
+ "VERSION:2.0",
+ "PRODID:-//Python Software Foundation//Python release schedule//EN",
+ "X-WR-CALDESC:Python releases schedule from https://peps.python.org",
+ "X-WR-CALNAME:Python releases schedule",
]
assert cal_lines[-2:] == [
- 'END:VCALENDAR',
- '',
+ "END:VCALENDAR",
+ "",
]
@@ -36,14 +36,14 @@ def test_create_release_calendar_first_event() -> None:
cal_lines = serialize.release_schedule_calendar_lines(releases)
# Assert
- assert cal_lines[5] == 'BEGIN:VEVENT'
- assert cal_lines[6].startswith('DTSTAMP:')
- assert cal_lines[6].endswith('Z')
- assert cal_lines[7] == 'UID:python-X.Y.Zfinal@releases.python.org'
- assert cal_lines[8] == 'DTSTART;VALUE=DATE:20000101'
- assert cal_lines[9] == 'SUMMARY:Python X.Y.Z final'
+ assert cal_lines[5] == "BEGIN:VEVENT"
+ assert cal_lines[6].startswith("DTSTAMP:")
+ assert cal_lines[6].endswith("Z")
+ assert cal_lines[7] == "UID:python-X.Y.Zfinal@releases.python.org"
+ assert cal_lines[8] == "DTSTART;VALUE=DATE:20000101"
+ assert cal_lines[9] == "SUMMARY:Python X.Y.Z final"
assert cal_lines[10] == (
- 'DESCRIPTION:Note: These characters need escaping: \\\\ \\, \\; \\n'
+ "DESCRIPTION:Note: These characters need escaping: \\\\ \\, \\; \\n"
)
- assert cal_lines[11] == 'URL:https://peps.python.org/pep-9999/'
- assert cal_lines[12] == 'END:VEVENT'
+ assert cal_lines[11] == "URL:https://peps.python.org/pep-9999/"
+ assert cal_lines[12] == "END:VEVENT"
diff --git a/release_management/update_release_schedules.py b/release_management/update_release_schedules.py
index aeb33583dac..4128d61a4de 100644
--- a/release_management/update_release_schedules.py
+++ b/release_management/update_release_schedules.py
@@ -42,25 +42,27 @@
TODAY = dt.date.today()
-SKIPPED_VERSIONS = frozenset({
- '1.6',
- '2.0',
- '2.1',
- '2.2',
- '2.3',
- '2.4',
- '2.5',
- '2.6',
- '2.7',
- '3.0',
- '3.1',
- '3.2',
- '3.3',
- '3.4',
- '3.5',
- '3.6',
- '3.7',
-})
+SKIPPED_VERSIONS = frozenset(
+ {
+ "1.6",
+ "2.0",
+ "2.1",
+ "2.2",
+ "2.3",
+ "2.4",
+ "2.5",
+ "2.6",
+ "2.7",
+ "3.0",
+ "3.1",
+ "3.2",
+ "3.3",
+ "3.4",
+ "3.5",
+ "3.6",
+ "3.7",
+ }
+)
def update_peps() -> None:
@@ -84,46 +86,46 @@ def create_schedules(
bugfix_ends: dt.date,
) -> ReleaseSchedules:
schedules: ReleaseSchedules = {
- ('feature', 'actual'): [],
- ('feature', 'expected'): [],
- ('bugfix', 'actual'): [],
- ('bugfix', 'expected'): [],
- ('security', 'actual'): [],
+ ("feature", "actual"): [],
+ ("feature", "expected"): [],
+ ("bugfix", "actual"): [],
+ ("bugfix", "expected"): [],
+ ("security", "actual"): [],
}
# first entry into the dictionary
- db_state: ReleaseState = 'actual' if TODAY >= start_of_development else 'expected'
- schedules['feature', db_state].append(
+ db_state: ReleaseState = "actual" if TODAY >= start_of_development else "expected"
+ schedules["feature", db_state].append(
ReleaseInfo(
- stage=f'{version} development begins',
+ stage=f"{version} development begins",
state=db_state,
date=start_of_development,
)
)
for release_info in releases:
- if release_info.stage.startswith(f'{version}.0'):
- schedules['feature', release_info.state].append(release_info)
+ if release_info.stage.startswith(f"{version}.0"):
+ schedules["feature", release_info.state].append(release_info)
elif release_info.date <= bugfix_ends:
- schedules['bugfix', release_info.state].append(release_info)
+ schedules["bugfix", release_info.state].append(release_info)
else:
- assert release_info.state == 'actual', release_info
- schedules['security', release_info.state].append(release_info)
+ assert release_info.state == "actual", release_info
+ schedules["security", release_info.state].append(release_info)
return schedules
def update_pep(metadata: VersionMetadata, schedules: ReleaseSchedules) -> None:
- pep_path = PEP_ROOT.joinpath(f'pep-{metadata.pep:0>4}.rst')
- pep_lines = iter(pep_path.read_text(encoding='utf-8').splitlines())
+ pep_path = PEP_ROOT.joinpath(f"pep-{metadata.pep:0>4}.rst")
+ pep_lines = iter(pep_path.read_text(encoding="utf-8").splitlines())
output_lines: list[str] = []
- schedule_name = ''
+ schedule_name = ""
for line in pep_lines:
output_lines.append(line)
- if line.startswith('.. ') and 'schedule' in line:
- assert line.startswith('.. release schedule: ')
- schedule_name = line.removeprefix('.. release schedule: ')
- assert schedule_name in {'feature', 'bugfix', 'security'}
+ if line.startswith(".. ") and "schedule" in line:
+ assert line.startswith(".. release schedule: ")
+ schedule_name = line.removeprefix(".. release schedule: ")
+ assert schedule_name in {"feature", "bugfix", "security"}
output_lines += generate_schedule_lists(
schedules,
schedule_name=schedule_name,
@@ -133,18 +135,18 @@ def update_pep(metadata: VersionMetadata, schedules: ReleaseSchedules) -> None:
# skip source lines until the end of schedule marker
while True:
line = next(pep_lines, None)
- if line == '.. release schedule: ends':
+ if line == ".. release schedule: ends":
output_lines.append(line)
break
if line is None:
- raise ValueError('No end of schedule marker found!')
+ raise ValueError("No end of schedule marker found!")
if not schedule_name:
- raise ValueError('No schedule markers found!')
+ raise ValueError("No schedule markers found!")
- output_lines.append('') # trailing newline
- with open(pep_path, 'wb') as f:
- f.write(b'\n'.join(line.encode('utf-8') for line in output_lines))
+ output_lines.append("") # trailing newline
+ with open(pep_path, "wb") as f:
+ f.write(b"\n".join(line.encode("utf-8") for line in output_lines))
def generate_schedule_lists(
@@ -154,26 +156,26 @@ def generate_schedule_lists(
feature_freeze_date: dt.date = dt.date.min,
) -> Iterator[str]:
state: ReleaseState
- for state in 'actual', 'expected':
+ for state in "actual", "expected":
if not schedules.get((schedule_name, state)):
continue
- yield ''
- if schedule_name != 'security':
- yield f'{state.title()}:'
- yield ''
+ yield ""
+ if schedule_name != "security":
+ yield f"{state.title()}:"
+ yield ""
for release_info in schedules[schedule_name, state]:
yield release_info.schedule_bullet
if release_info.note:
- yield f' ({release_info.note})'
+ yield f" ({release_info.note})"
if release_info.date == feature_freeze_date:
- yield ' (No new features beyond this point.)'
+ yield " (No new features beyond this point.)"
- if schedule_name == 'bugfix':
- yield ' (Final regular bugfix release with binary installers)'
+ if schedule_name == "bugfix":
+ yield " (Final regular bugfix release with binary installers)"
- yield ''
+ yield ""
-if __name__ == '__main__':
+if __name__ == "__main__":
update_peps()