Belle II Software development
utils.py
1#!/usr/bin/env python3
2
3
10
11"""
12basf2.utils - Helper functions for printing basf2 objects
13---------------------------------------------------------
14
15This modules contains some utility functions used by basf2, mainly for printing
16things.
17"""
18
19import inspect as _inspect
20from shutil import get_terminal_size as _get_terminal_size
21import textwrap as _textwrap
22import pybasf2
23
24
25def 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
32def 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
200def 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
256def 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
299def 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(f' {module.name()}')
313 print('=' * (len(module.name()) + 4))
314 print(f'Description: {module.description()}')
315 if shared_lib_path is not None:
316 print(f'Found in: {shared_lib_path}')
317 print(f'Package: {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
364def 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 += f' #{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 += f' #{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
406def 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
411def 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
418def 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 = str(_inspect.signature(function))
444 for key, value in replacements.items():
445 signature = signature.replace(key, value)
446 function_doc = function.__doc__
447 if not function_doc:
448 function_doc = '(no documentation)'
449 desc_list.append((function.__name__, signature + '\n' + function_doc))
450
451 with Pager('List of available functions in ' + module_name, True):
452 pretty_print_description_list(desc_list)