comparison frontends/src/jp/common.py @ 2298:276e546b7619

jp (common): new ansi_ljust, ansi_rjust and ansi_center command + table: - ansi_ljust, ansi_rjust and ansi_center are equivalent to string.ljust, rjust and center, but handle strings with ANSI escape codes - new Table class to handle tabular data and display them with ASCII borders/separations. - Table is quite flexible, filters can be applied to data, header can be shown or not, columns/headers can be left/right aligned. - table can be displayed without decoration to align text data, using display_blank helping method
author Goffi <goffi@goffi.org>
date Sun, 02 Jul 2017 20:09:21 +0200
parents 4bc9a2c2d6c9
children 07deebea71f3
comparison
equal deleted inserted replaced
2297:ad2a8e8b52da 2298:276e546b7619
17 # You should have received a copy of the GNU Affero General Public License 17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. 18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 19
20 from sat_frontends.jp.constants import Const as C 20 from sat_frontends.jp.constants import Const as C
21 from sat.core.i18n import _ 21 from sat.core.i18n import _
22 from sat.core import exceptions
22 from sat.tools.common import regex 23 from sat.tools.common import regex
23 from sat.tools.common import uri 24 from sat.tools.common import uri
25 from sat.tools.common.ansi import ANSI as A
24 from sat.tools import config 26 from sat.tools import config
25 from ConfigParser import NoSectionError, NoOptionError 27 from ConfigParser import NoSectionError, NoOptionError
26 import json 28 import json
27 import os 29 import os
28 import os.path 30 import os.path
44 } 46 }
45 47
46 SECURE_UNLINK_MAX = 10 48 SECURE_UNLINK_MAX = 10
47 SECURE_UNLINK_DIR = ".backup" 49 SECURE_UNLINK_DIR = ".backup"
48 METADATA_SUFF = '_metadata.json' 50 METADATA_SUFF = '_metadata.json'
51
52
53 def ansi_ljust(s, width):
54 """ljust method handling ANSI escape codes"""
55 cleaned = regex.ansiRemove(s)
56 return s + u' ' * (width - len(cleaned))
57
58
59 def ansi_center(s, width):
60 """ljust method handling ANSI escape codes"""
61 cleaned = regex.ansiRemove(s)
62 diff = width - len(cleaned)
63 half = diff/2
64 return half * u' ' + s + (half + diff % 2) * u' '
65
66
67 def ansi_rjust(s, width):
68 """ljust method handling ANSI escape codes"""
69 cleaned = regex.ansiRemove(s)
70 return u' ' * (width - len(cleaned)) + s
49 71
50 72
51 def getTmpDir(sat_conf, cat_dir, sub_dir=None): 73 def getTmpDir(sat_conf, cat_dir, sub_dir=None):
52 """Return directory used to store temporary files 74 """Return directory used to store temporary files
53 75
472 494
473 if self.use_metadata: 495 if self.use_metadata:
474 return pubsub_service, pubsub_node, pubsub_item, content_file_path, content_file_obj, metadata 496 return pubsub_service, pubsub_node, pubsub_item, content_file_path, content_file_obj, metadata
475 else: 497 else:
476 return pubsub_service, pubsub_node, pubsub_item, content_file_path, content_file_obj 498 return pubsub_service, pubsub_node, pubsub_item, content_file_path, content_file_obj
499
500
501 class Table(object):
502
503 def __init__(self, host, data, headers=None, filters=None):
504 """
505 @param data(list[list]): table data
506 all lines must have the same number of columns
507 @param headers(iterable[unicode], None): names/titles of the columns
508 if not None, must have same number of columns as data
509 @param filters(iterable[(callable, unicode)], None): values filters
510 the callable will get col value as argument and must return a string
511 if it's unicode, it will be used with .format and must countain u'{}' which will be replaced with the string
512 if not None, must have same number of columns as data
513 """
514 self.host = host
515 # headers are columns names/titles, can be None
516 self.headers = headers
517 # sizes fof columns without headers,
518 # headers may be larger
519 self.sizes = []
520 # rows countains one list per row with columns values
521 self.rows = []
522
523 size = None
524 for line in data:
525 new_row = []
526 for idx, value in enumerate(line):
527 if filters is not None and filters[idx] is not None:
528 filter_ = filters[idx]
529 if isinstance(filter_, basestring):
530 col_value = filter_.format(value)
531 else:
532 col_value = filter_(value)
533 # we count size without ANSI code as they will change length of the string
534 # when it's mostly style/color changes.
535 col_size = len(regex.ansiRemove(col_value))
536 else:
537 col_value = unicode(value)
538 col_size = len(col_value)
539 new_row.append(col_value)
540 if size is None:
541 self.sizes.append(col_size)
542 else:
543 self.sizes[idx] = max(self.sizes[idx], col_size)
544 if size is None:
545 size = len(new_row)
546 if headers is not None and len(headers) != size:
547 raise exceptions.DataError(u'headers size is not coherent with rows')
548 else:
549 if len(new_row) != size:
550 raise exceptions.DataError(u'rows size is not coherent')
551 self.rows.append(new_row)
552
553 @staticmethod
554 def readDictValues(data, keys, defaults=None):
555 if defaults is None:
556 defaults = {}
557 for key in keys:
558 try:
559 yield data[key]
560 except KeyError as e:
561 default = defaults.get(key)
562 if default is not None:
563 yield default
564 else:
565 raise e
566
567 @classmethod
568 def fromDict(cls, host, data, keys=None, headers=None, filters=None, defaults=None):
569 """Prepare a table to display it
570
571 the whole data will be read and kept into memory,
572 to be printed
573 @param data(list[dict[unicode, unicode]]): data to create the table from
574 @param keys(iterable[unicode], None): keys to get
575 if None, all keys will be used
576 @param headers(iterable[unicode], None): name of the columns
577 names must be in same order as keys
578 @param filters(dict[unicode, (callable,unicode)), None): filter to use on values
579 keys correspond to keys to filter, and value is a callable or unicode which
580 will get the value as argument and must return a string
581 @param defaults(dict[unicode, unicode]): default value to use
582 if None, an exception will be raised if not value is found
583 """
584 if keys is None and headers is not None:
585 # FIXME: keys are not needed with OrderedDict,
586 raise exceptions.DataError(u'You must specify keys order to used headers')
587 if keys is None:
588 keys = data[0].keys()
589 if headers is None:
590 headers = keys
591 filters = [filters.get(k) for k in keys]
592 return cls(host, (cls.readDictValues(d, keys, defaults) for d in data), headers, filters)
593
594 def _headers(self, head_sep, alignment=u'left', style=None):
595 """Render headers
596
597 @param head_sep(unicode): sequence to use as separator
598 @param alignment(unicode): how to align, can be left, center or right
599 @param style(unicode, iterable[unicode], None): ANSI escape sequences to apply
600 """
601 headers = []
602 if isinstance(style, basestring):
603 style = [style]
604 for idx, header in enumerate(self.headers):
605 size = self.sizes[idx]
606 if alignment == u'left':
607 rendered = header[:size].ljust(size)
608 elif alignment == u'center':
609 rendered = header[:size].center(size)
610 elif alignment == u'right':
611 rendered = header[:size].rjust(size)
612 else:
613 raise exceptions.InternalError(u'bad alignment argument')
614 if style:
615 args = style + [rendered]
616 rendered = A.color(*args)
617 headers.append(rendered)
618 return head_sep.join(headers)
619
620 def display(self,
621 head_alignment = u'left',
622 columns_alignment = u'left',
623 head_style = None,
624 show_header=True,
625 show_borders=True,
626 col_sep=u' │ ',
627 top_left=u'┌',
628 top=u'─',
629 top_sep=u'─┬─',
630 top_right=u'┐',
631 left=u'│',
632 right=None,
633 head_sep=None,
634 head_line=u'┄',
635 head_line_left=u'├',
636 head_line_sep=u'┄┼┄',
637 head_line_right=u'┤',
638 bottom_left=u'└',
639 bottom=None,
640 bottom_sep=u'─┴─',
641 bottom_right=u'┘'
642 ):
643 """Print the table
644
645 @param show_header(bool): True if header need no be shown
646 @param show_borders(bool): True if borders need no be shown
647 @param head_alignment(unicode): how to align headers, can be left, center or right
648 @param columns_alignment(unicode): how to align columns, can be left, center or right
649 @param col_sep(unicode): separator betweens columns
650 @param head_line(unicode): character to use to make line under head
651 """
652 col_sep_size = len(regex.ansiRemove(col_sep))
653 if right is None:
654 right = left
655 if top_sep is None:
656 top_sep = col_sep_size * top
657 if head_sep is None:
658 head_sep = col_sep
659 if bottom is None:
660 bottom = top
661 if bottom_sep is None:
662 bottom_sep = col_sep_size * bottom
663 if not show_borders:
664 left = right = head_line_left = head_line_right = u''
665 # top border
666 if show_borders:
667 self.host.disp(
668 top_left
669 + top_sep.join([top*size for size in self.sizes])
670 + top_right
671 )
672
673 # headers
674 if show_header:
675 self.host.disp(
676 left
677 + self._headers(head_sep, head_alignment, head_style)
678 + right
679 )
680 # header line
681 self.host.disp(
682 head_line_left
683 + head_line_sep.join([head_line*size for size in self.sizes])
684 + head_line_right
685 )
686
687 # content
688 if columns_alignment == u'left':
689 alignment = lambda idx, s: ansi_ljust(s, self.sizes[idx])
690 elif columns_alignment == u'center':
691 alignment = lambda idx, s: ansi_center(s, self.sizes[idx])
692 elif columns_alignment == u'right':
693 alignment = lambda idx, s: ansi_rjust(s, self.sizes[idx])
694 else:
695 raise exceptions.InternalError(u'bad columns alignment argument')
696
697 for row in self.rows:
698 self.host.disp(left + col_sep.join([alignment(idx,c) for idx,c in enumerate(row)]) + right)
699
700 if show_borders:
701 # bottom border
702 self.host.disp(
703 bottom_left
704 + bottom_sep.join([bottom*size for size in self.sizes])
705 + bottom_right
706 )
707
708 def display_blank(self, **kwargs):
709 """Display table without visible borders"""
710 kwargs_ = {'col_sep':u' ', 'head_line_sep':u' ', 'show_borders':False}
711 kwargs_.update(kwargs)
712 self.display(self,
713 **kwargs_
714 )