Belle II Software development
basf2ext.py
1
8
9"""
10This module is a Sphinx Extension for the Belle~II Software:
11
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
18
19 .. b2-modules::
20 :package: framework
21 :modules: EventInfoSetter, EventInfoPrinter
22 :library: libcore.so
23 :regex-filter: Event.*
24
25* add directive to automatically document basf2 variables
26
27 .. b2-variables::
28 :group: Kinematics
29 :variables: x, y, z
30 :regex-filter: .*
31
32"""
33
34
35import os
36import re
37import textwrap
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
45
46
47def parse_with_titles(state, content):
48 """Shortcut function to parse a reStructuredText fragment into docutils nodes"""
49 node = nodes.container()
50 # copied from autodoc: necessary so that the child nodes get the right source/line set
51 node.document = state.document
52 if isinstance(content, str):
53 content = content.splitlines()
54 if not isinstance(content, StringList):
55 content = StringList(content)
56 nested_parse_with_titles(state, content, node)
57 return node.children
58
59
60class RenderDocstring(Directive):
61 """Directive to Render Docstring as Docutils nodes.
62 This is useful as standard reStructuredText does not parse Google
63 Docstrings but we support it in docstrings for python functions, modules
64 and variables. So to show example docstrings in the documentation we don't
65 want to write the example in Google Docstring and keep that synchronous
66 with a reStructuredText version
67 """
68
69 has_content = True
70 option_spec = {
71 "lines": directives.unchanged
72 }
73
74
75 def run(self):
76 """Just pass on the content to the autodoc-process-docstring event and
77 then parse the resulting reStructuredText."""
78 env = self.state.document.settings.env
79 content = list(self.content)
80 try:
81 start_index, end_index = (int(e) for e in self.options.get("lines", None).split(","))
82 content = content[start_index:end_index]
83 except Exception:
84 pass
85
86 # remove common whitespace
87 content = textwrap.dedent("\n".join(content)).splitlines()
88
89 env.app.emit('autodoc-process-docstring', "docstring", None, None, None, content)
90 return parse_with_titles(self.state, content)
91
92
93
94class ModuleListDirective(Directive):
95 has_content = False
96 option_spec = {
97 "library": directives.unchanged,
98 "modules": directives.unchanged,
99 "package": directives.unchanged,
100 "no-parameters": directives.flag,
101 "noindex": directives.flag,
102 "regex-filter": directives.unchanged,
103 "io-plots": directives.flag,
104 }
105
106
107 def show_module(self, module, library):
108 description = module.description().splitlines()
109 for i, line in enumerate(description):
110 description[i] = line.replace('|release|', self.state.document.settings.env.app.config.release)
111 # pretend to be the autodoc extension to let other events process
112 # the doc string. Enables Google/Numpy docstrings as well as a bit
113 # of doxygen docstring conversion we have
114 env = self.state.document.settings.env
115 env.app.emit('autodoc-process-docstring', "b2:module", module.name(), module, None, description)
116 description += ["", "",
117 f":Package: {module.package()}",
118 f":Library: {os.path.basename(library)}",
119 ]
120
121 if "no-parameters" not in self.options:
122 optional_params = []
123 required_params = []
124 for p in module.available_params():
125 dest = required_params if p.forceInSteering else optional_params
126 default = "" if p.forceInSteering else f", default={p.default!r}"
127 param_desc = p.description.splitlines()
128 # run the description through autodoc event to get
129 # Google/Numpy/doxygen style as well
130 env.app.emit('autodoc-process-docstring', 'b2:module:param', module.name() + '.' + p.name, p, None, param_desc)
131 param_desc = textwrap.indent("\n".join(param_desc), 8 * " ").splitlines()
132 dest += [f" * **{p.name}** *({p.type}{default})*"]
133 dest += param_desc
134
135 if (required_params):
136 description += [":Required Parameters:", " "] + required_params
137 if (optional_params):
138 description += [":Parameters:", " "] + optional_params
139
140 if "io-plots" in self.options:
141 image = f"build/ioplots/{module.name()}.png"
142 if os.path.exists(image):
143 description += [":IO diagram:", " ", f" .. image:: /{image}"]
144
145 content = [f".. b2:module:: {module.name()}"] + self.noindex + [" "]
146 content += [" " + e for e in description]
147 return parse_with_titles(self.state, content)
148
149 def run(self):
150 all_modules = list_available_modules().items()
151 # check if we have a list of modules to show if so filter the list of
152 # all modules
153 if "modules" in self.options:
154 modules = [e.strip() for e in self.options["modules"].split(",")]
155 all_modules = [e for e in all_modules if e[0] in modules]
156
157 # check if we have a regex-filter, if so filter list of modules
158 if "regex-filter" in self.options:
159 re_filter = re.compile(self.options["regex-filter"])
160 all_modules = [e for e in all_modules if re_filter.match(e[0]) is not None]
161
162 # aaand also filter by library (in case some one wants to document all
163 # modules found in a given library)
164 if "library" in self.options:
165 lib = self.options["library"].strip()
166 all_modules = [e for e in all_modules if os.path.basenam(e[1]) == lib]
167
168 # see if we have to forward noindex
169 self.noindex = [" :noindex:"] if "noindex" in self.options else []
170
171 # list of all docutil nodes we create to be returned
172 all_nodes = []
173
174 # now loop over all modules
175 for name, library in sorted(all_modules):
176 module = register_module(name)
177 # filter by package: can only be done after instantiating the module
178 if "package" in self.options and module.package() != self.options["package"]:
179 continue
180
181 # everyting set, create documentation for our module
182 all_nodes += self.show_module(module, library)
183
184 return all_nodes
185
186
187
188class VariableListDirective(Directive):
189 has_content = False
190 option_spec = {
191 "group": directives.unchanged,
192 "variables": directives.unchanged,
193 "regex-filter": directives.unchanged,
194 "description-regex-filter": directives.unchanged,
195 "noindex": directives.flag,
196 }
197
198
199 def run(self):
200 from ROOT import Belle2
202 self.noindex = [" :noindex:"] if "noindex" in self.options else []
203 all_variables = []
204 explicit_list = None
205 regex_filter = None
206 desc_regex_filter = None
207 if "variables" in self.options:
208 explicit_list = [e.strip() for e in self.options["variables"].split(",")]
209 if "regex-filter" in self.options:
210 regex_filter = re.compile(self.options["regex-filter"])
211 if "description-regex-filter" in self.options:
212 desc_regex_filter = re.compile(self.options["description-regex-filter"])
213
214 for var in manager.getVariables():
215 if "group" in self.options and self.options["group"] != var.group:
216 continue
217 if explicit_list and var.name not in explicit_list:
218 continue
219 if regex_filter and not regex_filter.match(var.name):
220 continue
221 if desc_regex_filter and not desc_regex_filter.match(var.description):
222 continue
223 all_variables.append(var)
224
225 all_nodes = []
226 env = self.state.document.settings.env
227 for var in sorted(all_variables, key=lambda x: x.name):
228
229 # for overloaded variables, we might have to flag noindex in the
230 # variable description so also check for that
231 index = self.noindex
232 if ":noindex:" in var.description:
233 index = [" :noindex:"]
234 var.description = var.description.replace(":noindex:", "")
235
236 docstring = var.description.splitlines()
237 # pretend to be the autodoc extension to let other events process
238 # the doc string. Enables Google/Numpy docstrings as well as a bit
239 # of doxygen docstring conversion we have
240 env.app.emit('autodoc-process-docstring', "b2:variable", var.name, var, None, docstring)
241
242 description = [f".. b2:variable:: {var.name}"] + index + [""]
243 description += [" " + e for e in docstring]
244 if "group" not in self.options:
245 description += ["", f" :Group: {var.group}"]
246
247 all_nodes += parse_with_titles(self.state, description)
248
249 return all_nodes
250
251
252def html_page_context(app, pagename, templatename, context, doctree):
253 """Provide Link to GitLab Repository, see https://mg.pov.lt/blog/sphinx-edit-on-github.html
254
255 this goes in conjunction with
256 site_scons/sphinx/_sphinxtemplates/sourcelink.html and adds a link to our
257 git repository instead to the local source link
258 """
259
260 if templatename != 'page.html' or not doctree:
261 return
262
263 path = os.path.relpath(doctree.get('source'), app.builder.srcdir)
264 repository = app.config.basf2_repository
265 if not repository:
266 return
267
268 commit = app.config.basf2_commitid
269 context["source_url"] = f"{repository}/-/blob/main/{path}"
270 if commit:
271 context["source_url"] = f"{repository}/-/blob/{commit}/{path}"
272
273
274def gitlab_issue_role(role, rawtext, text, lineno, inliner, options=None, content=None):
275 if content is None:
276 content = []
277 if options is None:
278 options = {}
279 issue_url = inliner.document.settings.env.app.config.basf2_issues
280 if not issue_url:
281 return [nodes.literal(rawtext, text=text, language=None)], []
282
283 url = f"{issue_url}/{text}"
284 return [nodes.reference(rawtext, text=text, refuri=url)], []
285
286
287def doxygen_role(role, rawtext, text, lineno, inliner, options=None, content=None):
288 if content is None:
289 content = []
290 if options is None:
291 options = {}
292 release_version = inliner.document.settings.env.app.config.release
293 match = re.match(r'(.+?)<(.+?)>', text)
294 if match:
295 display_text, url_text = match.groups()
296 else:
297 display_text = text
298 url_text = text
299 if "html#" in url_text:
300 url = f"https://software.belle2.org/{release_version}/doxygen/{url_text}"
301 else:
302 url = f"https://software.belle2.org/{release_version}/doxygen/{url_text}.html"
303
304 return [nodes.reference(rawsource=rawtext, text=display_text, refuri=url)], []
305
306
307def sphinx_role(role, rawtext, text, lineno, inliner, options=None, content=None):
308 if content is None:
309 content = []
310 if options is None:
311 options = {}
312 release_version = inliner.document.settings.env.app.config.release
313 match = re.match(r'(.+?)<(.+?)>', text)
314 if match:
315 display_text, url_text = match.groups()
316 else:
317 display_text = text
318 url_text = text
319 if "html#" in url_text:
320 url = f"https://software.belle2.org/{release_version}/sphinx/{url_text}"
321 else:
322 url = f"https://software.belle2.org/{release_version}/sphinx/{url_text}.html"
323
324 return [nodes.reference(rawsource=rawtext, text=display_text, refuri=url)], []
325
326
327def setup(app):
328 import basf2
329 basf2.logging.log_level = basf2.LogLevel.WARNING
330
331 app.add_config_value("basf2_repository", "", True)
332 app.add_config_value("basf2_commitid", "", True)
333 app.add_config_value("basf2_issues", "", True)
334 app.add_domain(Basf2Domain)
335 app.add_directive("b2-modules", ModuleListDirective)
336 app.add_directive("b2-variables", VariableListDirective)
337 app.add_directive("docstring", RenderDocstring)
338 app.add_role("issue", gitlab_issue_role)
339 app.add_role("doxygen", doxygen_role)
340 app.add_role("sphinx", sphinx_role)
341 app.connect('html-page-context', html_page_context)
342
343 # Sadly sphinx does not seem to add labels to custom indices ... :/
344 StandardDomain.initial_data["labels"]["b2-modindex"] = ("b2-modindex", "", "basf2 Module Index")
345 StandardDomain.initial_data["labels"]["b2-varindex"] = ("b2-varindex", "", "basf2 Variable Index")
346 StandardDomain.initial_data["anonlabels"]["b2-modindex"] = ("b2-modindex", "")
347 StandardDomain.initial_data["anonlabels"]["b2-varindex"] = ("b2-varindex", "")
348 return {'version': 0.2}
static Manager & Instance()
get singleton instance.
Definition: Manager.cc:26