Belle II Software  release-06-00-14
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  else:
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 
243  print(term_width * '-')
244  print('')
245 
246 
247 def print_all_modules(moduleList, package=''):
248  """
249  Loop over the list of available modules,
250  register them and print their information
251  """
252 
253  fail = False
254 
255  modules = []
256  for moduleName in moduleList:
257  try:
258  current_module = pybasf2._register_module(moduleName)
259  if package == '' or current_module.package() == package:
260  modules.append((current_module.package(), moduleName, current_module.description()))
261  except pybasf2.ModuleNotCreatedError:
262  pybasf2.B2ERROR(f'The module {moduleName} could not be loaded.')
263  fail = True
264  except Exception as e:
265  pybasf2.B2ERROR(f'An exception occurred when trying to load the module {moduleName}: {e}')
266  fail = True
267 
268  table = []
269  current_package = ''
270  for (packageName, moduleName, description) in sorted(modules):
271  if current_package != packageName:
272  current_package = packageName
273  table.append((current_package,))
274  table.append((moduleName, description))
275  if package != '' and len(table) == 0:
276  pybasf2.B2FATAL('Print module information: No module or package named "' +
277  package + '" found!')
278 
279  pretty_print_description_list(table)
280 
281  print('To show detailed information on a module, including its parameters,')
282  print("type \'basf2 -m ModuleName\'. Use \'basf2 -m package\' to only list")
283  print('modules belonging to a given package.')
284 
285  if fail:
286  pybasf2.B2FATAL("One or more modules could not be loaded. Please check the "
287  "following ERROR messages and contact the responsible authors.")
288 
289 
290 def print_params(module, print_values=True, shared_lib_path=None):
291  """
292  This function prints parameter information
293 
294  Parameters:
295  module: Print the parameter information of this module
296  print_values: Set it to True to print the current values of the parameters
297  shared_lib_path: The path of the shared library from which the module was
298  loaded
299  """
300 
301  print('')
302  print('=' * (len(module.name()) + 4))
303  print(' %s' % module.name())
304  print('=' * (len(module.name()) + 4))
305  print('Description: %s' % module.description())
306  if shared_lib_path is not None:
307  print('Found in: %s' % shared_lib_path)
308  print('Package: %s' % module.package())
309 
310  # gather output data in table
311  output = []
312  if print_values:
313  output.append([
314  'Parameter',
315  'Type',
316  'Default',
317  'Current',
318  'Steering',
319  'Description'])
320  else:
321  output.append(['Parameter', 'Type', 'Default', 'Description'])
322 
323  has_forced_params = False
324  paramList = module.available_params()
325  for paramItem in paramList:
326  defaultStr = str(paramItem.default)
327  valueStr = str(paramItem.values)
328  forceString = ''
329  if paramItem.forceInSteering:
330  forceString = '*'
331  has_forced_params = True
332  defaultStr = ''
333  if print_values:
334  output.append([
335  forceString + paramItem.name,
336  paramItem.type,
337  defaultStr,
338  valueStr,
339  paramItem.setInSteering,
340  paramItem.description])
341  else:
342  output.append([forceString + paramItem.name, paramItem.type,
343  defaultStr, paramItem.description])
344 
345  column_widths = [-25] * len(output[0])
346  column_widths[2] = -20 # default values
347  column_widths[-1] = '*' # description
348 
349  pretty_print_table(output, column_widths)
350  print('')
351  if has_forced_params:
352  print(' * denotes a required parameter.')
353 
354 
355 def print_path(path, defaults=False, description=False, indentation=0, title=True):
356  """
357  This function prints the modules in the given path and the module
358  parameters.
359  Parameters that are not set by the user are suppressed by default.
360 
361  Parameters:
362  defaults: Set it to True to print also the parameters with default values
363  description: Set to True to print the descriptions of modules and
364  parameters
365  indentation: an internal parameter to indent the whole output
366  (needed for outputting sub-paths)
367  title: show the title string or not (defaults to True)
368  """
369 
370  if title:
371  pybasf2.B2INFO('Modules and parameter settings in the path:')
372  index = 1
373 
374  indentation_string = ' ' * indentation
375 
376  for module in path.modules():
377  out = indentation_string + ' % 2d. % s' % (index, module.name())
378  if description:
379  out += ' #%s' % module.description()
380  print(out)
381  index += 1
382  for param in module.available_params():
383  if not defaults and param.values == param.default:
384  continue
385  out = indentation_string + f' {param.name}={param.values}'
386  if description:
387  out += ' #%s' % param.description
388  print(out)
389 
390  for condition in module.get_all_conditions():
391  out = "\n" + indentation_string + ' ' + str(condition) + ":"
392  print(out)
393  print_path(condition.get_path(), defaults=defaults, description=description, indentation=indentation + 6,
394  title=False)
395 
396 
397 def is_mod_function(mod, func):
398  """Return true if ``func`` is a function and defined in the module ``mod``"""
399  return _inspect.isfunction(func) and _inspect.getmodule(func) == mod
400 
401 
402 def list_functions(mod):
403  """
404  Returns list of function names defined in the given Python module.
405  """
406  return [func.__name__ for func in mod.__dict__.values() if is_mod_function(mod, func)]
407 
408 
409 def pretty_print_module(module, module_name, replacements=None):
410  """Pretty print the contents of a python module.
411  It will print all the functions defined in the given module to the console
412 
413  Arguments:
414  module: instance of the module or name with which it can be found in
415  `sys.modules`
416  module_name: readable module name
417  replacements (dict): dictionary containing text replacements: Every
418  occurrence of any key in the function signature will be replaced by
419  its value
420  """
421  from terminal_utils import Pager
422  desc_list = []
423 
424  if replacements is None:
425  replacements = {}
426 
427  # allow mod to be just the name of the module
428  if isinstance(module, str):
429  import sys
430  module = sys.modules[module]
431 
432  for function_name in sorted(list_functions(module), key=lambda x: x.lower()):
433  function = getattr(module, function_name)
434  signature = _inspect.formatargspec(*_inspect.getfullargspec(function))
435  for key, value in replacements.items():
436  signature = signature.replace(key, value)
437  desc_list.append((function.__name__, signature + '\n' + function.__doc__))
438 
439  with Pager('List of available functions in ' + module_name, True):
440  pretty_print_description_list(desc_list)