Belle II Software  release-08-01-10
utils.py
1 #!/usr/bin/env python3
2 
3 
10 
11 """
12 basf2.utils - Helper functions for printing basf2 objects
13 ---------------------------------------------------------
14 
15 This modules contains some utility functions used by basf2, mainly for printing
16 things.
17 """
18 
19 import inspect as _inspect
20 from shutil import get_terminal_size as _get_terminal_size
21 import textwrap as _textwrap
22 import pybasf2
23 
24 
25 def get_terminal_width():
26  """
27  Returns width of terminal in characters, or 80 if unknown.
28  """
29  return _get_terminal_size(fallback=(80, 24)).columns
30 
31 
32 def pretty_print_table(table, column_widths, first_row_is_heading=True, transform=None, min_flexible_width=10, *,
33  hline_formatter=None):
34  """
35  Pretty print a given table, by using available terminal size and
36  word wrapping fields as needed.
37 
38  Parameters:
39  table: A 2d list of table fields. Each row must have the same length.
40 
41  column_width: list of column widths, needs to be of same length as rows
42  in 'table'. Available fields are
43 
44  ``-n``
45  as needed, up to n characters, word wrap if longer
46 
47  ``0``
48  as long as needed, no wrapping
49 
50  ``n``
51  n characters (fixed)
52 
53  ``*``
54  use all available space, good for description fields.
55  If more than one column has a * they all get equal width
56 
57  ``+``
58  use all available space but at least the actual width. Only useful
59  to make the table span the full width of the terminal
60 
61  The column width can also start with ``>``, ``<`` or ``^`` in which case
62  it will be right, left or center aligned.
63 
64  first_row_is_heading: header specifies if we should take the first row
65  as table header and offset it a bit
66 
67  transform: either None or a callback function which takes three
68  arguments
69 
70  1. the elements of the row as a list
71  2. second the width of each column (without separator)
72  3. the preformatted text line.
73 
74  It should return a string representing the final line to be
75  printed.
76 
77  min_flexible_width: the minimum amount of characters for every column
78  marked with *
79 
80  hline_formatter: A callable function to format horizontal lines (above and
81  below the table header). Should be a callback with one parameter for
82  the total width of the table in characters and return a string that
83  is the horizontal line. If None is returned no line is printed.
84 
85  If argument is not given or given as None the default of printing '-'
86  signs are printed over the whole table width is used.
87 
88  .. versionchanged:: after release 5
89  Added support for column alignment
90  """
91 
92  # figure out how much space we need for each column (without separators)
93  act_column_widths = [len(cell) for cell in table[0]]
94  for row in table:
95  for (col, cell) in enumerate(row):
96  act_column_widths[col] = max(len(str(cell)), act_column_widths[col])
97 
98  # adjust act_column_widths to comply with user-specified widths
99  total_used_width = 0
100  long_columns = [] # index of * column, if found
101  # alignment character of the column following str.format
102  align = []
103  for (col, opt) in enumerate(column_widths):
104  # check if our column is aligned
105  if isinstance(opt, str) and opt[0] in "<>^":
106  align.append(opt[0])
107  opt = opt[1:]
108  else:
109  align.append('')
110  # and try to convert option to an int
111  try:
112  opt = int(opt)
113  except ValueError:
114  pass
115 
116  if opt == '*':
117  # handled after other fields are set
118  long_columns.append(col)
119  continue
120  elif opt == "+":
121  # handled after other fields are set. Distinguish from * by using by
122  # using negative indices
123  long_columns.append(- col - 1)
124  continue
125  elif isinstance(opt, int) and opt > 0:
126  # fixed width
127  act_column_widths[col] = opt
128  elif isinstance(opt, int) and opt == 0:
129  # as long as needed, nothing to do
130  pass
131  elif isinstance(opt, int) and opt < 0:
132  # width may be at most 'opt'
133  act_column_widths[col] = min(act_column_widths[col], -opt)
134  else:
135  print('Invalid column_widths option "' + str(opt) + '"')
136  return
137  total_used_width += act_column_widths[col]
138 
139  # add separators
140  total_used_width += len(act_column_widths) - 1
141 
142  if long_columns:
143  remaining_space = max(get_terminal_width() - total_used_width, len(long_columns) * min_flexible_width)
144  # ok split the table into even parts but divide up the remainder
145  col_width, remainder = divmod(remaining_space, len(long_columns))
146  for i, col in enumerate(long_columns):
147  size = col_width + (1 if i < remainder else 0)
148  if col < 0:
149  # negative index: a '+' specifier: make column large but at
150  # least as wide as content. So convert column to positive and
151  # set the width
152  col = -1 - col
153  act_column_widths[col] = max(size, act_column_widths[col])
154  # if we are larger than we should be add the amount to the total
155  # table width
156  total_used_width += act_column_widths[col] - size
157  else:
158  act_column_widths[col] = size
159 
160  total_used_width += remaining_space
161 
162  format_string = ' '.join(['{:%s%d}' % opt for opt in zip(align, act_column_widths[:-1])])
163  # don't print extra spaces at end of each line unless it's specifically aligned
164  if not align[-1]:
165  format_string += ' {}'
166  else:
167  format_string += ' {:%s%d}' % (align[-1], act_column_widths[-1])
168 
169  if hline_formatter is not None:
170  hline = hline_formatter(total_used_width)
171  else:
172  hline = total_used_width * "-"
173 
174  # print table
175  if first_row_is_heading and hline is not None:
176  print(hline)
177 
178  header_shown = False
179  for row in table:
180  # use automatic word wrapping on module description (last field)
181  wrapped_row = [_textwrap.wrap(str(row[i]), width) for (i, width) in
182  enumerate(act_column_widths)]
183  max_lines = max([len(col) for col in wrapped_row])
184  for line in range(max_lines):
185  for (i, cell) in enumerate(row):
186  if line < len(wrapped_row[i]):
187  row[i] = wrapped_row[i][line]
188  else:
189  row[i] = ''
190  line = format_string.format(*row)
191  if transform is not None:
192  line = transform(row, act_column_widths, line)
193  print(line)
194 
195  if not header_shown and first_row_is_heading and hline is not None:
196  print(hline)
197  header_shown = True
198 
199 
200 def pretty_print_description_list(rows):
201  """
202  Given a list of 2-tuples, print a nicely formatted description list.
203  Rows with only one entry are interpreted as sub-headings
204  """
205  term_width = get_terminal_width()
206  # indentation width
207  module_width = 24
208  # text wrapper class to format description to terminal width
209  wrapper = _textwrap.TextWrapper(width=term_width, initial_indent="",
210  subsequent_indent=" " * (module_width))
211 
212  useColors = pybasf2.LogPythonInterface.terminal_supports_colors()
213 
214  def bold(text):
215  """Use ANSI sequence to show string in bold"""
216  if useColors:
217  return '\x1b[1m' + text + '\x1b[0m'
218  return text
219 
220  print('')
221  print(term_width * '-')
222  # loop over all modules
223  for row in rows:
224  if len(row) == 1:
225  subheading = row[0]
226  print('')
227  print(bold(subheading).center(term_width))
228  elif len(row) == 2:
229  name, description = row
230  for i, line in enumerate(description.splitlines()):
231  if i == 0:
232  # set indent of the first description line to have enough
233  # space for the module name (at least module_width) and
234  # output a bold module name and the description next to it
235  wrapper.initial_indent = max(module_width, len(name) + 1) * " "
236  print(bold(name.ljust(module_width - 1)), wrapper.fill(line).lstrip())
237  else:
238  # not first line anymore, no module name in front so initial
239  # indent is equal to subsequent indent
240  wrapper.initial_indent = wrapper.subsequent_indent
241  print(wrapper.fill(line))
242  else:
243  name, description, vartype = row
244  for i, line in enumerate(description.splitlines()):
245  if i == 0:
246  wrapper.initial_indent = max(module_width, len(name + vartype) + 4) * " "
247  print(bold((name+' ['+vartype+']').ljust(module_width - 1)), wrapper.fill(line).lstrip())
248  else:
249  wrapper.initial_indent = wrapper.subsequent_indent
250  print(wrapper.fill(line))
251 
252  print(term_width * '-')
253  print('')
254 
255 
256 def print_all_modules(moduleList, package=''):
257  """
258  Loop over the list of available modules,
259  register them and print their information
260  """
261 
262  fail = False
263 
264  modules = []
265  for moduleName in moduleList:
266  try:
267  current_module = pybasf2._register_module(moduleName)
268  if package == '' or current_module.package() == package:
269  modules.append((current_module.package(), moduleName, current_module.description()))
270  except pybasf2.ModuleNotCreatedError:
271  pybasf2.B2ERROR(f'The module {moduleName} could not be loaded.')
272  fail = True
273  except Exception as e:
274  pybasf2.B2ERROR(f'An exception occurred when trying to load the module {moduleName}: {e}')
275  fail = True
276 
277  table = []
278  current_package = ''
279  for (packageName, moduleName, description) in sorted(modules):
280  if current_package != packageName:
281  current_package = packageName
282  table.append((current_package,))
283  table.append((moduleName, description))
284  if package != '' and len(table) == 0:
285  pybasf2.B2FATAL('Print module information: No module or package named "' +
286  package + '" found!')
287 
288  pretty_print_description_list(table)
289 
290  print('To show detailed information on a module, including its parameters,')
291  print("type \'basf2 -m ModuleName\'. Use \'basf2 -m package\' to only list")
292  print('modules belonging to a given package.')
293 
294  if fail:
295  pybasf2.B2FATAL("One or more modules could not be loaded. Please check the "
296  "following ERROR messages and contact the responsible authors.")
297 
298 
299 def print_params(module, print_values=True, shared_lib_path=None):
300  """
301  This function prints parameter information
302 
303  Parameters:
304  module: Print the parameter information of this module
305  print_values: Set it to True to print the current values of the parameters
306  shared_lib_path: The path of the shared library from which the module was
307  loaded
308  """
309 
310  print('')
311  print('=' * (len(module.name()) + 4))
312  print(' %s' % module.name())
313  print('=' * (len(module.name()) + 4))
314  print('Description: %s' % module.description())
315  if shared_lib_path is not None:
316  print('Found in: %s' % shared_lib_path)
317  print('Package: %s' % module.package())
318 
319  # gather output data in table
320  output = []
321  if print_values:
322  output.append([
323  'Parameter',
324  'Type',
325  'Default',
326  'Current',
327  'Steering',
328  'Description'])
329  else:
330  output.append(['Parameter', 'Type', 'Default', 'Description'])
331 
332  has_forced_params = False
333  paramList = module.available_params()
334  for paramItem in paramList:
335  defaultStr = str(paramItem.default)
336  valueStr = str(paramItem.values)
337  forceString = ''
338  if paramItem.forceInSteering:
339  forceString = '*'
340  has_forced_params = True
341  defaultStr = ''
342  if print_values:
343  output.append([
344  forceString + paramItem.name,
345  paramItem.type,
346  defaultStr,
347  valueStr,
348  paramItem.setInSteering,
349  paramItem.description])
350  else:
351  output.append([forceString + paramItem.name, paramItem.type,
352  defaultStr, paramItem.description])
353 
354  column_widths = [-25] * len(output[0])
355  column_widths[2] = -20 # default values
356  column_widths[-1] = '*' # description
357 
358  pretty_print_table(output, column_widths)
359  print('')
360  if has_forced_params:
361  print(' * denotes a required parameter.')
362 
363 
364 def print_path(path, defaults=False, description=False, indentation=0, title=True):
365  """
366  This function prints the modules in the given path and the module
367  parameters.
368  Parameters that are not set by the user are suppressed by default.
369 
370  Parameters:
371  defaults: Set it to True to print also the parameters with default values
372  description: Set to True to print the descriptions of modules and
373  parameters
374  indentation: an internal parameter to indent the whole output
375  (needed for outputting sub-paths)
376  title: show the title string or not (defaults to True)
377  """
378 
379  if title:
380  pybasf2.B2INFO('Modules and parameter settings in the path:')
381  index = 1
382 
383  indentation_string = ' ' * indentation
384 
385  for module in path.modules():
386  out = indentation_string + ' % 2d. % s' % (index, module.name())
387  if description:
388  out += ' #%s' % module.description()
389  print(out)
390  index += 1
391  for param in module.available_params():
392  if not defaults and param.values == param.default:
393  continue
394  out = indentation_string + f' {param.name}={param.values}'
395  if description:
396  out += ' #%s' % param.description
397  print(out)
398 
399  for condition in module.get_all_conditions():
400  out = "\n" + indentation_string + ' ' + str(condition) + ":"
401  print(out)
402  print_path(condition.get_path(), defaults=defaults, description=description, indentation=indentation + 6,
403  title=False)
404 
405 
406 def is_mod_function(mod, func):
407  """Return true if ``func`` is a function and defined in the module ``mod``"""
408  return _inspect.isfunction(func) and _inspect.getmodule(func) == mod
409 
410 
411 def list_functions(mod):
412  """
413  Returns list of function names defined in the given Python module.
414  """
415  return [func.__name__ for func in mod.__dict__.values() if is_mod_function(mod, func)]
416 
417 
418 def pretty_print_module(module, module_name, replacements=None):
419  """Pretty print the contents of a python module.
420  It will print all the functions defined in the given module to the console
421 
422  Arguments:
423  module: instance of the module or name with which it can be found in
424  `sys.modules`
425  module_name: readable module name
426  replacements (dict): dictionary containing text replacements: Every
427  occurrence of any key in the function signature will be replaced by
428  its value
429  """
430  from terminal_utils import Pager
431  desc_list = []
432 
433  if replacements is None:
434  replacements = {}
435 
436  # allow mod to be just the name of the module
437  if isinstance(module, str):
438  import sys
439  module = sys.modules[module]
440 
441  for function_name in sorted(list_functions(module), key=lambda x: x.lower()):
442  function = getattr(module, function_name)
443  signature = _inspect.formatargspec(*_inspect.getfullargspec(function))
444  for key, value in replacements.items():
445  signature = signature.replace(key, value)
446  desc_list.append((function.__name__, signature + '\n' + function.__doc__))
447 
448  with Pager('List of available functions in ' + module_name, True):
449  pretty_print_description_list(desc_list)