Belle II Software  light-2403-persian
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 
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 
94 class 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 
186 class 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 
250 def 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 
272 def 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 
285 def 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