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