comparison frontends/src/jp/common.py @ 2493:984792a451bc

jp (common/table): a column can be hidden + fix for empty tables - avoid crash when table is empty - a column can now be hidden on display, this can be useful if one data is needed (e.g. to change color of an other column with a filter), but we don't want to display it - filters have a new argument with all data of a row (for the same reason as above, one column can be used to change display of other column)
author Goffi <goffi@goffi.org>
date Wed, 28 Feb 2018 18:28:39 +0100
parents 0046283a285d
children 21d43eab3fb9
comparison
equal deleted inserted replaced
2492:fcf0ae8102b8 2493:984792a451bc
23 from sat.tools.common import regex 23 from sat.tools.common import regex
24 from sat.tools.common import uri 24 from sat.tools.common import uri
25 from sat.tools.common.ansi import ANSI as A 25 from sat.tools.common.ansi import ANSI as A
26 from sat.tools import config 26 from sat.tools import config
27 from ConfigParser import NoSectionError, NoOptionError 27 from ConfigParser import NoSectionError, NoOptionError
28 from collections import namedtuple
28 import json 29 import json
29 import os 30 import os
30 import os.path 31 import os.path
31 import time 32 import time
32 import tempfile 33 import tempfile
507 508
508 class Table(object): 509 class Table(object):
509 510
510 def __init__(self, host, data, headers=None, filters=None, use_buffer=False): 511 def __init__(self, host, data, headers=None, filters=None, use_buffer=False):
511 """ 512 """
512 @param data(list[list]): table data 513 @param data(iterable[list]): table data
513 all lines must have the same number of columns 514 all lines must have the same number of columns
514 @param headers(iterable[unicode], None): names/titles of the columns 515 @param headers(iterable[unicode], None): names/titles of the columns
515 if not None, must have same number of columns as data 516 if not None, must have same number of columns as data
516 @param filters(iterable[(callable, unicode)], None): values filters 517 @param filters(iterable[(callable, unicode)], None): values filters
517 the callable will get col value as argument and must return a string 518 the callable will get col value as argument and must return a string
528 self.sizes = [] 529 self.sizes = []
529 # rows countains one list per row with columns values 530 # rows countains one list per row with columns values
530 self.rows = [] 531 self.rows = []
531 532
532 size = None 533 size = None
533 for line in data: 534 if headers:
535 row_cls = namedtuple('RowData', headers)
536 else:
537 row_cls = tuple
538
539 for row_data in data:
534 new_row = [] 540 new_row = []
535 for idx, value in enumerate(line): 541 row_data_list = list(row_data)
542 for idx, value in enumerate(row_data_list):
536 if filters is not None and filters[idx] is not None: 543 if filters is not None and filters[idx] is not None:
537 filter_ = filters[idx] 544 filter_ = filters[idx]
538 if isinstance(filter_, basestring): 545 if isinstance(filter_, basestring):
539 col_value = filter_.format(value) 546 col_value = filter_.format(value)
540 else: 547 else:
541 col_value = filter_(value) 548 col_value = filter_(value, row_cls(*row_data_list))
542 # we count size without ANSI code as they will change length of the string 549 # we count size without ANSI code as they will change length of the string
543 # when it's mostly style/color changes. 550 # when it's mostly style/color changes.
544 col_size = len(regex.ansiRemove(col_value)) 551 col_size = len(regex.ansiRemove(col_value))
545 else: 552 else:
546 col_value = unicode(value) 553 col_value = unicode(value)
608 if headers is None: 615 if headers is None:
609 headers = keys 616 headers = keys
610 filters = [filters.get(k) for k in keys] 617 filters = [filters.get(k) for k in keys]
611 return cls(host, (cls.readDictValues(d, keys, defaults) for d in data), headers, filters) 618 return cls(host, (cls.readDictValues(d, keys, defaults) for d in data), headers, filters)
612 619
613 def _headers(self, head_sep, alignment=u'left', style=None): 620 def _headers(self, head_sep, headers, sizes, alignment=u'left', style=None):
614 """Render headers 621 """Render headers
615 622
616 @param head_sep(unicode): sequence to use as separator 623 @param head_sep(unicode): sequence to use as separator
617 @param alignment(unicode): how to align, can be left, center or right 624 @param alignment(unicode): how to align, can be left, center or right
618 @param style(unicode, iterable[unicode], None): ANSI escape sequences to apply 625 @param style(unicode, iterable[unicode], None): ANSI escape sequences to apply
619 """ 626 @param headers(list[unicode]): headers to show
620 headers = [] 627 @param sizes(list[int]): sizes of columns
628 """
629 rendered_headers = []
621 if isinstance(style, basestring): 630 if isinstance(style, basestring):
622 style = [style] 631 style = [style]
623 for idx, header in enumerate(self.headers): 632 for idx, header in enumerate(headers):
624 size = self.sizes[idx] 633 size = sizes[idx]
625 if alignment == u'left': 634 if alignment == u'left':
626 rendered = header[:size].ljust(size) 635 rendered = header[:size].ljust(size)
627 elif alignment == u'center': 636 elif alignment == u'center':
628 rendered = header[:size].center(size) 637 rendered = header[:size].center(size)
629 elif alignment == u'right': 638 elif alignment == u'right':
631 else: 640 else:
632 raise exceptions.InternalError(u'bad alignment argument') 641 raise exceptions.InternalError(u'bad alignment argument')
633 if style: 642 if style:
634 args = style + [rendered] 643 args = style + [rendered]
635 rendered = A.color(*args) 644 rendered = A.color(*args)
636 headers.append(rendered) 645 rendered_headers.append(rendered)
637 return head_sep.join(headers) 646 return head_sep.join(rendered_headers)
638 647
639 def _disp(self, data): 648 def _disp(self, data):
640 """output data (can be either bufferised or printed)""" 649 """output data (can be either bufferised or printed)"""
641 if self._buffer is not None: 650 if self._buffer is not None:
642 self._buffer.append(data) 651 self._buffer.append(data)
647 head_alignment = u'left', 656 head_alignment = u'left',
648 columns_alignment = u'left', 657 columns_alignment = u'left',
649 head_style = None, 658 head_style = None,
650 show_header=True, 659 show_header=True,
651 show_borders=True, 660 show_borders=True,
661 hide_cols=None,
652 col_sep=u' │ ', 662 col_sep=u' │ ',
653 top_left=u'┌', 663 top_left=u'┌',
654 top=u'─', 664 top=u'─',
655 top_sep=u'─┬─', 665 top_sep=u'─┬─',
656 top_right=u'┐', 666 top_right=u'┐',
668 ): 678 ):
669 """Print the table 679 """Print the table
670 680
671 @param show_header(bool): True if header need no be shown 681 @param show_header(bool): True if header need no be shown
672 @param show_borders(bool): True if borders need no be shown 682 @param show_borders(bool): True if borders need no be shown
683 @param hide_cols(None, iterable(unicode)): columns which should not be displayed
673 @param head_alignment(unicode): how to align headers, can be left, center or right 684 @param head_alignment(unicode): how to align headers, can be left, center or right
674 @param columns_alignment(unicode): how to align columns, can be left, center or right 685 @param columns_alignment(unicode): how to align columns, can be left, center or right
675 @param col_sep(unicode): separator betweens columns 686 @param col_sep(unicode): separator betweens columns
676 @param head_line(unicode): character to use to make line under head 687 @param head_line(unicode): character to use to make line under head
677 @param disp(callable, None): method to use to display the table 688 @param disp(callable, None): method to use to display the table
678 None to use self.host.disp 689 None to use self.host.disp
679 """ 690 """
691 if not self.sizes:
692 # the table is empty
693 return
680 col_sep_size = len(regex.ansiRemove(col_sep)) 694 col_sep_size = len(regex.ansiRemove(col_sep))
695
696 # if we have columns to hide, we remove them from headers and size
697 if not hide_cols:
698 headers = self.headers
699 sizes = self.sizes
700 else:
701 headers = list(self.headers)
702 sizes = self.sizes[:]
703 ignore_idx = [headers.index(to_hide) for to_hide in hide_cols]
704 for to_hide in hide_cols:
705 hide_idx = headers.index(to_hide)
706 del headers[hide_idx]
707 del sizes[hide_idx]
708
681 if right is None: 709 if right is None:
682 right = left 710 right = left
683 if top_sep is None: 711 if top_sep is None:
684 top_sep = col_sep_size * top 712 top_sep = col_sep_size * top
685 if head_sep is None: 713 if head_sep is None:
692 left = right = head_line_left = head_line_right = u'' 720 left = right = head_line_left = head_line_right = u''
693 # top border 721 # top border
694 if show_borders: 722 if show_borders:
695 self._disp( 723 self._disp(
696 top_left 724 top_left
697 + top_sep.join([top*size for size in self.sizes]) 725 + top_sep.join([top*size for size in sizes])
698 + top_right 726 + top_right
699 ) 727 )
700 728
701 # headers 729 # headers
702 if show_header: 730 if show_header:
703 self._disp( 731 self._disp(
704 left 732 left
705 + self._headers(head_sep, head_alignment, head_style) 733 + self._headers(head_sep, headers, sizes, head_alignment, head_style)
706 + right 734 + right
707 ) 735 )
708 # header line 736 # header line
709 self._disp( 737 self._disp(
710 head_line_left 738 head_line_left
711 + head_line_sep.join([head_line*size for size in self.sizes]) 739 + head_line_sep.join([head_line*size for size in sizes])
712 + head_line_right 740 + head_line_right
713 ) 741 )
714 742
715 # content 743 # content
716 if columns_alignment == u'left': 744 if columns_alignment == u'left':
717 alignment = lambda idx, s: ansi_ljust(s, self.sizes[idx]) 745 alignment = lambda idx, s: ansi_ljust(s, sizes[idx])
718 elif columns_alignment == u'center': 746 elif columns_alignment == u'center':
719 alignment = lambda idx, s: ansi_center(s, self.sizes[idx]) 747 alignment = lambda idx, s: ansi_center(s, sizes[idx])
720 elif columns_alignment == u'right': 748 elif columns_alignment == u'right':
721 alignment = lambda idx, s: ansi_rjust(s, self.sizes[idx]) 749 alignment = lambda idx, s: ansi_rjust(s, sizes[idx])
722 else: 750 else:
723 raise exceptions.InternalError(u'bad columns alignment argument') 751 raise exceptions.InternalError(u'bad columns alignment argument')
724 752
725 for row in self.rows: 753 for row in self.rows:
754 if hide_cols:
755 row = [v for idx,v in enumerate(row) if idx not in ignore_idx]
726 self._disp(left + col_sep.join([alignment(idx,c) for idx,c in enumerate(row)]) + right) 756 self._disp(left + col_sep.join([alignment(idx,c) for idx,c in enumerate(row)]) + right)
727 757
728 if show_borders: 758 if show_borders:
729 # bottom border 759 # bottom border
730 self._disp( 760 self._disp(
731 bottom_left 761 bottom_left
732 + bottom_sep.join([bottom*size for size in self.sizes]) 762 + bottom_sep.join([bottom*size for size in sizes])
733 + bottom_right 763 + bottom_right
734 ) 764 )
735 # we return self so string can be used after display (table.display().string) 765 # we return self so string can be used after display (table.display().string)
736 return self 766 return self
737 767
738 def display_blank(self, **kwargs): 768 def display_blank(self, **kwargs):
739 """Display table without visible borders""" 769 """Display table without visible borders"""
740 kwargs_ = {'col_sep':u' ', 'head_line_sep':u' ', 'show_borders':False} 770 kwargs_ = {'col_sep':u' ', 'head_line_sep':u' ', 'show_borders':False}
741 kwargs_.update(kwargs) 771 kwargs_.update(kwargs)
742 return self.display(self, 772 return self.display(**kwargs_)
743 **kwargs_
744 )