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