Mercurial > libervia-backend
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 ) |