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