10This module is a Sphinx Extension for the Belle~II Software:
12* add a domain "b2" for modules and variables.
13 Modules can be documented using the ``.. b2:module::` directive and variables
14 using the ``.. b2:variable::` directive. They can be cross referenced with
15 :b2:mod: and :b2:var: respectively
16* add an index for basf2 modules
17* add a directive to automatically document basf2 modules similar to autodoc
21 :modules: EventInfoSetter, EventInfoPrinter
23 :regex-filter: Event.*
25* add directive to automatically document basf2 variables
38from docutils
import nodes
39from sphinx.util.nodes
import nested_parse_with_titles
40from docutils.parsers.rst
import Directive, directives
41from docutils.statemachine
import StringList
42from basf2domain
import Basf2Domain
43from basf2
import list_available_modules, register_module
44from sphinx.domains.std
import StandardDomain
45import xml.etree.ElementTree
as ET
48def parse_with_titles(state, content):
49 """Shortcut function to parse a reStructuredText fragment into docutils nodes"""
50 node = nodes.container()
52 node.document = state.document
53 if isinstance(content, str):
54 content = content.splitlines()
55 if not isinstance(content, StringList):
56 content = StringList(content)
57 nested_parse_with_titles(state, content, node)
62 """Directive to Render Docstring as Docutils nodes.
63 This is useful as standard reStructuredText does not parse Google
64 Docstrings but we support it in docstrings for python functions, modules
65 and variables. So to show example docstrings in the documentation we don't
66 want to write the example in Google Docstring and keep that synchronous
67 with a reStructuredText version
72 "lines": directives.unchanged
77 """Just pass on the content to the autodoc-process-docstring event and
78 then parse the resulting reStructuredText."""
79 env = self.
state.document.settings.env
80 content = list(self.content)
82 start_index, end_index = (int(e)
for e
in self.options.get(
"lines",
None).split(
","))
83 content = content[start_index:end_index]
88 content = textwrap.dedent(
"\n".join(content)).splitlines()
90 env.app.emit(
'autodoc-process-docstring',
"docstring",
None,
None,
None, content)
91 return parse_with_titles(self.
state, content)
95class ModuleListDirective(Directive):
98 "library": directives.unchanged,
99 "modules": directives.unchanged,
100 "package": directives.unchanged,
101 "no-parameters": directives.flag,
102 "noindex": directives.flag,
103 "regex-filter": directives.unchanged,
104 "io-plots": directives.flag,
108 def show_module(self, module, library):
109 description = module.description().splitlines()
110 for i, line
in enumerate(description):
111 description[i] = line.replace(
'|release|', self.
state.document.settings.env.app.config.release)
115 env = self.
state.document.settings.env
116 env.app.emit(
'autodoc-process-docstring',
"b2:module", module.name(), module,
None, description)
117 description += [
"",
"",
118 f
":Package: {module.package()}",
119 f
":Library: {os.path.basename(library)}",
122 if "no-parameters" not in self.options:
125 for p
in module.available_params():
126 dest = required_params
if p.forceInSteering
else optional_params
127 default =
"" if p.forceInSteering
else f
", default={p.default!r}"
128 param_desc = p.description.splitlines()
131 env.app.emit(
'autodoc-process-docstring',
'b2:module:param', module.name() +
'.' + p.name, p,
None, param_desc)
132 param_desc = textwrap.indent(
"\n".join(param_desc), 8 *
" ").splitlines()
133 dest += [f
" * **{p.name}** *({p.type}{default})*"]
136 if (required_params):
137 description += [
":Required Parameters:",
" "] + required_params
138 if (optional_params):
139 description += [
":Parameters:",
" "] + optional_params
141 if "io-plots" in self.options:
142 image = f
"build/ioplots/{module.name()}.png"
143 if os.path.exists(image):
144 description += [
":IO diagram:",
" ", f
" .. image:: /{image}"]
146 content = [f
".. b2:module:: {module.name()}"] + self.noindex + [
" "]
147 content += [
" " + e
for e
in description]
148 return parse_with_titles(self.
state, content)
151 all_modules = list_available_modules().items()
154 if "modules" in self.options:
155 modules = [e.strip()
for e
in self.options[
"modules"].split(
",")]
156 all_modules = [e
for e
in all_modules
if e[0]
in modules]
159 if "regex-filter" in self.options:
160 re_filter = re.compile(self.options[
"regex-filter"])
161 all_modules = [e
for e
in all_modules
if re_filter.match(e[0])
is not None]
165 if "library" in self.options:
166 lib = self.options[
"library"].strip()
167 all_modules = [e
for e
in all_modules
if os.path.basename(e[1]) == lib]
170 self.noindex = [
" :noindex:"]
if "noindex" in self.options
else []
176 for name, library
in sorted(all_modules):
177 module = register_module(name)
179 if "package" in self.options
and module.package() != self.options[
"package"]:
183 all_nodes += self.show_module(module, library)
188def doxygen_file_page(filename: str) -> str:
189 base, ext = os.path.splitext(os.path.basename(filename))
190 if ext.startswith(
"."):
192 return f
"{base}_8{ext}_source.html"
195def doxygen_source_link(location_elem):
197 Given a <location> element from Doxygen XML,
198 build html page name and anchor
200 file_path = location_elem.attrib[
'file']
201 line = int(location_elem.attrib[
'line'])
203 page = doxygen_file_page(file_path)
204 anchor = f
"#l{line:05d}"
209def build_doxygen_anchor_map(xml_dir):
211 Parse Doxygen XML files and build mapping:
212 qualifiedname -> "File_8ext_source.html#lNNNNN"
213 If no <location> element is available, fall back to anchorfile#anchor.
214 xml_dir: path to doxygen/xml (must contain index.xml)
215 Returns: dict { "VarName": "classFoo_1_1Bar.html#a1234abcd" }
220 index_path = os.path.join(xml_dir,
"index.xml")
221 if not os.path.exists(index_path):
223 tree = ET.parse(index_path)
224 root = tree.getroot()
226 for comp
in root.findall(
"compound"):
227 refid = comp.attrib[
"refid"]
228 compound_file = os.path.join(xml_dir, f
"{refid}.xml")
229 if not os.path.exists(compound_file):
232 ctree = ET.parse(compound_file)
233 croot = ctree.getroot()
235 for member
in croot.findall(
".//memberdef"):
236 kind = member.attrib.get(
"kind")
237 if kind
not in (
"function",
"variable"):
240 name = member.findtext(
"name")
241 qualified = member.findtext(
"qualifiedname")
242 if qualified
and not qualified.startswith(
"Belle2::Variable::"):
244 key = qualified
or name
247 location = member.find(
"location")
250 if location
is not None and "file" in location.attrib
and "line" in location.attrib:
251 href = doxygen_source_link(location)
255 anchor_file = member.findtext(
"anchorfile")
256 anchor_id = member.findtext(
"anchor")
257 if anchor_file
and anchor_id:
258 href = f
"{anchor_file}#{anchor_id}"
261 definition = member.findtext(
"definition")
or ""
262 if definition.startswith(
"REGISTER_VARIABLE"):
267 if key
not in anchors:
271 if "_source.html" in href
and "_source.html" not in anchors[key]:
278class VariableListDirective(Directive):
281 "group": directives.unchanged,
282 "variables": directives.unchanged,
283 "regex-filter": directives.unchanged,
284 "description-regex-filter": directives.unchanged,
285 "noindex": directives.flag,
286 "filename": directives.unchanged,
291 from ROOT
import Belle2
293 self.noindex = [
" :noindex:"]
if "noindex" in self.options
else []
297 desc_regex_filter =
None
298 if "variables" in self.options:
299 explicit_list = [e.strip()
for e
in self.options[
"variables"].split(
",")]
300 if "regex-filter" in self.options:
301 regex_filter = re.compile(self.options[
"regex-filter"])
302 if "description-regex-filter" in self.options:
303 desc_regex_filter = re.compile(self.options[
"description-regex-filter"])
305 for var
in manager.getVariables():
306 if "group" in self.options
and self.options[
"group"] != var.group:
308 if explicit_list
and var.name
not in explicit_list:
310 if regex_filter
and not regex_filter.match(var.name):
312 if desc_regex_filter
and not desc_regex_filter.match(str(var.description)):
314 all_variables.append(var)
317 env = self.
state.document.settings.env
318 baseurl = env.app.config.basf2_doxygen_baseurl
319 anchor_map = getattr(env.app.env,
"b2_anchor_map", {})
320 for var
in sorted(all_variables, key=
lambda x: x.name):
325 desc = str(var.description)
326 desc = textwrap.dedent(desc).strip(
"\n")
327 if ":noindex:" in var.description:
328 index = [
" :noindex:"]
329 desc = desc.replace(
":noindex:",
"")
331 docstring = desc.splitlines()
335 env.app.emit(
'autodoc-process-docstring',
"b2:variable", var.name, var,
None, docstring)
337 description = [f
".. b2:variable:: {var.name}"] + index + [
""]
338 qualifiedName = f
"Belle2::Variable::{var.name}"
339 qualifiedFunctionName = f
"Belle2::Variable::{var.functionName}"
340 key = qualifiedName
if qualifiedName
in anchor_map
else qualifiedFunctionName
341 if key
in anchor_map:
342 link = f
"{baseurl}{anchor_map[key]}"
343 description.insert(1, f
" :source: {link}")
344 description += [
" " + e
for e
in docstring]
345 if "group" not in self.options:
346 description += [
"", f
" :Group: {var.group}"]
348 all_nodes += parse_with_titles(self.
state, description)
353def html_page_context(app, pagename, templatename, context, doctree):
354 """Provide Link to GitLab Repository, see https://mg.pov.lt/blog/sphinx-edit-on-github.html
356 this goes in conjunction with
357 site_scons/sphinx/_sphinxtemplates/sourcelink.html and adds a link to our
358 git repository instead to the local source link
361 if templatename !=
'page.html' or not doctree:
364 path = os.path.relpath(doctree.get(
'source'), app.builder.srcdir)
365 repository = app.config.basf2_repository
369 commit = app.config.basf2_commitid
370 context[
"source_url"] = f
"{repository}/-/blob/main/{path}"
372 context[
"source_url"] = f
"{repository}/-/blob/{commit}/{path}"
375def gitlab_issue_role(role, rawtext, text, lineno, inliner, options=None, content=None):
380 issue_url = inliner.document.settings.env.app.config.basf2_issues
382 return [nodes.literal(rawtext, text=text, language=
None)], []
384 url = f
"{issue_url}/{text}"
385 return [nodes.reference(rawtext, text=text, refuri=url)], []
388def doxygen_role(role, rawtext, text, lineno, inliner, options=None, content=None):
393 release_version = inliner.document.settings.env.app.config.release
394 match = re.match(
r'(.+?)<(.+?)>', text)
396 display_text, url_text = match.groups()
400 if "html#" in url_text:
401 url = f
"https://software.belle2.org/{release_version}/doxygen/{url_text}"
403 url = f
"https://software.belle2.org/{release_version}/doxygen/{url_text}.html"
405 return [nodes.reference(rawsource=rawtext, text=display_text, refuri=url)], []
408def sphinx_role(role, rawtext, text, lineno, inliner, options=None, content=None):
413 release_version = inliner.document.settings.env.app.config.release
414 match = re.match(
r'(.+?)<(.+?)>', text)
416 display_text, url_text = match.groups()
420 if "html#" in url_text:
421 url = f
"https://software.belle2.org/{release_version}/sphinx/{url_text}"
423 url = f
"https://software.belle2.org/{release_version}/sphinx/{url_text}.html"
425 return [nodes.reference(rawsource=rawtext, text=display_text, refuri=url)], []
428def load_doxygen_anchors(app):
429 xml_dir = app.config.basf2_doxygen_xml_dir
430 app.env.b2_anchor_map = build_doxygen_anchor_map(xml_dir)
435 basf2.logging.log_level = basf2.LogLevel.WARNING
437 app.add_config_value(
"basf2_repository",
"",
True)
438 app.add_config_value(
"basf2_commitid",
"",
True)
439 app.add_config_value(
"basf2_issues",
"",
True)
440 app.add_config_value(
"basf2_doxygen_baseurl",
"",
"env")
441 app.add_config_value(
"basf2_doxygen_xml_dir",
None,
"env")
442 app.add_domain(Basf2Domain)
443 app.add_directive(
"b2-modules", ModuleListDirective)
444 app.add_directive(
"b2-variables", VariableListDirective)
445 app.add_directive(
"docstring", RenderDocstring)
446 app.add_role(
"issue", gitlab_issue_role)
447 app.add_role(
"doxygen", doxygen_role)
448 app.add_role(
"sphinx", sphinx_role)
449 app.connect(
'html-page-context', html_page_context)
450 app.connect(
"builder-inited", load_doxygen_anchors)
453 StandardDomain.initial_data[
"labels"][
"b2-modindex"] = (
"b2-modindex",
"",
"basf2 Module Index")
454 StandardDomain.initial_data[
"labels"][
"b2-varindex"] = (
"b2-varindex",
"",
"basf2 Variable Index")
455 StandardDomain.initial_data[
"anonlabels"][
"b2-modindex"] = (
"b2-modindex",
"")
456 StandardDomain.initial_data[
"anonlabels"][
"b2-varindex"] = (
"b2-varindex",
"")
457 return {
'version': 0.3}
static Manager & Instance()
get singleton instance.