diff options
Diffstat (limited to 'cmd2')
-rw-r--r-- | cmd2/ansi.py | 11 | ||||
-rw-r--r-- | cmd2/cmd2.py | 10 | ||||
-rw-r--r-- | cmd2/table_creator.py | 206 | ||||
-rw-r--r-- | cmd2/utils.py | 45 |
4 files changed, 168 insertions, 104 deletions
diff --git a/cmd2/ansi.py b/cmd2/ansi.py index 91188a9c..5517ecf0 100644 --- a/cmd2/ansi.py +++ b/cmd2/ansi.py @@ -943,7 +943,7 @@ class RgbBg(BgColor): # TODO: Remove this PyShadowingNames usage when deprecated fg and bg classes are removed. # noinspection PyShadowingNames def style( - text: Any, + value: Any, *, fg: Optional[FgColor] = None, bg: Optional[BgColor] = None, @@ -959,7 +959,7 @@ def style( The styling is self contained which means that at the end of the string reset code(s) are issued to undo whatever styling was done at the beginning. - :param text: text to format (anything convertible to a str) + :param value: object whose text is to be styled :param fg: foreground color provided as any subclass of FgColor (e.g. Fg, EightBitFg, RgbFg) Defaults to no color. :param bg: foreground color provided as any subclass of BgColor (e.g. Bg, EightBitBg, RgbBg) @@ -978,9 +978,6 @@ def style( # List of strings that remove style removals: List[AnsiSequence] = [] - # Convert the text object into a string if it isn't already one - text_formatted = str(text) - # Process the style settings if fg is not None: additions.append(fg) @@ -1014,8 +1011,8 @@ def style( additions.append(TextStyle.UNDERLINE_ENABLE) removals.append(TextStyle.UNDERLINE_DISABLE) - # Combine the ANSI style sequences with the text - return "".join(map(str, additions)) + text_formatted + "".join(map(str, removals)) + # Combine the ANSI style sequences with the value's text + return "".join(map(str, additions)) + str(value) + "".join(map(str, removals)) # Default styles for printing strings of various types. diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index b647c48a..c06b9617 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1062,7 +1062,7 @@ class Cmd(cmd.Cmd): been piped to another process and that process terminates before the cmd2 command is finished executing. - :param msg: message to print (anything convertible to a str) + :param msg: object to print :param end: string appended after the end of the message, default a newline """ try: @@ -1080,7 +1080,7 @@ class Cmd(cmd.Cmd): def perror(self, msg: Any = '', *, end: str = '\n', apply_style: bool = True) -> None: """Print message to sys.stderr - :param msg: message to print (anything convertible to a str) + :param msg: object to print :param end: string appended after the end of the message, default a newline :param apply_style: If True, then ansi.style_error will be applied to the message text. Set to False in cases where the message text already has the desired style. Defaults to True. @@ -1094,7 +1094,7 @@ class Cmd(cmd.Cmd): def pwarning(self, msg: Any = '', *, end: str = '\n', apply_style: bool = True) -> None: """Wraps perror, but applies ansi.style_warning by default - :param msg: message to print (anything convertible to a str) + :param msg: object to print :param end: string appended after the end of the message, default a newline :param apply_style: If True, then ansi.style_warning will be applied to the message text. Set to False in cases where the message text already has the desired style. Defaults to True. @@ -1134,7 +1134,7 @@ class Cmd(cmd.Cmd): """For printing nonessential feedback. Can be silenced with `quiet`. Inclusion in redirected output is controlled by `feedback_to_output`. - :param msg: message to print (anything convertible to a str) + :param msg: object to print :param end: string appended after the end of the message, default a newline """ if not self.quiet: @@ -1149,7 +1149,7 @@ class Cmd(cmd.Cmd): Never uses a pager inside of a script (Python or text) or when output is being redirected or piped or when stdout or stdin are not a fully functional terminal. - :param msg: message to print to current stdout (anything convertible to a str) + :param msg: object to print :param end: string appended after the end of the message, default a newline :param chop: True -> causes lines longer than the screen width to be chopped (truncated) rather than wrapped - truncated text is still accessible by scrolling with the right & left arrow keys diff --git a/cmd2/table_creator.py b/cmd2/table_creator.py index 50bc5909..0778d0c8 100644 --- a/cmd2/table_creator.py +++ b/cmd2/table_creator.py @@ -6,7 +6,6 @@ The general use case is to inherit from TableCreator to create a table class wit There are already implemented and ready-to-use examples of this below TableCreator's code. """ import copy -import functools import io from collections import ( deque, @@ -82,12 +81,13 @@ class Column: :param header_vert_align: vertical alignment of header cells (defaults to bottom) :param override_header_style: if True, then the table is allowed to apply text styles to the header, which may interfere with any styles the header already has. If False, the header is printed as is. - Table classes which apply style to headers must respect this flag. (defaults to True) + Table classes which apply style to headers must respect this flag. See the BorderedTable + class for an example of this. (defaults to True) :param data_horiz_align: horizontal alignment of data cells (defaults to left) :param data_vert_align: vertical alignment of data cells (defaults to top) :param override_data_style: if True, then the table is allowed to apply text styles to the data, which may interfere with any styles the data already has. If False, the data is printed as is. - Table classes which apply style to data must respect this flag. See the AlternatingTable + Table classes which apply style to data must respect this flag. See the BorderedTable class for an example of this. (defaults to True) :param max_data_lines: maximum lines allowed in a data cell. If line count exceeds this, then the final line displayed will be truncated with an ellipsis. (defaults to INFINITY) @@ -683,7 +683,17 @@ class BorderedTable(TableCreator): between columns can also be toggled. This class can be used to create the whole table at once or one row at a time. """ - def __init__(self, cols: Sequence[Column], *, tab_width: int = 4, column_borders: bool = True, padding: int = 1) -> None: + def __init__( + self, + cols: Sequence[Column], + *, + tab_width: int = 4, + column_borders: bool = True, + padding: int = 1, + border_fg: Optional[ansi.FgColor] = None, + header_bg: Optional[ansi.BgColor] = None, + data_bg: Optional[ansi.BgColor] = None, + ) -> None: """ BorderedTable initializer @@ -694,16 +704,53 @@ class BorderedTable(TableCreator): appearance. Turning off column borders results in a unified appearance between a row's cells. (Defaults to True) :param padding: number of spaces between text and left/right borders of cell + :param border_fg: optional foreground color for borders (defaults to None) + :param header_bg: optional background color for header cells (defaults to None) + :param data_bg: optional background color for data cells (defaults to None) :raises: ValueError if padding is less than 0 """ super().__init__(cols, tab_width=tab_width) - self.empty_data = [EMPTY for _ in self.cols] + self.empty_data = [EMPTY] * len(self.cols) self.column_borders = column_borders if padding < 0: raise ValueError("Padding cannot be less than 0") self.padding = padding + self.border_fg = border_fg + self.header_bg = header_bg + self.data_bg = data_bg + + def apply_border_fg(self, value: Any) -> str: + """ + If defined, apply the border foreground color to border text + :param value: object whose text is to be colored + :return: formatted text + """ + if self.border_fg is None: + return str(value) + return ansi.style(value, fg=self.border_fg) + + def apply_header_bg(self, value: Any) -> str: + """ + If defined, apply the header background color to header text + :param value: object whose text is to be colored + :return: formatted text + """ + if self.header_bg is None: + return str(value) + return ansi.style(value, bg=self.header_bg) + + def apply_data_bg(self, value: Any) -> str: + """ + If defined, apply the data background color to data text + :param value: object whose text is to be colored + :return: formatted data string + """ + if self.data_bg is None: + return str(value) + return ansi.style(value, bg=self.data_bg) + @classmethod def base_width(cls, num_cols: int, *, column_borders: bool = True, padding: int = 1) -> int: """ @@ -735,6 +782,8 @@ class BorderedTable(TableCreator): def generate_table_top_border(self) -> str: """Generate a border which appears at the top of the header and data section""" + fill_char = '═' + pre_line = '╔' + self.padding * '═' inter_cell = self.padding * '═' @@ -745,11 +794,17 @@ class BorderedTable(TableCreator): post_line = self.padding * '═' + '╗' return self.generate_row( - row_data=self.empty_data, fill_char='═', pre_line=pre_line, inter_cell=inter_cell, post_line=post_line + row_data=self.empty_data, + fill_char=self.apply_border_fg(fill_char), + pre_line=self.apply_border_fg(pre_line), + inter_cell=self.apply_border_fg(inter_cell), + post_line=self.apply_border_fg(post_line), ) def generate_header_bottom_border(self) -> str: """Generate a border which appears at the bottom of the header""" + fill_char = '═' + pre_line = '╠' + self.padding * '═' inter_cell = self.padding * '═' @@ -760,26 +815,39 @@ class BorderedTable(TableCreator): post_line = self.padding * '═' + '╣' return self.generate_row( - row_data=self.empty_data, fill_char='═', pre_line=pre_line, inter_cell=inter_cell, post_line=post_line + row_data=self.empty_data, + fill_char=self.apply_border_fg(fill_char), + pre_line=self.apply_border_fg(pre_line), + inter_cell=self.apply_border_fg(inter_cell), + post_line=self.apply_border_fg(post_line), ) def generate_row_bottom_border(self) -> str: """Generate a border which appears at the bottom of rows""" - pre_line = '╟' + self.padding * '─' + fill_char = self.apply_data_bg('─') + + pre_line = '╟' + self.apply_data_bg(self.padding * '─') inter_cell = self.padding * '─' if self.column_borders: inter_cell += '┼' inter_cell += self.padding * '─' + inter_cell = self.apply_data_bg(inter_cell) - post_line = self.padding * '─' + '╢' + post_line = self.apply_data_bg(self.padding * '─') + '╢' return self.generate_row( - row_data=self.empty_data, fill_char='─', pre_line=pre_line, inter_cell=inter_cell, post_line=post_line + row_data=self.empty_data, + fill_char=self.apply_border_fg(fill_char), + pre_line=self.apply_border_fg(pre_line), + inter_cell=self.apply_border_fg(inter_cell), + post_line=self.apply_border_fg(post_line), ) def generate_table_bottom_border(self) -> str: """Generate a border which appears at the bottom of the table""" + fill_char = '═' + pre_line = '╚' + self.padding * '═' inter_cell = self.padding * '═' @@ -790,25 +858,44 @@ class BorderedTable(TableCreator): post_line = self.padding * '═' + '╝' return self.generate_row( - row_data=self.empty_data, fill_char='═', pre_line=pre_line, inter_cell=inter_cell, post_line=post_line + row_data=self.empty_data, + fill_char=self.apply_border_fg(fill_char), + pre_line=self.apply_border_fg(pre_line), + inter_cell=self.apply_border_fg(inter_cell), + post_line=self.apply_border_fg(post_line), ) def generate_header(self) -> str: """Generate table header""" - pre_line = '║' + self.padding * SPACE + fill_char = self.apply_header_bg(SPACE) + + pre_line = self.apply_border_fg('║') + self.apply_header_bg(self.padding * SPACE) inter_cell = self.padding * SPACE if self.column_borders: - inter_cell += '│' + inter_cell += self.apply_border_fg('│') inter_cell += self.padding * SPACE + inter_cell = self.apply_header_bg(inter_cell) - post_line = self.padding * SPACE + '║' + post_line = self.apply_header_bg(self.padding * SPACE) + self.apply_border_fg('║') + + # Apply background color to header text in Columns which allow it + to_display: List[Any] = [] + for index, col in enumerate(self.cols): + if col.override_header_style: + to_display.append(self.apply_header_bg(col.header)) + else: + to_display.append(col.header) # Create the bordered header header_buf = io.StringIO() header_buf.write(self.generate_table_top_border()) header_buf.write('\n') - header_buf.write(self.generate_row(pre_line=pre_line, inter_cell=inter_cell, post_line=post_line)) + header_buf.write( + self.generate_row( + row_data=to_display, fill_char=fill_char, pre_line=pre_line, inter_cell=inter_cell, post_line=post_line + ) + ) header_buf.write('\n') header_buf.write(self.generate_header_bottom_border()) @@ -821,16 +908,29 @@ class BorderedTable(TableCreator): :param row_data: data with an entry for each column in the row :return: data row string """ - pre_line = '║' + self.padding * SPACE + fill_char = self.apply_data_bg(SPACE) + + pre_line = self.apply_border_fg('║') + self.apply_data_bg(self.padding * SPACE) inter_cell = self.padding * SPACE if self.column_borders: - inter_cell += '│' + inter_cell += self.apply_border_fg('│') inter_cell += self.padding * SPACE + inter_cell = self.apply_data_bg(inter_cell) + + post_line = self.apply_data_bg(self.padding * SPACE) + self.apply_border_fg('║') - post_line = self.padding * SPACE + '║' + # Apply background color to data text in Columns which allow it + to_display: List[Any] = [] + for index, col in enumerate(self.cols): + if col.override_data_style: + to_display.append(self.apply_data_bg(row_data[index])) + else: + to_display.append(row_data[index]) - return self.generate_row(row_data=row_data, pre_line=pre_line, inter_cell=inter_cell, post_line=post_line) + return self.generate_row( + row_data=to_display, fill_char=fill_char, pre_line=pre_line, inter_cell=inter_cell, post_line=post_line + ) def generate_table(self, table_data: Sequence[Sequence[Any]], *, include_header: bool = True) -> str: """ @@ -870,9 +970,6 @@ class AlternatingTable(BorderedTable): Implementation of BorderedTable which uses background colors to distinguish between rows instead of row border lines. This class can be used to create the whole table at once or one row at a time. - AlternatingTable will not apply background color to data whose Columns set override_data_style to False. - Background color will still be applied to those Columns's padding and fill characters. - To nest an AlternatingTable within another AlternatingTable, set override_data_style to False on the Column which contains the nested table. That will prevent the current row's background color from affecting the colors of the nested table. @@ -885,8 +982,10 @@ class AlternatingTable(BorderedTable): tab_width: int = 4, column_borders: bool = True, padding: int = 1, - bg_odd: Optional[ansi.BgColor] = None, - bg_even: Optional[ansi.BgColor] = ansi.Bg.DARK_GRAY, + border_fg: Optional[ansi.FgColor] = None, + header_bg: Optional[ansi.BgColor] = None, + odd_bg: Optional[ansi.BgColor] = None, + even_bg: Optional[ansi.BgColor] = ansi.Bg.DARK_GRAY, ) -> None: """ AlternatingTable initializer @@ -900,28 +999,31 @@ class AlternatingTable(BorderedTable): appearance. Turning off column borders results in a unified appearance between a row's cells. (Defaults to True) :param padding: number of spaces between text and left/right borders of cell - :param bg_odd: optional background color for odd numbered rows (defaults to None) - :param bg_even: optional background color for even numbered rows (defaults to Bg.DARK_GRAY) + :param border_fg: optional foreground color for borders (defaults to None) + :param header_bg: optional background color for header cells (defaults to None) + :param odd_bg: optional background color for odd numbered data rows (defaults to None) + :param even_bg: optional background color for even numbered data rows (defaults to StdBg.DARK_GRAY) :raises: ValueError if padding is less than 0 """ - super().__init__(cols, tab_width=tab_width, column_borders=column_borders, padding=padding) + super().__init__( + cols, tab_width=tab_width, column_borders=column_borders, padding=padding, border_fg=border_fg, header_bg=header_bg + ) self.row_num = 1 - self.bg_odd = None if bg_odd is None else functools.partial(ansi.style, bg=bg_odd) - self.bg_even = None if bg_even is None else functools.partial(ansi.style, bg=bg_even) + self.odd_bg = odd_bg + self.even_bg = even_bg - def _apply_bg_color(self, data: Any) -> str: + def apply_data_bg(self, value: Any) -> str: """ - Convert data to text and apply background color to it based on what row is being generated - - :param data: data being colored - :return: converted data + Apply background color to data text based on what row is being generated and whether a color has been defined + :param value: object whose text is to be colored + :return: formatted data string """ - if self.row_num % 2 == 0 and self.bg_even is not None: - return self.bg_even(data) - elif self.row_num % 2 != 0 and self.bg_odd is not None: - return self.bg_odd(data) + if self.row_num % 2 == 0 and self.even_bg is not None: + return ansi.style(value, bg=self.even_bg) + elif self.row_num % 2 != 0 and self.odd_bg is not None: + return ansi.style(value, bg=self.odd_bg) else: - return str(data) + return str(value) def generate_data_row(self, row_data: Sequence[Any]) -> str: """ @@ -930,31 +1032,7 @@ class AlternatingTable(BorderedTable): :param row_data: data with an entry for each column in the row :return: data row string """ - # Only color the padding and not the outer border characters - pre_line = '║' + self._apply_bg_color(self.padding * SPACE) - - inter_cell = self.padding * SPACE - if self.column_borders: - inter_cell += '│' - inter_cell += self.padding * SPACE - inter_cell = self._apply_bg_color(inter_cell) - - # Only color the padding and not the outer border characters - post_line = self._apply_bg_color(self.padding * SPACE) + '║' - - fill_char = self._apply_bg_color(SPACE) - - # Apply background colors to data whose Columns allow it - to_display: List[Any] = [] - for index, col in enumerate(self.cols): - if col.override_data_style: - to_display.append(self._apply_bg_color(row_data[index])) - else: - to_display.append(row_data[index]) - - row = self.generate_row( - row_data=to_display, fill_char=fill_char, pre_line=pre_line, inter_cell=inter_cell, post_line=post_line - ) + row = super().generate_data_row(row_data) self.row_num += 1 return row diff --git a/cmd2/utils.py b/cmd2/utils.py index 733cfc24..5f2ceaf4 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -787,17 +787,23 @@ def align_text( if width < 1: raise ValueError("width must be at least 1") - # Handle tabs + # Convert tabs to spaces text = text.replace('\t', ' ' * tab_width) fill_char = fill_char.replace('\t', ' ') - if len(ansi.strip_style(fill_char)) != 1: + # Save fill_char with no styles for use later + stripped_fill_char = ansi.strip_style(fill_char) + if len(stripped_fill_char) != 1: raise TypeError("Fill character must be exactly one character long") fill_char_width = ansi.style_aware_wcswidth(fill_char) if fill_char_width == -1: raise (ValueError("Fill character is an unprintable character")) + # Isolate the style chars before and after the fill character. We will use them when building sequences of + # of fill characters. Instead of repeating the style characters for each fill character, we'll wrap each sequence. + fill_char_style_begin, fill_char_style_end = fill_char.split(stripped_fill_char) + if text: lines = text.splitlines() else: @@ -810,23 +816,6 @@ def align_text( # This also allows the lines to be used independently and still have their style. TableCreator does this. aggregate_styles = '' - # Save the ANSI style sequences in fill_char - fill_char_styles = get_styles_in_text(fill_char) - - # Create a space with the same style as fill_char for cases in which - # fill_char does not divide evenly into the gap. - styled_space = '' - char_index = 0 - while char_index < len(fill_char): - if char_index in fill_char_styles: - # Preserve this style in styled_space - styled_space += fill_char_styles[char_index] - char_index += len(fill_char_styles[char_index]) - else: - # We've reached the visible fill_char. Replace it with a space. - styled_space += ' ' - char_index += 1 - for index, line in enumerate(lines): if index > 0: text_buf.write('\n') @@ -860,22 +849,22 @@ def align_text( right_fill_width = 0 # Determine how many fill characters are needed to cover the width - left_fill = (left_fill_width // fill_char_width) * fill_char - right_fill = (right_fill_width // fill_char_width) * fill_char + left_fill = (left_fill_width // fill_char_width) * stripped_fill_char + right_fill = (right_fill_width // fill_char_width) * stripped_fill_char # In cases where the fill character display width didn't divide evenly into - # the gap being filled, pad the remainder with styled_space. - left_fill += styled_space * (left_fill_width - ansi.style_aware_wcswidth(left_fill)) - right_fill += styled_space * (right_fill_width - ansi.style_aware_wcswidth(right_fill)) + # the gap being filled, pad the remainder with space. + left_fill += ' ' * (left_fill_width - ansi.style_aware_wcswidth(left_fill)) + right_fill += ' ' * (right_fill_width - ansi.style_aware_wcswidth(right_fill)) - # Don't allow styles in fill_char and text to affect one another - if fill_char_styles or aggregate_styles or line_styles: + # Don't allow styles in fill characters and text to affect one another + if fill_char_style_begin or fill_char_style_end or aggregate_styles or line_styles: if left_fill: - left_fill = ansi.TextStyle.RESET_ALL + left_fill + left_fill = ansi.TextStyle.RESET_ALL + fill_char_style_begin + left_fill + fill_char_style_end left_fill += ansi.TextStyle.RESET_ALL if right_fill: - right_fill = ansi.TextStyle.RESET_ALL + right_fill + right_fill = ansi.TextStyle.RESET_ALL + fill_char_style_begin + right_fill + fill_char_style_end right_fill += ansi.TextStyle.RESET_ALL # Write the line and restore any styles from previous lines |