55The general use case is to inherit from TableCreator to create a table class with custom formatting options.
66There are already implemented and ready-to-use examples of this below TableCreator's code.
77"""
8+ import copy
89import functools
910import io
1011from collections import deque
@@ -103,7 +104,7 @@ def __init__(self, cols: Sequence[Column], *, tab_width: int = 4) -> None:
103104 :param tab_width: all tabs will be replaced with this many spaces. If a row's fill_char is a tab,
104105 then it will be converted to one space.
105106 """
106- self .cols = cols
107+ self .cols = copy . copy ( cols )
107108 self .tab_width = tab_width
108109
109110 @staticmethod
@@ -465,8 +466,9 @@ def __init__(self) -> None:
465466 if cell_index == len (self .cols ) - 1 :
466467 row_buf .write (post_line )
467468
468- # Add a newline if this is not the last row
469- row_buf .write ('\n ' )
469+ # Add a newline if this is not the last line
470+ if line_index < total_lines - 1 :
471+ row_buf .write ('\n ' )
470472
471473 return row_buf .getvalue ()
472474
@@ -480,39 +482,78 @@ class SimpleTable(TableCreator):
480482 Implementation of TableCreator which generates a borderless table with an optional divider row after the header.
481483 This class can be used to create the whole table at once or one row at a time.
482484 """
485+ # Spaces between cells
486+ INTER_CELL = 2 * SPACE
487+
483488 def __init__ (self , cols : Sequence [Column ], * , tab_width : int = 4 , divider_char : Optional [str ] = '-' ) -> None :
484489 """
485490 SimpleTable initializer
486491
487492 :param cols: column definitions for this table
488493 :param tab_width: all tabs will be replaced with this many spaces. If a row's fill_char is a tab,
489494 then it will be converted to one space.
490- :param divider_char: optional character used to build the header divider row. If provided, its value must meet the
491- same requirements as fill_char in TableCreator.generate_row() or exceptions will be raised.
492- Set this to None if you don't want a divider row. (Defaults to dash)
495+ :param divider_char: optional character used to build the header divider row. Set this to None if you don't
496+ want a divider row. Defaults to dash. (Cannot be a line breaking character)
497+ :raises: TypeError if fill_char is more than one character (not including ANSI style sequences)
498+ :raises: ValueError if text or fill_char contains an unprintable character
493499 """
500+ if divider_char is not None :
501+ if len (ansi .strip_style (divider_char )) != 1 :
502+ raise TypeError ("Divider character must be exactly one character long" )
503+
504+ divider_char_width = ansi .style_aware_wcswidth (divider_char )
505+ if divider_char_width == - 1 :
506+ raise (ValueError ("Divider character is an unprintable character" ))
507+
494508 super ().__init__ (cols , tab_width = tab_width )
495509 self .divider_char = divider_char
496- self .empty_data = [EMPTY for _ in self .cols ]
497510
498- def generate_header (self ) -> str :
511+ @classmethod
512+ def base_width (cls , num_cols : int ) -> int :
499513 """
500- Generate header with an optional divider row
514+ Utility method to calculate the display width required for a table before data is added to it.
515+ This is useful when determining how wide to make your columns to have a table be a specific width.
516+
517+ :param num_cols: how many columns the table will have
518+ :return: base width
519+ :raises: ValueError if num_cols is less than 1
501520 """
521+ if num_cols < 1 :
522+ raise ValueError ("Column count cannot be less than 1" )
523+
524+ data_str = SPACE
525+ data_width = ansi .style_aware_wcswidth (data_str ) * num_cols
526+
527+ tbl = cls ([Column (data_str )] * num_cols )
528+ data_row = tbl .generate_data_row ([data_str ] * num_cols )
529+
530+ return ansi .style_aware_wcswidth (data_row ) - data_width
531+
532+ def total_width (self ) -> int :
533+ """Calculate the total display width of this table"""
534+ base_width = self .base_width (len (self .cols ))
535+ data_width = sum (col .width for col in self .cols )
536+ return base_width + data_width
537+
538+ def generate_header (self ) -> str :
539+ """Generate table header with an optional divider row"""
502540 header_buf = io .StringIO ()
503541
504542 # Create the header labels
505- if self .divider_char is None :
506- inter_cell = 2 * SPACE
507- else :
508- inter_cell = SPACE * ansi .style_aware_wcswidth (2 * self .divider_char )
509- header = self .generate_row (inter_cell = inter_cell )
543+ header = self .generate_row (inter_cell = self .INTER_CELL )
510544 header_buf .write (header )
511545
512- # Create the divider. Use empty strings for the row_data.
546+ # Create the divider if necessary
513547 if self .divider_char is not None :
514- divider = self .generate_row (row_data = self .empty_data , fill_char = self .divider_char ,
515- inter_cell = (2 * self .divider_char ))
548+ total_width = self .total_width ()
549+ divider_char_width = ansi .style_aware_wcswidth (self .divider_char )
550+
551+ # Make divider as wide as table and use padding if width of
552+ # divider_char does not divide evenly into table width.
553+ divider = self .divider_char * (total_width // divider_char_width )
554+ divider += SPACE * (total_width % divider_char_width )
555+
556+ header_buf .write ('\n ' )
516557 header_buf .write (divider )
517558 return header_buf .getvalue ()
518559
@@ -523,11 +564,7 @@ def generate_data_row(self, row_data: Sequence[Any]) -> str:
523564 :param row_data: data with an entry for each column in the row
524565 :return: data row string
525566 """
526- if self .divider_char is None :
527- inter_cell = 2 * SPACE
528- else :
529- inter_cell = SPACE * ansi .style_aware_wcswidth (2 * self .divider_char )
530- return self .generate_row (row_data = row_data , inter_cell = inter_cell )
567+ return self .generate_row (row_data = row_data , inter_cell = self .INTER_CELL )
531568
532569 def generate_table (self , table_data : Sequence [Sequence [Any ]], * ,
533570 include_header : bool = True , row_spacing : int = 1 ) -> str :
@@ -548,13 +585,17 @@ def generate_table(self, table_data: Sequence[Sequence[Any]], *,
548585 if include_header :
549586 header = self .generate_header ()
550587 table_buf .write (header )
588+ if len (table_data ) > 0 :
589+ table_buf .write ('\n ' )
551590
552591 for index , row_data in enumerate (table_data ):
553592 if index > 0 and row_spacing > 0 :
554593 table_buf .write (row_spacing * '\n ' )
555594
556595 row = self .generate_data_row (row_data )
557596 table_buf .write (row )
597+ if index < len (table_data ) - 1 :
598+ table_buf .write ('\n ' )
558599
559600 return table_buf .getvalue ()
560601
@@ -586,6 +627,35 @@ def __init__(self, cols: Sequence[Column], *, tab_width: int = 4,
586627 raise ValueError ("Padding cannot be less than 0" )
587628 self .padding = padding
588629
630+ @classmethod
631+ def base_width (cls , num_cols : int , * , column_borders : bool = True , padding : int = 1 ) -> int :
632+ """
633+ Utility method to calculate the display width required for a table before data is added to it.
634+ This is useful when determining how wide to make your columns to have a table be a specific width.
635+
636+ :param num_cols: how many columns the table will have
637+ :param column_borders: if True, borders between columns will be included in the calculation (Defaults to True)
638+ :param padding: number of spaces between text and left/right borders of cell
639+ :return: base width
640+ :raises: ValueError if num_cols is less than 1
641+ """
642+ if num_cols < 1 :
643+ raise ValueError ("Column count cannot be less than 1" )
644+
645+ data_str = SPACE
646+ data_width = ansi .style_aware_wcswidth (data_str ) * num_cols
647+
648+ tbl = cls ([Column (data_str )] * num_cols , column_borders = column_borders , padding = padding )
649+ data_row = tbl .generate_data_row ([data_str ] * num_cols )
650+
651+ return ansi .style_aware_wcswidth (data_row ) - data_width
652+
653+ def total_width (self ) -> int :
654+ """Calculate the total display width of this table"""
655+ base_width = self .base_width (len (self .cols ), column_borders = self .column_borders , padding = self .padding )
656+ data_width = sum (col .width for col in self .cols )
657+ return base_width + data_width
658+
589659 def generate_table_top_border (self ):
590660 """Generate a border which appears at the top of the header and data section"""
591661 pre_line = '╔' + self .padding * '═'
@@ -643,10 +713,7 @@ def generate_table_bottom_border(self):
643713 inter_cell = inter_cell , post_line = post_line )
644714
645715 def generate_header (self ) -> str :
646- """
647- Generate header
648- :return: header string
649- """
716+ """Generate table header"""
650717 pre_line = '║' + self .padding * SPACE
651718
652719 inter_cell = self .padding * SPACE
@@ -659,7 +726,9 @@ def generate_header(self) -> str:
659726 # Create the bordered header
660727 header_buf = io .StringIO ()
661728 header_buf .write (self .generate_table_top_border ())
729+ header_buf .write ('\n ' )
662730 header_buf .write (self .generate_row (pre_line = pre_line , inter_cell = inter_cell , post_line = post_line ))
731+ header_buf .write ('\n ' )
663732 header_buf .write (self .generate_header_bottom_border ())
664733
665734 return header_buf .getvalue ()
@@ -699,13 +768,17 @@ def generate_table(self, table_data: Sequence[Sequence[Any]], *, include_header:
699768 top_border = self .generate_table_top_border ()
700769 table_buf .write (top_border )
701770
771+ table_buf .write ('\n ' )
772+
702773 for index , row_data in enumerate (table_data ):
703774 if index > 0 :
704775 row_bottom_border = self .generate_row_bottom_border ()
705776 table_buf .write (row_bottom_border )
777+ table_buf .write ('\n ' )
706778
707779 row = self .generate_data_row (row_data )
708780 table_buf .write (row )
781+ table_buf .write ('\n ' )
709782
710783 table_buf .write (self .generate_table_bottom_border ())
711784 return table_buf .getvalue ()
@@ -797,9 +870,12 @@ def generate_table(self, table_data: Sequence[Sequence[Any]], *, include_header:
797870 top_border = self .generate_table_top_border ()
798871 table_buf .write (top_border )
799872
873+ table_buf .write ('\n ' )
874+
800875 for row_data in table_data :
801876 row = self .generate_data_row (row_data )
802877 table_buf .write (row )
878+ table_buf .write ('\n ' )
803879
804880 table_buf .write (self .generate_table_bottom_border ())
805881 return table_buf .getvalue ()
0 commit comments