Belle II Software light-2406-ragdoll
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 # pretend to be the autodoc extension to let other events process
110 # the doc string. Enables Google/Numpy docstrings as well as a bit
111 # of doxygen docstring conversion we have
112 env = self.state.document.settings.env
113 env.app.emit('autodoc-process-docstring', "b2:module", module.name(), module, None, description)
114 description += ["", "",
115 f":Package: {module.package()}",
116 f":Library: {os.path.basename(library)}",
117 ]
118
119 if "no-parameters" not in self.options:
120 optional_params = []
121 required_params = []
122 for p in module.available_params():
123 dest = required_params if p.forceInSteering else optional_params
124 default = "" if p.forceInSteering else f", default={p.default!r}"
125 param_desc = p.description.splitlines()
126 # run the description through autodoc event to get
127 # Google/Numpy/doxygen style as well
128 env.app.emit('autodoc-process-docstring', 'b2:module:param', module.name() + '.' + p.name, p, None, param_desc)
129 param_desc = textwrap.indent("\n".join(param_desc), 8 * " ").splitlines()
130 dest += [f" * **{p.name}** *({p.type}{default})*"]
131 dest += param_desc
132
133 if(required_params):
134 description += [":Required Parameters:", " "] + required_params
135 if(optional_params):
136 description += [":Parameters:", " "] + optional_params
137
138 if "io-plots" in self.options:
139 image = f"build/ioplots/{module.name()}.png"
140 if os.path.exists(image):
141 description += [":IO diagram:", " ", f" .. image:: /{image}"]
142
143 content = [f".. b2:module:: {module.name()}"] + self.noindex + [" "]
144 content += [" " + e for e in description]
145 return parse_with_titles(self.state, content)
146
147 def run(self):
148 all_modules = list_available_modules().items()
149 # check if we have a list of modules to show if so filter the list of
150 # all modules
151 if "modules" in self.options:
152 modules = [e.strip() for e in self.options["modules"].split(",")]
153 all_modules = [e for e in all_modules if e[0] in modules]
154
155 # check if we have a regex-filter, if so filter list of modules
156 if "regex-filter" in self.options:
157 re_filter = re.compile(self.options["regex-filter"])
158 all_modules = [e for e in all_modules if re_filter.match(e[0]) is not None]
159
160 # aaand also filter by library (in case some one wants to document all
161 # modules found in a given library)
162 if "library" in self.options:
163 lib = self.options["library"].strip()
164 all_modules = [e for e in all_modules if os.path.basenam(e[1]) == lib]
165
166 # see if we have to forward noindex
167 self.noindex = [" :noindex:"] if "noindex" in self.options else []
168
169 # list of all docutil nodes we create to be returned
170 all_nodes = []
171
172 # now loop over all modules
173 for name, library in sorted(all_modules):
174 module = register_module(name)
175 # filter by package: can only be done after instantiating the module
176 if "package" in self.options and module.package() != self.options["package"]:
177 continue
178
179 # everyting set, create documentation for our module
180 all_nodes += self.show_module(module, library)
181
182 return all_nodes
183
184
185
186class VariableListDirective(Directive):
187 has_content = False
188 option_spec = {
189 "group": directives.unchanged,
190 "variables": directives.unchanged,
191 "regex-filter": directives.unchanged,
192 "description-regex-filter": directives.unchanged,
193 "noindex": directives.flag,
194 }
195
196
197 def run(self):
198 from ROOT import Belle2
200 self.noindex = [" :noindex:"] if "noindex" in self.options else []
201 all_variables = []
202 explicit_list = None
203 regex_filter = None
204 desc_regex_filter = None
205 if "variables" in self.options:
206 explicit_list = [e.strip() for e in self.options["variables"].split(",")]
207 if "regex-filter" in self.options:
208 regex_filter = re.compile(self.options["regex-filter"])
209 if "description-regex-filter" in self.options:
210 desc_regex_filter = re.compile(self.options["description-regex-filter"])
211
212 for var in manager.getVariables():
213 if "group" in self.options and self.options["group"] != var.group:
214 continue
215 if explicit_list and var.name not in explicit_list:
216 continue
217 if regex_filter and not regex_filter.match(var.name):
218 continue
219 if desc_regex_filter and not desc_regex_filter.match(var.description):
220 continue
221 all_variables.append(var)
222
223 all_nodes = []
224 env = self.state.document.settings.env
225 for var in sorted(all_variables, key=lambda x: x.name):
226
227 # for overloaded variables, we might have to flag noindex in the
228 # variable description so also check for that
229 index = self.noindex
230 if ":noindex:" in var.description:
231 index = [" :noindex:"]
232 var.description = var.description.replace(":noindex:", "")
233
234 docstring = var.description.splitlines()
235 # pretend to be the autodoc extension to let other events process
236 # the doc string. Enables Google/Numpy docstrings as well as a bit
237 # of doxygen docstring conversion we have
238 env.app.emit('autodoc-process-docstring', "b2:variable", var.name, var, None, docstring)
239
240 description = [f".. b2:variable:: {var.name}"] + index + [""]
241 description += [" " + e for e in docstring]
242 if "group" not in self.options:
243 description += ["", f" :Group: {var.group}"]
244
245 all_nodes += parse_with_titles(self.state, description)
246
247 return all_nodes
248
249
250def html_page_context(app, pagename, templatename, context, doctree):
251 """Provide Link to GitLab Repository, see https://mg.pov.lt/blog/sphinx-edit-on-github.html
252
253 this goes in conjunction with
254 site_scons/sphinx/_sphinxtemplates/sourcelink.html and adds a link to our
255 git repository instead to the local source link
256 """
257
258 if templatename != 'page.html' or not doctree:
259 return
260
261 path = os.path.relpath(doctree.get('source'), app.builder.srcdir)
262 repository = app.config.basf2_repository
263 if not repository:
264 return
265
266 commit = app.config.basf2_commitid
267 context["source_url"] = f"{repository}/-/blob/main/{path}"
268 if commit:
269 context["source_url"] = f"{repository}/-/blob/{commit}/{path}"
270
271
272def gitlab_issue_role(role, rawtext, text, lineno, inliner, options=None, content=None):
273 if content is None:
274 content = []
275 if options is None:
276 options = {}
277 issue_url = inliner.document.settings.env.app.config.basf2_issues
278 if not issue_url:
279 return [nodes.literal(rawtext, text=text, language=None)], []
280
281 url = f"{issue_url}/{text}"
282 return [nodes.reference(rawtext, text=text, refuri=url)], []
283
284
285def setup(app):
286 import basf2
287 basf2.logging.log_level = basf2.LogLevel.WARNING
288
289 app.add_config_value("basf2_repository", "", True)
290 app.add_config_value("basf2_commitid", "", True)
291 app.add_config_value("basf2_issues", "", True)
292 app.add_domain(Basf2Domain)
293 app.add_directive("b2-modules", ModuleListDirective)
294 app.add_directive("b2-variables", VariableListDirective)
295 app.add_directive("docstring", RenderDocstring)
296 app.add_role("issue", gitlab_issue_role)
297 app.connect('html-page-context', html_page_context)
298
299 # Sadly sphinx does not seem to add labels to custom indices ... :/
300 StandardDomain.initial_data["labels"]["b2-modindex"] = ("b2-modindex", "", "basf2 Module Index")
301 StandardDomain.initial_data["labels"]["b2-varindex"] = ("b2-varindex", "", "basf2 Variable Index")
302 StandardDomain.initial_data["anonlabels"]["b2-modindex"] = ("b2-modindex", "")
303 StandardDomain.initial_data["anonlabels"]["b2-varindex"] = ("b2-varindex", "")
304 return {'version': 0.2}
static Manager & Instance()
get singleton instance.
Definition: Manager.cc:25