D7net
Home
Console
Upload
information
Create File
Create Folder
About
Tools
:
/
lib
/
python3
/
dist-packages
/
uaclient
/
cli
/
Filename :
formatter.py
back
Copy
import abc import os import re import sys import textwrap from enum import Enum from typing import Any, Dict, List, Optional # noqa: F401 from uaclient.config import UAConfig from uaclient.messages import TxtColor COLOR_FORMATTING_PATTERN = r"\033\[.*?m" LINK_START_PATTERN = r"\033]8;;.+?\033\\+" LINK_END = "\033]8;;\033\\" UTF8_ALTERNATIVES = { "—": "-", "✘": "x", "✔": "*", } # type: Dict[str, str] class ContentAlignment(Enum): LEFT = "l" RIGHT = "r" # Class attributes and methods so we don't need singletons or globals for this class ProOutputFormatterConfig: use_utf8 = True use_color = True # Initializing the class after the import is useful for unit testing @classmethod def init(cls, cfg: UAConfig): cls.use_utf8 = ( sys.stdout.encoding is not None and "UTF-8" in sys.stdout.encoding.upper() ) cls.use_color = sys.stdout.isatty() and os.getenv("NO_COLOR") is None @classmethod def disable_color(cls) -> None: cls.use_color = False ProOutputFormatterConfig.init(cfg=UAConfig()) def create_link(text: str, url: str) -> str: return "\033]8;;{url}\033\\{text}\033]8;;\033\\".format(url=url, text=text) def real_len(text: str) -> int: # ignore colors if existing result = re.sub(COLOR_FORMATTING_PATTERN, "", text) # Ignore link control characters and metadata result = re.sub(LINK_START_PATTERN, "", result) result = result.replace(LINK_END, "") return len(result) def _get_default_length(): if sys.stdout.isatty(): return os.get_terminal_size().columns # If you're not in a tty, we don't care about string length # If you have a thousand characters line, well, wow return 999 def process_formatter_config(text: str) -> str: output = text if not ProOutputFormatterConfig.use_color: output = re.sub(COLOR_FORMATTING_PATTERN, "", text) if not ProOutputFormatterConfig.use_utf8: for char, alternative in UTF8_ALTERNATIVES.items(): output = output.replace(char, alternative) output = output.encode("ascii", "ignore").decode() if not sys.stdout.isatty(): output = re.sub(LINK_START_PATTERN, "", output) output = output.replace(LINK_END, "") return output # We can't rely on textwrap because of the real_len function # Textwrap is using a magic regex instead def wrap_text(text: str, max_width: int) -> List[str]: if real_len(text) <= max_width: return [text] words = text.split() wrapped_lines = [] current_line = "" for word in words: if real_len(current_line) + real_len(word) >= max_width: wrapped_lines.append(current_line.strip()) current_line = word else: current_line += " " + word if current_line: wrapped_lines.append(current_line.strip()) return wrapped_lines class ProOutputFormatter(abc.ABC): @abc.abstractmethod def to_string(self, line_length: Optional[int] = None) -> str: pass def __str__(self): return self.to_string() class Table(ProOutputFormatter): SEPARATOR = " " * 2 def __init__( self, headers: Optional[List[str]] = None, rows: Optional[List[List[str]]] = None, alignment: Optional[List[ContentAlignment]] = None, ): self.headers = headers if headers is not None else [] self.rows = rows if rows is not None else [] self.column_sizes = self._get_column_sizes() self.alignment = ( alignment if alignment is not None else [ContentAlignment.LEFT] * len(self.column_sizes) ) if len(self.alignment) != len(self.column_sizes): raise ValueError( "'alignment' list should have length {}".format( len(self.column_sizes) ) ) self.last_column_size = self.column_sizes[-1] @staticmethod def ljust(string: str, total_length: int) -> str: str_length = real_len(string) if str_length >= total_length: return string return string + " " * (total_length - str_length) @staticmethod def rjust(string: str, total_length: int) -> str: str_length = real_len(string) if str_length >= total_length: return string return " " * (total_length - str_length) + string def _get_column_sizes(self) -> List[int]: if not self.headers and not self.rows: raise ValueError( "Empty table not supported. Please provide headers or rows." ) if self.rows and any(len(item) == 0 for item in self.rows): raise ValueError( "Empty row not supported. Please provide content for each row." ) all_content = [] if self.headers: all_content.append(self.headers) if self.rows: all_content.extend(self.rows) expected_length = len(all_content[0]) if not all(len(item) == expected_length for item in all_content): raise ValueError( "Mixed lengths in table content. " "Please provide headers / rows of the same length." ) column_sizes = [] for i in range(len(all_content[0])): column_sizes.append( max(real_len(str(item[i])) for item in all_content) ) return column_sizes def to_string(self, line_length: Optional[int] = None) -> str: if line_length is None: line_length = _get_default_length() rows = self.rows if self._get_line_length() > line_length: rows = self.wrap_last_column(line_length) output = "" if self.headers: output += ( TxtColor.BOLD + self._fill_row(self.headers) + TxtColor.ENDC + "\n" ) for row in rows: output += self._fill_row(row) output += "\n" return process_formatter_config(output) def _get_line_length(self) -> int: return sum(self.column_sizes) + (len(self.column_sizes) - 1) * len( self.SEPARATOR ) def wrap_last_column(self, max_length: int) -> List[List[str]]: self.last_column_size = max_length - ( sum(self.column_sizes[:-1]) + (len(self.column_sizes) - 1) * len(self.SEPARATOR) ) new_rows = [] for row in self.rows: if len(row[-1]) <= self.last_column_size: new_rows.append(row) else: wrapped_last_column = wrap_text(row[-1], self.last_column_size) new_rows.append(row[:-1] + [wrapped_last_column[0]]) for extra_line in wrapped_last_column[1:]: new_row = [" "] * (len(self.column_sizes) - 1) + [ extra_line ] new_rows.append(new_row) return new_rows def _fill_row(self, row: List[str]) -> str: output = "" for i in range(len(row) - 1): if self.alignment[i] == ContentAlignment.LEFT: output += ( self.ljust(row[i], self.column_sizes[i]) + self.SEPARATOR ) elif self.alignment[i] == ContentAlignment.RIGHT: output += ( self.rjust(row[i], self.column_sizes[i]) + self.SEPARATOR ) if self.alignment[-1] == ContentAlignment.LEFT: output += row[-1] elif self.alignment[-1] == ContentAlignment.RIGHT: output += self.rjust(row[-1], self.last_column_size) return output class Block(ProOutputFormatter): INDENT_SIZE = 4 INDENT_CHAR = " " def __init__( self, title: Optional[str] = None, content: Optional[List[Any]] = None, ): self.title = title self.content = content if content is not None else [] def to_string(self, line_length: Optional[int] = None) -> str: if line_length is None: line_length = _get_default_length() line_length -= self.INDENT_SIZE output = "" if self.title: output += ( TxtColor.BOLD + TxtColor.DISABLEGREY + self.title + TxtColor.ENDC + "\n" ) for item in self.content: if isinstance(item, ProOutputFormatter): item_str = item.to_string(line_length=line_length) else: item_str = "\n".join(wrap_text(str(item), line_length)) + "\n" output += textwrap.indent( item_str, self.INDENT_CHAR * self.INDENT_SIZE ) return process_formatter_config(output)