1 # Copyright (C) 2020-2023 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <https://www.gnu.org/licenses/>.
18 """Sphinx plugin to render Mailman Core configuration file schema.cfg."""
23 from docutils
import nodes
24 from docutils
.parsers
.rst
import Directive
, directives
25 from docutils
.statemachine
import ViewList
26 from importlib
.resources
import files
27 from sphinx
.util
.nodes
import nested_parse_with_titles
30 def get_config_text():
31 """Get Mailman's schema.cfg as str"""
32 return files('mailman.config').joinpath('schema.cfg').read_text()
35 def get_section_text(section
, schema_text
):
36 """Get the text of a give ini section.
38 This includes the region between two `[section]` headers in the ini file.
40 :param section: The name of the section.
41 :param schema_text: The whole config file contents.
42 :returns: The str of all the value in the section.
43 :raises ValueError: If the name of the section can't be found.
45 # Split the whole file at the boundary of [sections].
46 sections
= re
.split(r
'^\[(?P<header>[^]]+)\]',
47 schema_text
, flags
=re
.MULTILINE
)
48 if not f
'{section}' in sections
:
49 raise ValueError('Invalid section name {}'.format(section
))
50 section_index
= sections
.index(section
)
51 section_text
= sections
[section_index
+ 1]
56 """Check if a paragraph is comment without any options.
58 :param para: The paragraph text.
59 :returns: True if all lines start with '#', False otherwise.
62 for line
in para
.splitlines():
63 if not line
.startswith('#'):
68 def get_options(section_text
):
69 """Parse the key:value pairs from it along with comments.
71 Given the text of a section, split the whole text with empty lines
72 ('\n\n'). For each part get the (key: value) pairs by letting configparser
73 parse the text. The remaining lines of text, which ends up being the
74 comments in the file serve as the documentation for those key: value pairs.
76 Note: We append a `[dummy]` section name to the section_text since
77 configparser will refuse to parse a section text that doesn't include a
78 `[section]` header. There is no real significance of that since we
79 immidiately discard the section name.
81 If the section starts off with a block of just comment, it is called
84 The return format looks something like:
86 ([{'key': 'value', 'key2': 'value2', 'doc': 'Comments'}], "Section Doc")
88 The first item is a list of dictionaries, each of which represents a
89 paragraph in the ini text. All (key: value) pairs are in the dictionary
90 and the comments as a part of the 'doc'. When there are no comments, 'doc'
93 :param section_text: The whole text of the section, not include the header
95 :returns: Parsed options, docs and section docs.
98 opts_list
= section_text
.split('\n\n')
101 # We check for a section leve doc by looking if the
102 # first *two* paragraphs are both comments and *only* comments.
103 if is_comment(opts_list
[0]) and is_comment(opts_list
[1]):
104 section_doc
= opts_list
.pop(0)
106 for each
in opts_list
:
107 if each
.strip() == '':
109 # Configparser will refuse to parse section without it's name.
110 each
= '[dummy]\n' + each
111 config
= configparser
.ConfigParser()
112 config
.read_string(each
)
114 for key
in config
['dummy']:
115 data
[key
] = config
['dummy'][key
]
117 doc
= '\n'.join(line
for line
in each
.splitlines() if line
.startswith('#'))
118 data
['doc'] = doc
.replace('#', '')
120 return options
, section_doc
123 def get_section_rst(section
, section_doc
, opts
):
124 """Convert the section text into formatted ReST.
126 A section from ini file that looks like this:
129 # This is a section level documentation.
131 # This documentation if for immediately following key:value
134 Is converted to ReST that looks something like:
143 This documentation is for the immediately following key:value
145 rst
= '``[{}]``\n{}\n'.format(section
, '='*(len(section
) + 6))
147 rst
+= section_doc
.replace('#', '')
151 doc
= each
.pop('doc')
152 for opt
, value
in each
.items():
153 rst
+= '{}\n{}\n'.format(opt
, '~'*len(opt
))
155 rst
+= '**default**: {}\n\n'.format(value
)
156 rst
+= doc
.replace('#', '')
161 class ConfigSectionDirective(Directive
):
162 """Sphinx plugin that renders Mailman's ini configuration as ReST."""
164 required_arguments
= 1
165 final_argument_whitespace
= True
170 """Split the arguments as a list of sections and render as ReST."""
172 sections
= self
.arguments
[0].split()
175 for section
in sections
:
177 config_text
= get_config_text()
178 section_text
= get_section_text(section
, config_text
)
179 section_opts
, section_doc
= get_options(section_text
)
180 section_rst
= get_section_rst(section
, section_doc
, section_opts
)
181 for line
in section_rst
.splitlines():
182 rst
.append(line
, 'fakefile.rst', lineno
)
185 node
= nodes
.section()
186 node
.document
= self
.state
.document
187 nested_parse_with_titles(self
.state
, rst
, node
)
188 child_nodes
.extend(node
.children
)
193 app
.add_directive('configsection', ConfigSectionDirective
)
197 'parallel_read_safe': True,
198 'parallel_write_safe': True,