#!/usr/bin/python3 """Misc utilities that did not fit anywhere nicely. These are intended for internal use only: The API is subject to change. """ import functools import itertools def debug(func): """A function decorator that will print function calls and their results""" @functools.wraps(func) def wrapper(*args, **kwargs): print("calling {0}(args={1}, kwargs={2})".format(func, args, kwargs)) res = func(*args, **kwargs) print("{f}(args={a}, kwargs={kw}) => {res}".format(f=func.__name__, a=args, kw=kwargs, res=res)) return res return wrapper def unique_everseen(iterable, key=None): """List unique elements, preserving order. Remember all elements ever seen >>> list(unique_everseen('AAAABBBCCDAABBB')) ['A', 'B', 'C', 'D'] >>> list(unique_everseen('ABBCcAD', str.lower)) ['A', 'B', 'C', 'D'] """ seen = set() seen_add = seen.add if key is None: for element in itertools.filterfalse(seen.__contains__, iterable): seen_add(element) yield element else: for element in iterable: k = key(element) if k not in seen: seen_add(k) yield element def human(obj): """Equivalent to str(), but more humane This will attempt to give a 'human' value of the object - which is usually subtly different from str(). If the object does not define a '__human__' method, it will resort to the normal str() """ if hasattr(obj, '__human__'): return obj.__human__() return str(obj) def select_columns(table, columns): """Extract the named columns from a dict-of-dicts. Row order will be preserved. Note: If the original table is a Table class, the result will no longer be updateable, and only cached values can be retrieved. """ res = dict() for rowkey, oldrow in table.items(): newrow = dict() for colname in columns: if colname in oldrow: newrow[colname] = oldrow[colname] if newrow: res[rowkey] = newrow return res def unselect_columns(table, columns): """Return a new table without the listed columns Row order will be preserved. Note: If the original table is a Table class, the result will no longer be updateable, and only cached values can be retrieved. """ res = dict() for rowkey, oldrow in table.items(): newrow = dict() for colname in oldrow.keys(): if colname not in columns: newrow[colname] = oldrow[colname] if newrow: res[rowkey] = newrow return res def filter_table(table, selector): """Filter rows in tables Only rows for which the selector is returning True(ness) will be included in the result. The selector will receive two args: key (for the row) and the row. Row order will be preserved. Note: If the original table is a Table class, the result will no longer be updateable, and only cached values can be retrieved. """ res = dict() for rowkey, row in table.items(): if selector(rowkey, row): res[rowkey] = row return res def sort_table(table, key): """Sort the rows in the table according to the key. The key is expected to be a function which receives the row as a parameter Note: If the original table is a Table class, the result will no longer be updateable, and only cached values can be retrieved. """ res = dict() sorted_keys = sorted(table, key=lambda x: key(table[x])) res = {keyval: table[keyval] for keyval in sorted_keys} return res def format_table(table_rows): """Print a table in a nice human-readable format. The table is expected to be a dict, where each key is the row ID, and the value is a dict. Each row in turn is also a dict, with the key as the column name. This is mostly useful for development - e.g. printing snmp table_rows things, but might be useful for other things too... >>> print(format_table( ... {"1": {"country": "Denmark", ... "language": "Danish", ... "Lego": "quite awesome", ... "Intellibility/Readability": 11}, ... "2": {"country": "Sweden", ... "language": "Swedish", ... "Crazy": True}, ... "8": {"language": "Python", ... "Crazy": 0.5, ... "Intellibility/Readability": True}, ... })) +---------+----------+---------------+---------------------------+-------+ | country | language | Lego | Intellibility/Readability | Crazy | +---------+----------+---------------+---------------------------+-------+ | Denmark | Danish | quite awesome | 11 | | | Sweden | Swedish | | | True | | | Python | | True | 0.5 | +---------+----------+---------------+---------------------------+-------+ """ def column_values(colname): return map(human, [row[colname] if row[colname] is not None else "" for row in table_rows.values() if colname in row]) column_names = list(unique_everseen([fieldname for row in table_rows.values() for fieldname in row.keys() if any(column_values(fieldname))])) # print("Column names:", column_names) column_widths = {colname: max([len(colname)] + \ list(map(len, column_values(colname)))) for colname in column_names} # print("Columnn widths:", column_widths) def horiz_line(vbar="+"): res = vbar for column_name in column_names: res += "-" res += "-" * column_widths[column_name] res += "-" + vbar return res def row_header(column_names): res = '|' for column_name in column_names: res += ' ' + human(column_name).ljust(column_widths[column_name]) res += ' |' return res def row_text(row): res = '|' for column_name in column_names: cellvalue = row.get(column_name) val = human(cellvalue) if cellvalue is not None else "" res += ' ' + val.ljust(column_widths[column_name]) res += ' |' return res fmt = horiz_line() + "\n" fmt += row_header(column_names) + "\n" fmt += horiz_line() + "\n" for _rowid, row in table_rows.items(): # TODO: Show the row ID? fmt += row_text(row) + "\n" fmt += horiz_line() return fmt def format_by_row(table_rows): """Get a string representation of a table for human consumption. The table is expected to be a dict, where each key is the row ID, and the value is a dict. Each row in turn is also a dict, with the key as the column name. This lists each row as a sequence of lines, followed by the next row etc. This format is well suited for tables with many columns and/or narrow terminals. >>> print(format_by_row( ... {"1": {"country": "Denmark", ... "language": "Danish", ... "Lego": "quite awesome", ... "Intellibility/Readability": 11}, ... "2": {"country": "Sweden", ... "language": "Swedish", ... "Crazy": True}, ... "8": {"language": "Python", ... "Crazy": 0.5, ... "Intellibility/Readability": True}, ... })) Row: 1 country : Denmark language : Danish Lego : quite awesome Intellibility/Readability : 11 <BLANKLINE> Row: 2 country : Sweden language : Swedish Crazy : True <BLANKLINE> Row: 8 language : Python Crazy : 0.5 Intellibility/Readability : True <BLANKLINE> """ res = "" for rownum, (rowkey, row) in enumerate(table_rows.items(), start=0): if rownum: # Blank lines between rows res += "\n" res += format_one_row(rowkey, row) return res def format_one_row(rowkey, row): """Produce a string representation of one row. This will list the attributes: one per line, with the name followed by the value (separated by a colon) - e.g.: >>> print(format_one_row('Parrot', dict(species='Norwegian Blue', status='Pining'))) Row: Parrot species : Norwegian Blue status : Pining <BLANKLINE> """ res = "Row: " + rowkey + "\n" namelength = max([len(name) for name in row.keys()]) for name, value in row.items(): res += " " + name.ljust(namelength) + " : " + human(value) + "\n" return res def _run_tests(): import doctest import sys fail_count, test_count = doctest.testmod(report=True) if fail_count: raise SystemExit("%d out of %d doc tests failed" % (fail_count, test_count)) print("%s: Doc tests were all OK" % sys.argv[0]) if __name__ == "__main__": _run_tests()