Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@
'c_annotations',
'changes',
'glossary_search',
'grammar_snippet',
'lexers',
'pydoc_topics',
'pyspecific',
'sphinx.ext.coverage',
'sphinx.ext.doctest',
'sphinx.ext.extlinks',
'grammar_snippet',
]

# Skip if downstream redistributors haven't installed them
Expand Down
60 changes: 43 additions & 17 deletions Doc/tools/extensions/grammar_snippet.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
"""Support for documenting Python's grammar."""

from __future__ import annotations

import re
from typing import TYPE_CHECKING

from docutils import nodes
from docutils.parsers.rst import directives
Expand All @@ -7,21 +12,38 @@
from sphinx.util.docutils import SphinxDirective
from sphinx.util.nodes import make_id

if TYPE_CHECKING:
from collections.abc import Sequence
from typing import Any

from docutils.nodes import Node
from sphinx.application import Sphinx
from sphinx.util.typing import ExtensionMetadata

class SnippetStringNode(nodes.inline):

class snippet_string_node(nodes.inline): # noqa: N801 (snake_case is fine)
"""Node for a string literal in a grammar snippet."""

def __init__(self) -> None:
def __init__(
self,
rawsource: str = '',
text: str = '',
*children: Node,
**attributes: Any,
) -> None:
super().__init__(rawsource, text, *children, **attributes)
# Use the Pygments highlight class for `Literal.String.Other`
super().__init__(classes=['sx'])
self['classes'].append('sx')


class GrammarSnippetBase(SphinxDirective):
"""Common functionality for GrammarSnippetDirective & CompatProductionList."""

# The option/argument handling is left to the individual classes.

def make_grammar_snippet(self, options, content):
def make_grammar_snippet(
self, options: dict[str, Any], content: Sequence[str]
) -> list[nodes.paragraph]:
"""Create a literal block from options & content."""

group_name = options['group']
Expand Down Expand Up @@ -65,22 +87,20 @@ def make_grammar_snippet(self, options, content):
last_pos = match.end()

# Handle matches
groupdict = {
group_dict = {
name: content
for name, content in match.groupdict().items()
if content is not None
}
match groupdict:
match group_dict:
case {'rule_name': name}:
literal += self.make_link_target_for_token(
group_name, name
)
case {'rule_ref': ref_text}:
literal += token_xrefs(ref_text, group_name)
case {'single_quoted': name} | {'double_quoted': name}:
string_node = SnippetStringNode()
string_node += nodes.Text(name)
literal += string_node
literal += snippet_string_node('', name)
case _:
raise ValueError('unhandled match')
literal += nodes.Text(line[last_pos:] + '\n')
Expand All @@ -93,8 +113,10 @@ def make_grammar_snippet(self, options, content):

return [node]

def make_link_target_for_token(self, group_name, name):
"""Return a literal node which is a link target for the given token"""
def make_link_target_for_token(
self, group_name: str, name: str
) -> addnodes.literal_strong:
"""Return a literal node which is a link target for the given token."""
name_node = addnodes.literal_strong()

# Cargo-culted magic to make `name_node` a link target
Expand Down Expand Up @@ -138,20 +160,20 @@ class GrammarSnippetDirective(GrammarSnippetBase):

has_content = True
option_spec = {
'group': directives.unchanged,
'group': directives.unchanged_required,
}

# We currently ignore arguments.
required_arguments = 0
optional_arguments = 1
final_argument_whitespace = True

def run(self):
def run(self) -> list[nodes.paragraph]:
return self.make_grammar_snippet(self.options, self.content)


class CompatProductionList(GrammarSnippetBase):
"""Create grammar snippets from ReST productionlist syntax
"""Create grammar snippets from reST productionlist syntax

This is intended to be a transitional directive, used while we switch
from productionlist to grammar-snippet.
Expand All @@ -165,7 +187,7 @@ class CompatProductionList(GrammarSnippetBase):
final_argument_whitespace = True
option_spec = {}

def run(self):
def run(self) -> list[nodes.paragraph]:
# The "content" of a productionlist is actually the first and only
# argument. The first line is the group; the rest is the content lines.
lines = self.arguments[0].splitlines()
Expand All @@ -185,9 +207,13 @@ def run(self):
return self.make_grammar_snippet(options, content)


def setup(app):
def setup(app: Sphinx) -> ExtensionMetadata:
app.add_directive('grammar-snippet', GrammarSnippetDirective)
app.add_directive_to_domain(
'std', 'productionlist', CompatProductionList, override=True
)
return {'version': '1.0', 'parallel_read_safe': True}
return {
'version': '1.0',
'parallel_read_safe': True,
'parallel_write_safe': True,
}
Loading