1+ """Support for documenting Python's grammar."""
2+
3+ from __future__ import annotations
4+
15import re
6+ from typing import TYPE_CHECKING
27
38from docutils import nodes
49from docutils .parsers .rst import directives
712from sphinx .util .docutils import SphinxDirective
813from sphinx .util .nodes import make_id
914
15+ if TYPE_CHECKING :
16+ from collections .abc import Sequence
17+ from typing import Any
18+
19+ from docutils .nodes import Node
20+ from sphinx .application import Sphinx
21+ from sphinx .util .typing import ExtensionMetadata
1022
11- class SnippetStringNode (nodes .inline ):
23+
24+ class snippet_string_node (nodes .inline ): # noqa: N801 (snake_case is fine)
1225 """Node for a string literal in a grammar snippet."""
1326
14- def __init__ (self ) -> None :
27+ def __init__ (
28+ self ,
29+ rawsource : str = '' ,
30+ text : str = '' ,
31+ * children : Node ,
32+ ** attributes : Any ,
33+ ) -> None :
34+ super ().__init__ (rawsource , text , * children , ** attributes )
1535 # Use the Pygments highlight class for `Literal.String.Other`
16- super (). __init__ ( classes = [ 'sx' ] )
36+ self [ 'classes' ]. append ( 'sx' )
1737
1838
1939class GrammarSnippetBase (SphinxDirective ):
2040 """Common functionality for GrammarSnippetDirective & CompatProductionList."""
2141
2242 # The option/argument handling is left to the individual classes.
2343
24- def make_grammar_snippet (self , options , content ):
44+ def make_grammar_snippet (
45+ self , options : dict [str , Any ], content : Sequence [str ]
46+ ) -> list [nodes .paragraph ]:
2547 """Create a literal block from options & content."""
2648
2749 group_name = options ['group' ]
@@ -65,22 +87,20 @@ def make_grammar_snippet(self, options, content):
6587 last_pos = match .end ()
6688
6789 # Handle matches
68- groupdict = {
90+ group_dict = {
6991 name : content
7092 for name , content in match .groupdict ().items ()
7193 if content is not None
7294 }
73- match groupdict :
95+ match group_dict :
7496 case {'rule_name' : name }:
7597 literal += self .make_link_target_for_token (
7698 group_name , name
7799 )
78100 case {'rule_ref' : ref_text }:
79101 literal += token_xrefs (ref_text , group_name )
80102 case {'single_quoted' : name } | {'double_quoted' : name }:
81- string_node = SnippetStringNode ()
82- string_node += nodes .Text (name )
83- literal += string_node
103+ literal += snippet_string_node ('' , name )
84104 case _:
85105 raise ValueError ('unhandled match' )
86106 literal += nodes .Text (line [last_pos :] + '\n ' )
@@ -93,8 +113,10 @@ def make_grammar_snippet(self, options, content):
93113
94114 return [node ]
95115
96- def make_link_target_for_token (self , group_name , name ):
97- """Return a literal node which is a link target for the given token"""
116+ def make_link_target_for_token (
117+ self , group_name : str , name : str
118+ ) -> addnodes .literal_strong :
119+ """Return a literal node which is a link target for the given token."""
98120 name_node = addnodes .literal_strong ()
99121
100122 # Cargo-culted magic to make `name_node` a link target
@@ -138,20 +160,20 @@ class GrammarSnippetDirective(GrammarSnippetBase):
138160
139161 has_content = True
140162 option_spec = {
141- 'group' : directives .unchanged ,
163+ 'group' : directives .unchanged_required ,
142164 }
143165
144166 # We currently ignore arguments.
145167 required_arguments = 0
146168 optional_arguments = 1
147169 final_argument_whitespace = True
148170
149- def run (self ):
171+ def run (self ) -> list [ nodes . paragraph ] :
150172 return self .make_grammar_snippet (self .options , self .content )
151173
152174
153175class CompatProductionList (GrammarSnippetBase ):
154- """Create grammar snippets from ReST productionlist syntax
176+ """Create grammar snippets from reST productionlist syntax
155177
156178 This is intended to be a transitional directive, used while we switch
157179 from productionlist to grammar-snippet.
@@ -165,7 +187,7 @@ class CompatProductionList(GrammarSnippetBase):
165187 final_argument_whitespace = True
166188 option_spec = {}
167189
168- def run (self ):
190+ def run (self ) -> list [ nodes . paragraph ] :
169191 # The "content" of a productionlist is actually the first and only
170192 # argument. The first line is the group; the rest is the content lines.
171193 lines = self .arguments [0 ].splitlines ()
@@ -185,9 +207,13 @@ def run(self):
185207 return self .make_grammar_snippet (options , content )
186208
187209
188- def setup (app ) :
210+ def setup (app : Sphinx ) -> ExtensionMetadata :
189211 app .add_directive ('grammar-snippet' , GrammarSnippetDirective )
190212 app .add_directive_to_domain (
191213 'std' , 'productionlist' , CompatProductionList , override = True
192214 )
193- return {'version' : '1.0' , 'parallel_read_safe' : True }
215+ return {
216+ 'version' : '1.0' ,
217+ 'parallel_read_safe' : True ,
218+ 'parallel_write_safe' : True ,
219+ }
0 commit comments