diff --git a/bitutilities.py b/bitutilities.py index f779880..4ee00d2 100644 --- a/bitutilities.py +++ b/bitutilities.py @@ -1,5 +1,6 @@ from collections import deque from typing_extensions import Self +from lib.prettytable import PrettyTable class BasicRegister: @@ -13,14 +14,17 @@ class BasicRegister: self.memory: deque[bool] = deque(memory) def __repr__(self) -> str: - return f"Memory: {[int(value) for value in self.memory]}" + return "".join([str(int(value)) for value in self.memory]) def __str__(self) -> str: - return f"Memory: {[int(value) for value in self.memory]}" + return "".join([str(int(value)) for value in self.memory]) def __len__(self) -> int: return len(self.memory) + def compact_str(self) -> str: + return "".join([str(int(value)) for value in self.memory]) + def adjusted_by_size(self, resulting_size: int) -> Self: """ Adjusts a register to a given size. @@ -81,14 +85,17 @@ class Counter: self.memory: deque[bool] = deque([i == "1" for i in bin(value)[2:]]) def __repr__(self) -> str: - return f"Memory: {[int(value) for value in self.memory]}" + return "".join([str(int(value)) for value in self.memory]) def __str__(self) -> str: - return f"Memory: {[int(value) for value in self.memory]}" + return "".join([str(int(value)) for value in self.memory]) def __len__(self) -> int: return len(self.memory) + def compact_str(self) -> str: + return "".join([str(int(value)) for value in self.memory]) + def decrement(self): self.memory = binary_subtraction(self, BasicRegister([False] * (len(self.memory) - 1) + [True])).memory @@ -206,6 +213,18 @@ def align_registers(*registers: BasicRegister) -> tuple[BasicRegister, ...]: return tuple(reg.adjusted_by_size(required_size) for reg in registers) +def format_device_state_table(table) -> str: + pt = PrettyTable() + pt.field_names = table[0] + + for block in table[1:]: + for line in block[:-1]: + pt.add_row(line) + pt.add_row(block[-1], divider = True) + + return pt.get_string() + + def binary_multiplication_method_1(first_term: BasicRegister, second_term: BasicRegister) -> BasicRegister: """ Multiplies two terms containing binary numbers using first method. @@ -224,15 +243,29 @@ def binary_multiplication_method_1(first_term: BasicRegister, second_term: Basic rg3 = BasicRegister(second_term.memory) ct = Counter(n) + data_table = [["iter", "RG1", "RG2", "RG3", "CT", "MicroOperations"]] + + i = 0 + data_table.append([]) + data_table[-1].append(list(map(str, [i, rg1, rg2, rg3, ct, "-"]))) + while ct.non_zero(): + i += 1 + data_table.append([]) + if rg2.memory[n-1]: rg1 = binary_sum(rg1, rg3) + data_table[-1].append(list(map(str, [i, rg1, rg2, rg3, ct, "RG1 := RG1 + RG3"]))) rg2.right_shift(rg1.memory[n-1]) rg1.right_shift() ct.decrement() - return BasicRegister(list(rg1.memory) + list(rg2.memory)) + print(ct.memory) + + data_table[-1].append(list(map(str, [i, rg1, rg2, rg3, ct, "RG2 := RG1[1].r(RG2)\nRG1 := 0.r(RG1)\nCT := CT - 1"]))) + + return BasicRegister(list(rg1.memory) + list(rg2.memory)), data_table def binary_multiplication_method_2(first_term: BasicRegister, second_term: BasicRegister) -> BasicRegister: """ @@ -251,14 +284,26 @@ def binary_multiplication_method_2(first_term: BasicRegister, second_term: Basic rg2 = BasicRegister(first_term.memory) rg3 = BasicRegister([False] * n + list(second_term.memory)) + i = 0 + data_table = [["iter", "RG1", "RG2", "RG3", "MicroOperations"]] + + data_table.append([]) + data_table[-1].append(list(map(str, [i, rg1, rg2, rg3, "-"]))) + while any(rg2.memory): + i += 1 + data_table.append([]) + if rg2.memory[n-1]: rg1 = binary_sum(rg1, rg3) + data_table[-1].append(list(map(str, [i, rg1, rg2, rg3, "RG1 := RG1 + RG3"]))) rg2.right_shift() rg3.left_shift() - return rg1 + data_table[-1].append(list(map(str, [i, rg1, rg2, rg3, "RG2 := 0.r(RG2)\nRG3 := l(RG3).0"]))) + + return rg1, data_table def binary_multiplication_method_3(first_term: BasicRegister, second_term: BasicRegister) -> BasicRegister: """ @@ -273,22 +318,34 @@ def binary_multiplication_method_3(first_term: BasicRegister, second_term: Basic first_term, second_term = align_registers(first_term, second_term) n: int = len(first_term) + data_table = [["iter", "RG2", "RG1", "RG3", "CT", "MicroOperations"]] + rg1 = BasicRegister([False] * n) rg2 = BasicRegister(list(first_term.memory) + [False]) rg3 = BasicRegister([False] * (n+1) + list(second_term.memory)) ct = Counter(n) + i = 0 + data_table.append([]) + data_table[-1].append(list(map(str, [i, rg2, rg1, rg3, ct, "-"]))) + while ct.non_zero(): + i += 1 + data_table.append([]) + if rg2.memory[0]: result: list[bool] = list(binary_sum(BasicRegister(rg2.memory + rg1.memory), rg3).memory) rg2 = BasicRegister(result[:n+1]) rg1 = BasicRegister(result[n+1:]) + data_table[-1].append(list(map(str, [i, rg2, rg1, rg3, ct, "RG2.RG1 := RG2.RG1 + RG3"]))) rg2.left_shift(rg1.memory[0]) rg1.left_shift() ct.decrement() - return BasicRegister((list(rg2.memory) + list(rg1.memory))[:-1]) + data_table[-1].append(list(map(str, [i, rg2, rg1, rg3, ct, "RG2.RG1 := l(RG2.RG1).0\nCT := CT - 1"]))) + + return BasicRegister((list(rg2.memory) + list(rg1.memory))[:-1]), data_table def binary_multiplication_method_4(first_term: BasicRegister, second_term: BasicRegister) -> BasicRegister: """ @@ -307,11 +364,23 @@ def binary_multiplication_method_4(first_term: BasicRegister, second_term: Basic rg2 = BasicRegister(first_term.memory) rg3 = BasicRegister([False] + list(second_term.memory) + [False] * n) + data_table = [["iter", "RG1", "RG2", "RG3", "MicroOperations"]] + + i = 0 + data_table.append([]) + data_table[-1].append(list(map(str, [i, rg1, rg2, rg3, "-"]))) + while any(rg2.memory): + i += 1 + data_table.append([]) + if rg2.memory[0]: rg1 = binary_sum(rg1, rg3) + data_table[-1].append(list(map(str, [i, rg1, rg2, rg3, "RG1 := RG1 + RG3"]))) rg2.left_shift() rg3.right_shift() - return BasicRegister(list(rg1.memory)[:-1]) + data_table[-1].append(list(map(str, [i, rg1, rg2, rg3, "RG2 := l(RG2).0\nRG3 := 0.r(RG3)"]))) + + return BasicRegister(list(rg1.memory)[:-1]), data_table diff --git a/lib/prettytable.py b/lib/prettytable.py new file mode 100644 index 0000000..b4b8eed --- /dev/null +++ b/lib/prettytable.py @@ -0,0 +1,2534 @@ +#!/usr/bin/env python +# +# Copyright (c) 2009-2014, Luke Maurits +# All rights reserved. +# With contributions from: +# * Chris Clark +# * Klein Stephane +# * John Filleau +# * Vladimir Vrzić +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import annotations + +import copy +import csv +import io +import json +import math +import random +import re +import textwrap +from html import escape +from html.parser import HTMLParser +from typing import Any + +import wcwidth # type: ignore + +# hrule styles +FRAME = 0 +ALL = 1 +NONE = 2 +HEADER = 3 + +# Table styles +DEFAULT = 10 +MSWORD_FRIENDLY = 11 +PLAIN_COLUMNS = 12 +MARKDOWN = 13 +ORGMODE = 14 +DOUBLE_BORDER = 15 +SINGLE_BORDER = 16 +RANDOM = 20 +BASE_ALIGN_VALUE = "base_align_value" + +_re = re.compile(r"\033\[[0-9;]*m|\033\(B") + + +def _get_size(text): + lines = text.split("\n") + height = len(lines) + width = max(_str_block_width(line) for line in lines) + return width, height + + +class PrettyTable: + def __init__(self, field_names=None, **kwargs) -> None: + """Return a new PrettyTable instance + + Arguments: + + encoding - Unicode encoding scheme used to decode any encoded input + title - optional table title + field_names - list or tuple of field names + fields - list or tuple of field names to include in displays + start - index of first data row to include in output + end - index of last data row to include in output PLUS ONE (list slice style) + header - print a header showing field names (True or False) + header_style - stylisation to apply to field names in header + ("cap", "title", "upper", "lower" or None) + border - print a border around the table (True or False) + preserve_internal_border - print a border inside the table even if + border is disabled (True or False) + hrules - controls printing of horizontal rules after rows. + Allowed values: FRAME, HEADER, ALL, NONE + vrules - controls printing of vertical rules between columns. + Allowed values: FRAME, ALL, NONE + int_format - controls formatting of integer data + float_format - controls formatting of floating point data + custom_format - controls formatting of any column using callable + min_table_width - minimum desired table width, in characters + max_table_width - maximum desired table width, in characters + min_width - minimum desired field width, in characters + max_width - maximum desired field width, in characters + padding_width - number of spaces on either side of column data + (only used if left and right paddings are None) + left_padding_width - number of spaces on left hand side of column data + right_padding_width - number of spaces on right hand side of column data + vertical_char - single character string used to draw vertical lines + horizontal_char - single character string used to draw horizontal lines + horizontal_align_char - single character string used to indicate alignment + junction_char - single character string used to draw line junctions + top_junction_char - single character string used to draw top line junctions + bottom_junction_char - + single character string used to draw bottom line junctions + right_junction_char - single character string used to draw right line junctions + left_junction_char - single character string used to draw left line junctions + top_right_junction_char - + single character string used to draw top-right line junctions + top_left_junction_char - + single character string used to draw top-left line junctions + bottom_right_junction_char - + single character string used to draw bottom-right line junctions + bottom_left_junction_char - + single character string used to draw bottom-left line junctions + sortby - name of field to sort rows by + sort_key - sorting key function, applied to data points before sorting + align - default align for each column (None, "l", "c" or "r") + valign - default valign for each row (None, "t", "m" or "b") + reversesort - True or False to sort in descending or ascending order + oldsortslice - Slice rows before sorting in the "old style" """ + + self.encoding = kwargs.get("encoding", "UTF-8") + + # Data + self._field_names: list[str] = [] + self._rows: list[list] = [] + self._dividers: list[bool] = [] + self.align = {} + self.valign = {} + self.max_width = {} + self.min_width = {} + self.int_format = {} + self.float_format = {} + self.custom_format = {} + + if field_names: + self.field_names = field_names + else: + self._widths: list[int] = [] + + # Options + self._options = [ + "title", + "start", + "end", + "fields", + "header", + "border", + "preserve_internal_border", + "sortby", + "reversesort", + "sort_key", + "attributes", + "format", + "hrules", + "vrules", + "int_format", + "float_format", + "custom_format", + "min_table_width", + "max_table_width", + "padding_width", + "left_padding_width", + "right_padding_width", + "vertical_char", + "horizontal_char", + "horizontal_align_char", + "junction_char", + "header_style", + "valign", + "xhtml", + "print_empty", + "oldsortslice", + "top_junction_char", + "bottom_junction_char", + "right_junction_char", + "left_junction_char", + "top_right_junction_char", + "top_left_junction_char", + "bottom_right_junction_char", + "bottom_left_junction_char", + "align", + "valign", + "max_width", + "min_width", + "none_format", + ] + for option in self._options: + if option in kwargs: + self._validate_option(option, kwargs[option]) + else: + kwargs[option] = None + + self._title = kwargs["title"] or None + self._start = kwargs["start"] or 0 + self._end = kwargs["end"] or None + self._fields = kwargs["fields"] or None + self._none_format: dict[None, None] = {} + + if kwargs["header"] in (True, False): + self._header = kwargs["header"] + else: + self._header = True + self._header_style = kwargs["header_style"] or None + if kwargs["border"] in (True, False): + self._border = kwargs["border"] + else: + self._border = True + if kwargs["preserve_internal_border"] in (True, False): + self._preserve_internal_border = kwargs["preserve_internal_border"] + else: + self._preserve_internal_border = False + self._hrules = kwargs["hrules"] or FRAME + self._vrules = kwargs["vrules"] or ALL + + self._sortby = kwargs["sortby"] or None + if kwargs["reversesort"] in (True, False): + self._reversesort = kwargs["reversesort"] + else: + self._reversesort = False + self._sort_key = kwargs["sort_key"] or (lambda x: x) + + # Column specific arguments, use property.setters + self.align = kwargs["align"] or {} + self.valign = kwargs["valign"] or {} + self.max_width = kwargs["max_width"] or {} + self.min_width = kwargs["min_width"] or {} + self.int_format = kwargs["int_format"] or {} + self.float_format = kwargs["float_format"] or {} + self.custom_format = kwargs["custom_format"] or {} + self.none_format = kwargs["none_format"] or {} + + self._min_table_width = kwargs["min_table_width"] or None + self._max_table_width = kwargs["max_table_width"] or None + if kwargs["padding_width"] is None: + self._padding_width = 1 + else: + self._padding_width = kwargs["padding_width"] + self._left_padding_width = kwargs["left_padding_width"] or None + self._right_padding_width = kwargs["right_padding_width"] or None + + self._vertical_char = kwargs["vertical_char"] or "|" + self._horizontal_char = kwargs["horizontal_char"] or "-" + self._horizontal_align_char = kwargs["horizontal_align_char"] + self._junction_char = kwargs["junction_char"] or "+" + self._top_junction_char = kwargs["top_junction_char"] + self._bottom_junction_char = kwargs["bottom_junction_char"] + self._right_junction_char = kwargs["right_junction_char"] + self._left_junction_char = kwargs["left_junction_char"] + self._top_right_junction_char = kwargs["top_right_junction_char"] + self._top_left_junction_char = kwargs["top_left_junction_char"] + self._bottom_right_junction_char = kwargs["bottom_right_junction_char"] + self._bottom_left_junction_char = kwargs["bottom_left_junction_char"] + + if kwargs["print_empty"] in (True, False): + self._print_empty = kwargs["print_empty"] + else: + self._print_empty = True + if kwargs["oldsortslice"] in (True, False): + self._oldsortslice = kwargs["oldsortslice"] + else: + self._oldsortslice = False + self._format = kwargs["format"] or False + self._xhtml = kwargs["xhtml"] or False + self._attributes = kwargs["attributes"] or {} + + def _justify(self, text, width, align): + excess = width - _str_block_width(text) + if align == "l": + return text + excess * " " + elif align == "r": + return excess * " " + text + else: + if excess % 2: + # Uneven padding + # Put more space on right if text is of odd length... + if _str_block_width(text) % 2: + return (excess // 2) * " " + text + (excess // 2 + 1) * " " + # and more space on left if text is of even length + else: + return (excess // 2 + 1) * " " + text + (excess // 2) * " " + # Why distribute extra space this way? To match the behaviour of + # the inbuilt str.center() method. + else: + # Equal padding on either side + return (excess // 2) * " " + text + (excess // 2) * " " + + def __getattr__(self, name): + if name == "rowcount": + return len(self._rows) + elif name == "colcount": + if self._field_names: + return len(self._field_names) + elif self._rows: + return len(self._rows[0]) + else: + return 0 + else: + raise AttributeError(name) + + def __getitem__(self, index): + new = PrettyTable() + new.field_names = self.field_names + for attr in self._options: + setattr(new, "_" + attr, getattr(self, "_" + attr)) + setattr(new, "_align", getattr(self, "_align")) + if isinstance(index, slice): + for row in self._rows[index]: + new.add_row(row) + elif isinstance(index, int): + new.add_row(self._rows[index]) + else: + raise IndexError(f"Index {index} is invalid, must be an integer or slice") + return new + + def __str__(self): + return self.get_string() + + def __repr__(self): + return self.get_string() + + def _repr_html_(self): + """ + Returns get_html_string value by default + as the repr call in Jupyter notebook environment + """ + return self.get_html_string() + + ############################## + # ATTRIBUTE VALIDATORS # + ############################## + + # The method _validate_option is all that should be used elsewhere in the code base + # to validate options. It will call the appropriate validation method for that + # option. The individual validation methods should never need to be called directly + # (although nothing bad will happen if they *are*). + # Validation happens in TWO places. + # Firstly, in the property setters defined in the ATTRIBUTE MANAGEMENT section. + # Secondly, in the _get_options method, where keyword arguments are mixed with + # persistent settings + + def _validate_option(self, option, val): + if option == "field_names": + self._validate_field_names(val) + elif option == "none_format": + self._validate_none_format(val) + elif option in ( + "start", + "end", + "max_width", + "min_width", + "min_table_width", + "max_table_width", + "padding_width", + "left_padding_width", + "right_padding_width", + "format", + ): + self._validate_nonnegative_int(option, val) + elif option == "sortby": + self._validate_field_name(option, val) + elif option == "sort_key": + self._validate_function(option, val) + elif option == "hrules": + self._validate_hrules(option, val) + elif option == "vrules": + self._validate_vrules(option, val) + elif option == "fields": + self._validate_all_field_names(option, val) + elif option in ( + "header", + "border", + "preserve_internal_border", + "reversesort", + "xhtml", + "print_empty", + "oldsortslice", + ): + self._validate_true_or_false(option, val) + elif option == "header_style": + self._validate_header_style(val) + elif option == "int_format": + self._validate_int_format(option, val) + elif option == "float_format": + self._validate_float_format(option, val) + elif option == "custom_format": + for k, formatter in val.items(): + self._validate_function(f"{option}.{k}", formatter) + elif option in ( + "vertical_char", + "horizontal_char", + "horizontal_align_char", + "junction_char", + "top_junction_char", + "bottom_junction_char", + "right_junction_char", + "left_junction_char", + "top_right_junction_char", + "top_left_junction_char", + "bottom_right_junction_char", + "bottom_left_junction_char", + ): + self._validate_single_char(option, val) + elif option == "attributes": + self._validate_attributes(option, val) + + def _validate_field_names(self, val): + # Check for appropriate length + if self._field_names: + try: + assert len(val) == len(self._field_names) + except AssertionError: + raise ValueError( + "Field name list has incorrect number of values, " + f"(actual) {len(val)}!={len(self._field_names)} (expected)" + ) + if self._rows: + try: + assert len(val) == len(self._rows[0]) + except AssertionError: + raise ValueError( + "Field name list has incorrect number of values, " + f"(actual) {len(val)}!={len(self._rows[0])} (expected)" + ) + # Check for uniqueness + try: + assert len(val) == len(set(val)) + except AssertionError: + raise ValueError("Field names must be unique") + + def _validate_none_format(self, val): + try: + if val is not None: + assert isinstance(val, str) + except AssertionError: + raise TypeError( + "Replacement for None value must be a string if being supplied." + ) + + def _validate_header_style(self, val): + try: + assert val in ("cap", "title", "upper", "lower", None) + except AssertionError: + raise ValueError( + "Invalid header style, use cap, title, upper, lower or None" + ) + + def _validate_align(self, val): + try: + assert val in ["l", "c", "r"] + except AssertionError: + raise ValueError(f"Alignment {val} is invalid, use l, c or r") + + def _validate_valign(self, val): + try: + assert val in ["t", "m", "b", None] + except AssertionError: + raise ValueError(f"Alignment {val} is invalid, use t, m, b or None") + + def _validate_nonnegative_int(self, name, val): + try: + assert int(val) >= 0 + except AssertionError: + raise ValueError(f"Invalid value for {name}: {val}") + + def _validate_true_or_false(self, name, val): + try: + assert val in (True, False) + except AssertionError: + raise ValueError(f"Invalid value for {name}. Must be True or False.") + + def _validate_int_format(self, name, val): + if val == "": + return + try: + assert isinstance(val, str) + assert val.isdigit() + except AssertionError: + raise ValueError( + f"Invalid value for {name}. Must be an integer format string." + ) + + def _validate_float_format(self, name, val): + if val == "": + return + try: + assert isinstance(val, str) + assert "." in val + bits = val.split(".") + assert len(bits) <= 2 + assert bits[0] == "" or bits[0].isdigit() + assert ( + bits[1] == "" + or bits[1].isdigit() + or (bits[1][-1] == "f" and bits[1].rstrip("f").isdigit()) + ) + except AssertionError: + raise ValueError( + f"Invalid value for {name}. Must be a float format string." + ) + + def _validate_function(self, name, val): + try: + assert hasattr(val, "__call__") + except AssertionError: + raise ValueError(f"Invalid value for {name}. Must be a function.") + + def _validate_hrules(self, name, val): + try: + assert val in (ALL, FRAME, HEADER, NONE) + except AssertionError: + raise ValueError( + f"Invalid value for {name}. Must be ALL, FRAME, HEADER or NONE." + ) + + def _validate_vrules(self, name, val): + try: + assert val in (ALL, FRAME, NONE) + except AssertionError: + raise ValueError(f"Invalid value for {name}. Must be ALL, FRAME, or NONE.") + + def _validate_field_name(self, name, val): + try: + assert (val in self._field_names) or (val is None) + except AssertionError: + raise ValueError(f"Invalid field name: {val}") + + def _validate_all_field_names(self, name, val): + try: + for x in val: + self._validate_field_name(name, x) + except AssertionError: + raise ValueError("Fields must be a sequence of field names") + + def _validate_single_char(self, name, val): + try: + assert _str_block_width(val) == 1 + except AssertionError: + raise ValueError(f"Invalid value for {name}. Must be a string of length 1.") + + def _validate_attributes(self, name, val): + try: + assert isinstance(val, dict) + except AssertionError: + raise TypeError("Attributes must be a dictionary of name/value pairs") + + ############################## + # ATTRIBUTE MANAGEMENT # + ############################## + @property + def rows(self) -> list[Any]: + return self._rows[:] + + @property + def dividers(self) -> list[bool]: + return self._dividers[:] + + @property + def xhtml(self) -> bool: + """Print
tags if True,
tags if False""" + return self._xhtml + + @xhtml.setter + def xhtml(self, val): + self._validate_option("xhtml", val) + self._xhtml = val + + @property + def none_format(self): + return self._none_format + + @none_format.setter + def none_format(self, val): + if not self._field_names: + self._none_format = {} + elif val is None or (isinstance(val, dict) and len(val) == 0): + for field in self._field_names: + self._none_format[field] = None + else: + self._validate_none_format(val) + for field in self._field_names: + self._none_format[field] = val + + @property + def field_names(self): + """List or tuple of field names + + When setting field_names, if there are already field names the new list + of field names must be the same length. Columns are renamed and row data + remains unchanged.""" + return self._field_names + + @field_names.setter + def field_names(self, val): + val = [str(x) for x in val] + self._validate_option("field_names", val) + old_names = None + if self._field_names: + old_names = self._field_names[:] + self._field_names = val + if self._align and old_names: + for old_name, new_name in zip(old_names, val): + self._align[new_name] = self._align[old_name] + for old_name in old_names: + if old_name not in self._align: + self._align.pop(old_name) + elif self._align: + for field_name in self._field_names: + self._align[field_name] = self._align[BASE_ALIGN_VALUE] + else: + self.align = "c" + if self._valign and old_names: + for old_name, new_name in zip(old_names, val): + self._valign[new_name] = self._valign[old_name] + for old_name in old_names: + if old_name not in self._valign: + self._valign.pop(old_name) + else: + self.valign = "t" + + @property + def align(self): + """Controls alignment of fields + Arguments: + + align - alignment, one of "l", "c", or "r" """ + return self._align + + @align.setter + def align(self, val): + if val is None or (isinstance(val, dict) and len(val) == 0): + if not self._field_names: + self._align = {BASE_ALIGN_VALUE: "c"} + else: + for field in self._field_names: + self._align[field] = "c" + else: + self._validate_align(val) + if not self._field_names: + self._align = {BASE_ALIGN_VALUE: val} + else: + for field in self._field_names: + self._align[field] = val + + @property + def valign(self): + """Controls vertical alignment of fields + Arguments: + + valign - vertical alignment, one of "t", "m", or "b" """ + return self._valign + + @valign.setter + def valign(self, val): + if not self._field_names: + self._valign = {} + elif val is None or (isinstance(val, dict) and len(val) == 0): + for field in self._field_names: + self._valign[field] = "t" + else: + self._validate_valign(val) + for field in self._field_names: + self._valign[field] = val + + @property + def max_width(self): + """Controls maximum width of fields + Arguments: + + max_width - maximum width integer""" + return self._max_width + + @max_width.setter + def max_width(self, val): + if val is None or (isinstance(val, dict) and len(val) == 0): + self._max_width = {} + else: + self._validate_option("max_width", val) + for field in self._field_names: + self._max_width[field] = val + + @property + def min_width(self): + """Controls minimum width of fields + Arguments: + + min_width - minimum width integer""" + return self._min_width + + @min_width.setter + def min_width(self, val): + if val is None or (isinstance(val, dict) and len(val) == 0): + self._min_width = {} + else: + self._validate_option("min_width", val) + for field in self._field_names: + self._min_width[field] = val + + @property + def min_table_width(self): + return self._min_table_width + + @min_table_width.setter + def min_table_width(self, val): + self._validate_option("min_table_width", val) + self._min_table_width = val + + @property + def max_table_width(self): + return self._max_table_width + + @max_table_width.setter + def max_table_width(self, val): + self._validate_option("max_table_width", val) + self._max_table_width = val + + @property + def fields(self): + """List or tuple of field names to include in displays""" + return self._fields + + @fields.setter + def fields(self, val): + self._validate_option("fields", val) + self._fields = val + + @property + def title(self): + """Optional table title + + Arguments: + + title - table title""" + return self._title + + @title.setter + def title(self, val): + self._title = str(val) + + @property + def start(self): + """Start index of the range of rows to print + + Arguments: + + start - index of first data row to include in output""" + return self._start + + @start.setter + def start(self, val): + self._validate_option("start", val) + self._start = val + + @property + def end(self): + """End index of the range of rows to print + + Arguments: + + end - index of last data row to include in output PLUS ONE (list slice style)""" + return self._end + + @end.setter + def end(self, val): + self._validate_option("end", val) + self._end = val + + @property + def sortby(self): + """Name of field by which to sort rows + + Arguments: + + sortby - field name to sort by""" + return self._sortby + + @sortby.setter + def sortby(self, val): + self._validate_option("sortby", val) + self._sortby = val + + @property + def reversesort(self): + """Controls direction of sorting (ascending vs descending) + + Arguments: + + reveresort - set to True to sort by descending order, or False to sort by + ascending order""" + return self._reversesort + + @reversesort.setter + def reversesort(self, val): + self._validate_option("reversesort", val) + self._reversesort = val + + @property + def sort_key(self): + """Sorting key function, applied to data points before sorting + + Arguments: + + sort_key - a function which takes one argument and returns something to be + sorted""" + return self._sort_key + + @sort_key.setter + def sort_key(self, val): + self._validate_option("sort_key", val) + self._sort_key = val + + @property + def header(self): + """Controls printing of table header with field names + + Arguments: + + header - print a header showing field names (True or False)""" + return self._header + + @header.setter + def header(self, val): + self._validate_option("header", val) + self._header = val + + @property + def header_style(self): + """Controls stylisation applied to field names in header + + Arguments: + + header_style - stylisation to apply to field names in header + ("cap", "title", "upper", "lower" or None)""" + return self._header_style + + @header_style.setter + def header_style(self, val): + self._validate_header_style(val) + self._header_style = val + + @property + def border(self): + """Controls printing of border around table + + Arguments: + + border - print a border around the table (True or False)""" + return self._border + + @border.setter + def border(self, val): + self._validate_option("border", val) + self._border = val + + @property + def preserve_internal_border(self): + """Controls printing of border inside table + + Arguments: + + preserve_internal_border - print a border inside the table even if + border is disabled (True or False)""" + return self._preserve_internal_border + + @preserve_internal_border.setter + def preserve_internal_border(self, val): + self._validate_option("preserve_internal_border", val) + self._preserve_internal_border = val + + @property + def hrules(self): + """Controls printing of horizontal rules after rows + + Arguments: + + hrules - horizontal rules style. Allowed values: FRAME, ALL, HEADER, NONE""" + return self._hrules + + @hrules.setter + def hrules(self, val): + self._validate_option("hrules", val) + self._hrules = val + + @property + def vrules(self): + """Controls printing of vertical rules between columns + + Arguments: + + vrules - vertical rules style. Allowed values: FRAME, ALL, NONE""" + return self._vrules + + @vrules.setter + def vrules(self, val): + self._validate_option("vrules", val) + self._vrules = val + + @property + def int_format(self): + """Controls formatting of integer data + Arguments: + + int_format - integer format string""" + return self._int_format + + @int_format.setter + def int_format(self, val): + if val is None or (isinstance(val, dict) and len(val) == 0): + self._int_format = {} + else: + self._validate_option("int_format", val) + for field in self._field_names: + self._int_format[field] = val + + @property + def float_format(self): + """Controls formatting of floating point data + Arguments: + + float_format - floating point format string""" + return self._float_format + + @float_format.setter + def float_format(self, val): + if val is None or (isinstance(val, dict) and len(val) == 0): + self._float_format = {} + else: + self._validate_option("float_format", val) + for field in self._field_names: + self._float_format[field] = val + + @property + def custom_format(self): + """Controls formatting of any column using callable + Arguments: + + custom_format - Dictionary of field_name and callable""" + return self._custom_format + + @custom_format.setter + def custom_format(self, val): + if val is None: + self._custom_format = {} + elif isinstance(val, dict): + for k, v in val.items(): + self._validate_function(f"custom_value.{k}", v) + self._custom_format = val + elif hasattr(val, "__call__"): + self._validate_function("custom_value", val) + for field in self._field_names: + self._custom_format[field] = val + else: + raise TypeError( + "The custom_format property need to be a dictionary or callable" + ) + + @property + def padding_width(self): + """The number of empty spaces between a column's edge and its content + + Arguments: + + padding_width - number of spaces, must be a positive integer""" + return self._padding_width + + @padding_width.setter + def padding_width(self, val): + self._validate_option("padding_width", val) + self._padding_width = val + + @property + def left_padding_width(self): + """The number of empty spaces between a column's left edge and its content + + Arguments: + + left_padding - number of spaces, must be a positive integer""" + return self._left_padding_width + + @left_padding_width.setter + def left_padding_width(self, val): + self._validate_option("left_padding_width", val) + self._left_padding_width = val + + @property + def right_padding_width(self): + """The number of empty spaces between a column's right edge and its content + + Arguments: + + right_padding - number of spaces, must be a positive integer""" + return self._right_padding_width + + @right_padding_width.setter + def right_padding_width(self, val): + self._validate_option("right_padding_width", val) + self._right_padding_width = val + + @property + def vertical_char(self): + """The character used when printing table borders to draw vertical lines + + Arguments: + + vertical_char - single character string used to draw vertical lines""" + return self._vertical_char + + @vertical_char.setter + def vertical_char(self, val): + val = str(val) + self._validate_option("vertical_char", val) + self._vertical_char = val + + @property + def horizontal_char(self): + """The character used when printing table borders to draw horizontal lines + + Arguments: + + horizontal_char - single character string used to draw horizontal lines""" + return self._horizontal_char + + @horizontal_char.setter + def horizontal_char(self, val): + val = str(val) + self._validate_option("horizontal_char", val) + self._horizontal_char = val + + @property + def horizontal_align_char(self): + """The character used to indicate column alignment in horizontal lines + + Arguments: + + horizontal_align_char - single character string used to indicate alignment""" + return self._bottom_left_junction_char or self.junction_char + + @horizontal_align_char.setter + def horizontal_align_char(self, val): + val = str(val) + self._validate_option("horizontal_align_char", val) + self._horizontal_align_char = val + + @property + def junction_char(self): + """The character used when printing table borders to draw line junctions + + Arguments: + + junction_char - single character string used to draw line junctions""" + return self._junction_char + + @junction_char.setter + def junction_char(self, val): + val = str(val) + self._validate_option("junction_char", val) + self._junction_char = val + + @property + def top_junction_char(self): + """The character used when printing table borders to draw top line junctions + + Arguments: + + top_junction_char - single character string used to draw top line junctions""" + return self._top_junction_char or self.junction_char + + @top_junction_char.setter + def top_junction_char(self, val): + val = str(val) + self._validate_option("top_junction_char", val) + self._top_junction_char = val + + @property + def bottom_junction_char(self): + """The character used when printing table borders to draw bottom line junctions + + Arguments: + + bottom_junction_char - + single character string used to draw bottom line junctions""" + return self._bottom_junction_char or self.junction_char + + @bottom_junction_char.setter + def bottom_junction_char(self, val): + val = str(val) + self._validate_option("bottom_junction_char", val) + self._bottom_junction_char = val + + @property + def right_junction_char(self): + """The character used when printing table borders to draw right line junctions + + Arguments: + + right_junction_char - + single character string used to draw right line junctions""" + return self._right_junction_char or self.junction_char + + @right_junction_char.setter + def right_junction_char(self, val): + val = str(val) + self._validate_option("right_junction_char", val) + self._right_junction_char = val + + @property + def left_junction_char(self): + """The character used when printing table borders to draw left line junctions + + Arguments: + + left_junction_char - single character string used to draw left line junctions""" + return self._left_junction_char or self.junction_char + + @left_junction_char.setter + def left_junction_char(self, val): + val = str(val) + self._validate_option("left_junction_char", val) + self._left_junction_char = val + + @property + def top_right_junction_char(self): + """ + The character used when printing table borders to draw top-right line junctions + + Arguments: + + top_right_junction_char - + single character string used to draw top-right line junctions""" + return self._top_right_junction_char or self.junction_char + + @top_right_junction_char.setter + def top_right_junction_char(self, val): + val = str(val) + self._validate_option("top_right_junction_char", val) + self._top_right_junction_char = val + + @property + def top_left_junction_char(self): + """ + The character used when printing table borders to draw top-left line junctions + + Arguments: + + top_left_junction_char - + single character string used to draw top-left line junctions""" + return self._top_left_junction_char or self.junction_char + + @top_left_junction_char.setter + def top_left_junction_char(self, val): + val = str(val) + self._validate_option("top_left_junction_char", val) + self._top_left_junction_char = val + + @property + def bottom_right_junction_char(self): + """The character used when printing table borders + to draw bottom-right line junctions + + Arguments: + + bottom_right_junction_char - + single character string used to draw bottom-right line junctions""" + return self._bottom_right_junction_char or self.junction_char + + @bottom_right_junction_char.setter + def bottom_right_junction_char(self, val): + val = str(val) + self._validate_option("bottom_right_junction_char", val) + self._bottom_right_junction_char = val + + @property + def bottom_left_junction_char(self): + """The character used when printing table borders + to draw bottom-left line junctions + + Arguments: + + bottom_left_junction_char - + single character string used to draw bottom-left line junctions""" + return self._bottom_left_junction_char or self.junction_char + + @bottom_left_junction_char.setter + def bottom_left_junction_char(self, val): + val = str(val) + self._validate_option("bottom_left_junction_char", val) + self._bottom_left_junction_char = val + + @property + def format(self): + """Controls whether or not HTML tables are formatted to match styling options + + Arguments: + + format - True or False""" + return self._format + + @format.setter + def format(self, val): + self._validate_option("format", val) + self._format = val + + @property + def print_empty(self): + """Controls whether or not empty tables produce a header and frame or just an + empty string + + Arguments: + + print_empty - True or False""" + return self._print_empty + + @print_empty.setter + def print_empty(self, val): + self._validate_option("print_empty", val) + self._print_empty = val + + @property + def attributes(self): + """A dictionary of HTML attribute name/value pairs to be included in the + tag when printing HTML + + Arguments: + + attributes - dictionary of attributes""" + return self._attributes + + @attributes.setter + def attributes(self, val): + self._validate_option("attributes", val) + self._attributes = val + + @property + def oldsortslice(self): + """oldsortslice - Slice rows before sorting in the "old style" """ + return self._oldsortslice + + @oldsortslice.setter + def oldsortslice(self, val): + self._validate_option("oldsortslice", val) + self._oldsortslice = val + + ############################## + # OPTION MIXER # + ############################## + + def _get_options(self, kwargs): + options = {} + for option in self._options: + if option in kwargs: + self._validate_option(option, kwargs[option]) + options[option] = kwargs[option] + else: + options[option] = getattr(self, option) + return options + + ############################## + # PRESET STYLE LOGIC # + ############################## + + def set_style(self, style) -> None: + if style == DEFAULT: + self._set_default_style() + elif style == MSWORD_FRIENDLY: + self._set_msword_style() + elif style == PLAIN_COLUMNS: + self._set_columns_style() + elif style == MARKDOWN: + self._set_markdown_style() + elif style == ORGMODE: + self._set_orgmode_style() + elif style == DOUBLE_BORDER: + self._set_double_border_style() + elif style == SINGLE_BORDER: + self._set_single_border_style() + elif style == RANDOM: + self._set_random_style() + else: + raise ValueError("Invalid pre-set style") + + def _set_orgmode_style(self): + self._set_default_style() + self.orgmode = True + + def _set_markdown_style(self): + self.header = True + self.border = True + self._hrules = None + self.padding_width = 1 + self.left_padding_width = 1 + self.right_padding_width = 1 + self.vertical_char = "|" + self.junction_char = "|" + self._horizontal_align_char = ":" + + def _set_default_style(self): + self.header = True + self.border = True + self._hrules = FRAME + self._vrules = ALL + self.padding_width = 1 + self.left_padding_width = 1 + self.right_padding_width = 1 + self.vertical_char = "|" + self.horizontal_char = "-" + self._horizontal_align_char = None + self.junction_char = "+" + self._top_junction_char = None + self._bottom_junction_char = None + self._right_junction_char = None + self._left_junction_char = None + self._top_right_junction_char = None + self._top_left_junction_char = None + self._bottom_right_junction_char = None + self._bottom_left_junction_char = None + + def _set_msword_style(self): + self.header = True + self.border = True + self._hrules = NONE + self.padding_width = 1 + self.left_padding_width = 1 + self.right_padding_width = 1 + self.vertical_char = "|" + + def _set_columns_style(self): + self.header = True + self.border = False + self.padding_width = 1 + self.left_padding_width = 0 + self.right_padding_width = 8 + + def _set_double_border_style(self): + self.horizontal_char = "═" + self.vertical_char = "║" + self.junction_char = "╬" + self.top_junction_char = "╦" + self.bottom_junction_char = "╩" + self.right_junction_char = "╣" + self.left_junction_char = "╠" + self.top_right_junction_char = "╗" + self.top_left_junction_char = "╔" + self.bottom_right_junction_char = "╝" + self.bottom_left_junction_char = "╚" + + def _set_single_border_style(self): + self.horizontal_char = "─" + self.vertical_char = "│" + self.junction_char = "┼" + self.top_junction_char = "┬" + self.bottom_junction_char = "┴" + self.right_junction_char = "┤" + self.left_junction_char = "├" + self.top_right_junction_char = "┐" + self.top_left_junction_char = "┌" + self.bottom_right_junction_char = "┘" + self.bottom_left_junction_char = "└" + + def _set_random_style(self): + # Just for fun! + self.header = random.choice((True, False)) + self.border = random.choice((True, False)) + self._hrules = random.choice((ALL, FRAME, HEADER, NONE)) + self._vrules = random.choice((ALL, FRAME, NONE)) + self.left_padding_width = random.randint(0, 5) + self.right_padding_width = random.randint(0, 5) + self.vertical_char = random.choice(r"~!@#$%^&*()_+|-=\{}[];':\",./;<>?") + self.horizontal_char = random.choice(r"~!@#$%^&*()_+|-=\{}[];':\",./;<>?") + self.junction_char = random.choice(r"~!@#$%^&*()_+|-=\{}[];':\",./;<>?") + self.preserve_internal_border = random.choice((True, False)) + + ############################## + # DATA INPUT METHODS # + ############################## + + def add_rows(self, rows) -> None: + """Add rows to the table + + Arguments: + + rows - rows of data, should be an iterable of lists, each list with as many + elements as the table has fields""" + for row in rows: + self.add_row(row) + + def add_row(self, row, *, divider=False) -> None: + """Add a row to the table + + Arguments: + + row - row of data, should be a list with as many elements as the table + has fields""" + + if self._field_names and len(row) != len(self._field_names): + raise ValueError( + "Row has incorrect number of values, " + f"(actual) {len(row)}!={len(self._field_names)} (expected)" + ) + if not self._field_names: + self.field_names = [f"Field {n + 1}" for n in range(0, len(row))] + self._rows.append(list(row)) + self._dividers.append(divider) + + def del_row(self, row_index) -> None: + """Delete a row from the table + + Arguments: + + row_index - The index of the row you want to delete. Indexing starts at 0.""" + + if row_index > len(self._rows) - 1: + raise IndexError( + f"Can't delete row at index {row_index}, " + f"table only has {len(self._rows)} rows" + ) + del self._rows[row_index] + del self._dividers[row_index] + + def add_column( + self, fieldname, column, align: str = "c", valign: str = "t" + ) -> None: + """Add a column to the table. + + Arguments: + + fieldname - name of the field to contain the new column of data + column - column of data, should be a list with as many elements as the + table has rows + align - desired alignment for this column - "l" for left, "c" for centre and + "r" for right + valign - desired vertical alignment for new columns - "t" for top, + "m" for middle and "b" for bottom""" + + if len(self._rows) in (0, len(column)): + self._validate_align(align) + self._validate_valign(valign) + self._field_names.append(fieldname) + self._align[fieldname] = align + self._valign[fieldname] = valign + for i in range(0, len(column)): + if len(self._rows) < i + 1: + self._rows.append([]) + self._dividers.append(False) + self._rows[i].append(column[i]) + else: + raise ValueError( + f"Column length {len(column)} does not match number of rows " + f"{len(self._rows)}" + ) + + def add_autoindex(self, fieldname: str = "Index"): + """Add an auto-incrementing index column to the table. + Arguments: + fieldname - name of the field to contain the new column of data""" + self._field_names.insert(0, fieldname) + self._align[fieldname] = self.align + self._valign[fieldname] = self.valign + for i, row in enumerate(self._rows): + row.insert(0, i + 1) + + def del_column(self, fieldname) -> None: + """Delete a column from the table + + Arguments: + + fieldname - The field name of the column you want to delete.""" + + if fieldname not in self._field_names: + raise ValueError( + "Can't delete column %r which is not a field name of this table." + " Field names are: %s" + % (fieldname, ", ".join(map(repr, self._field_names))) + ) + + col_index = self._field_names.index(fieldname) + del self._field_names[col_index] + for row in self._rows: + del row[col_index] + + def clear_rows(self) -> None: + """Delete all rows from the table but keep the current field names""" + + self._rows = [] + self._dividers = [] + + def clear(self) -> None: + """Delete all rows and field names from the table, maintaining nothing but + styling options""" + + self._rows = [] + self._dividers = [] + self._field_names = [] + self._widths = [] + + ############################## + # MISC PUBLIC METHODS # + ############################## + + def copy(self): + return copy.deepcopy(self) + + ############################## + # MISC PRIVATE METHODS # + ############################## + + def _format_value(self, field, value): + if isinstance(value, int) and field in self._int_format: + return ("%%%sd" % self._int_format[field]) % value + elif isinstance(value, float) and field in self._float_format: + return ("%%%sf" % self._float_format[field]) % value + + formatter = self._custom_format.get(field, (lambda f, v: str(v))) + return formatter(field, value) + + def _compute_table_width(self, options): + table_width = 2 if options["vrules"] in (FRAME, ALL) else 0 + per_col_padding = sum(self._get_padding_widths(options)) + for index, fieldname in enumerate(self.field_names): + if not options["fields"] or ( + options["fields"] and fieldname in options["fields"] + ): + table_width += self._widths[index] + per_col_padding + return table_width + + def _compute_widths(self, rows, options): + if options["header"]: + widths = [_get_size(field)[0] for field in self._field_names] + else: + widths = len(self.field_names) * [0] + + for row in rows: + for index, value in enumerate(row): + fieldname = self.field_names[index] + if self.none_format.get(fieldname) is not None: + if value == "None" or value is None: + value = self.none_format.get(fieldname) + if fieldname in self.max_width: + widths[index] = max( + widths[index], + min(_get_size(value)[0], self.max_width[fieldname]), + ) + else: + widths[index] = max(widths[index], _get_size(value)[0]) + if fieldname in self.min_width: + widths[index] = max(widths[index], self.min_width[fieldname]) + self._widths = widths + + # Are we exceeding max_table_width? + if self._max_table_width: + table_width = self._compute_table_width(options) + if table_width > self._max_table_width: + # Shrink widths in proportion + scale = 1.0 * self._max_table_width / table_width + widths = [int(math.floor(w * scale)) for w in widths] + self._widths = widths + + # Are we under min_table_width or title width? + if self._min_table_width or options["title"]: + if options["title"]: + title_width = len(options["title"]) + sum( + self._get_padding_widths(options) + ) + if options["vrules"] in (FRAME, ALL): + title_width += 2 + else: + title_width = 0 + min_table_width = self.min_table_width or 0 + min_width = max(title_width, min_table_width) + if options["border"]: + borders = len(widths) + 1 + elif options["preserve_internal_border"]: + borders = len(widths) + else: + borders = 0 + + # Subtract padding for each column and borders + min_width -= ( + sum([sum(self._get_padding_widths(options)) for _ in widths]) + borders + ) + # What is being scaled is content so we sum column widths + content_width = sum(widths) or 1 + + if content_width < min_width: + # Grow widths in proportion + scale = 1.0 * min_width / content_width + widths = [int(math.floor(w * scale)) for w in widths] + if sum(widths) < min_width: + widths[-1] += min_width - sum(widths) + self._widths = widths + + def _get_padding_widths(self, options): + if options["left_padding_width"] is not None: + lpad = options["left_padding_width"] + else: + lpad = options["padding_width"] + if options["right_padding_width"] is not None: + rpad = options["right_padding_width"] + else: + rpad = options["padding_width"] + return lpad, rpad + + def _get_rows(self, options): + """Return only those data rows that should be printed, based on slicing and + sorting. + + Arguments: + + options - dictionary of option settings.""" + + if options["oldsortslice"]: + rows = copy.deepcopy(self._rows[options["start"] : options["end"]]) + else: + rows = copy.deepcopy(self._rows) + + # Sort + if options["sortby"]: + sortindex = self._field_names.index(options["sortby"]) + # Decorate + rows = [[row[sortindex]] + row for row in rows] + # Sort + rows.sort(reverse=options["reversesort"], key=options["sort_key"]) + # Undecorate + rows = [row[1:] for row in rows] + + # Slice if necessary + if not options["oldsortslice"]: + rows = rows[options["start"] : options["end"]] + + return rows + + def _get_dividers(self, options): + """Return only those dividers that should be printed, based on slicing. + + Arguments: + + options - dictionary of option settings.""" + + if options["oldsortslice"]: + dividers = copy.deepcopy(self._dividers[options["start"] : options["end"]]) + else: + dividers = copy.deepcopy(self._dividers) + + if options["sortby"]: + dividers = [False for divider in dividers] + + return dividers + + def _format_row(self, row): + return [ + self._format_value(field, value) + for (field, value) in zip(self._field_names, row) + ] + + def _format_rows(self, rows): + return [self._format_row(row) for row in rows] + + ############################## + # PLAIN TEXT STRING METHODS # + ############################## + + def get_string(self, **kwargs) -> str: + """Return string representation of table in current state. + + Arguments: + + title - optional table title + start - index of first data row to include in output + end - index of last data row to include in output PLUS ONE (list slice style) + fields - names of fields (columns) to include + header - print a header showing field names (True or False) + border - print a border around the table (True or False) + preserve_internal_border - print a border inside the table even if + border is disabled (True or False) + hrules - controls printing of horizontal rules after rows. + Allowed values: ALL, FRAME, HEADER, NONE + vrules - controls printing of vertical rules between columns. + Allowed values: FRAME, ALL, NONE + int_format - controls formatting of integer data + float_format - controls formatting of floating point data + custom_format - controls formatting of any column using callable + padding_width - number of spaces on either side of column data (only used if + left and right paddings are None) + left_padding_width - number of spaces on left hand side of column data + right_padding_width - number of spaces on right hand side of column data + vertical_char - single character string used to draw vertical lines + horizontal_char - single character string used to draw horizontal lines + horizontal_align_char - single character string used to indicate alignment + junction_char - single character string used to draw line junctions + junction_char - single character string used to draw line junctions + top_junction_char - single character string used to draw top line junctions + bottom_junction_char - + single character string used to draw bottom line junctions + right_junction_char - single character string used to draw right line junctions + left_junction_char - single character string used to draw left line junctions + top_right_junction_char - + single character string used to draw top-right line junctions + top_left_junction_char - + single character string used to draw top-left line junctions + bottom_right_junction_char - + single character string used to draw bottom-right line junctions + bottom_left_junction_char - + single character string used to draw bottom-left line junctions + sortby - name of field to sort rows by + sort_key - sorting key function, applied to data points before sorting + reversesort - True or False to sort in descending or ascending order + print empty - if True, stringify just the header for an empty table, + if False return an empty string""" + + options = self._get_options(kwargs) + + lines = [] + + # Don't think too hard about an empty table + # Is this the desired behaviour? Maybe we should still print the header? + if self.rowcount == 0 and (not options["print_empty"] or not options["border"]): + return "" + + # Get the rows we need to print, taking into account slicing, sorting, etc. + rows = self._get_rows(options) + dividers = self._get_dividers(options) + + # Turn all data in all rows into Unicode, formatted as desired + formatted_rows = self._format_rows(rows) + + # Compute column widths + self._compute_widths(formatted_rows, options) + self._hrule = self._stringify_hrule(options) + + # Add title + title = options["title"] or self._title + if title: + lines.append(self._stringify_title(title, options)) + + # Add header or top of border + if options["header"]: + lines.append(self._stringify_header(options)) + elif options["border"] and options["hrules"] in (ALL, FRAME): + lines.append(self._stringify_hrule(options, where="top_")) + if title and options["vrules"] in (ALL, FRAME): + lines[-1] = ( + self.left_junction_char + lines[-1][1:-1] + self.right_junction_char + ) + + # Add rows + for row, divider in zip(formatted_rows[:-1], dividers[:-1]): + lines.append(self._stringify_row(row, options, self._hrule)) + if divider: + lines.append(self._stringify_hrule(options, where="bottom_")) + if formatted_rows: + lines.append( + self._stringify_row( + formatted_rows[-1], + options, + self._stringify_hrule(options, where="bottom_"), + ) + ) + + # Add bottom of border + if options["border"] and options["hrules"] == FRAME: + lines.append(self._stringify_hrule(options, where="bottom_")) + + if "orgmode" in self.__dict__ and self.orgmode is True: + tmp = list() + for line in lines: + tmp.extend(line.split("\n")) + lines = ["|" + line[1:-1] + "|" for line in tmp] + + return "\n".join(lines) + + def _stringify_hrule(self, options, where=""): + if not options["border"] and not options["preserve_internal_border"]: + return "" + lpad, rpad = self._get_padding_widths(options) + if options["vrules"] in (ALL, FRAME): + bits = [options[where + "left_junction_char"]] + else: + bits = [options["horizontal_char"]] + # For tables with no data or fieldnames + if not self._field_names: + bits.append(options[where + "right_junction_char"]) + return "".join(bits) + for field, width in zip(self._field_names, self._widths): + if options["fields"] and field not in options["fields"]: + continue + + line = (width + lpad + rpad) * options["horizontal_char"] + + # If necessary, add column alignment characters (e.g. ":" for Markdown) + if self._horizontal_align_char: + if self._align[field] in ("l", "c"): + line = self._horizontal_align_char + line[1:] + if self._align[field] in ("c", "r"): + line = line[:-1] + self._horizontal_align_char + + bits.append(line) + if options["vrules"] == ALL: + bits.append(options[where + "junction_char"]) + else: + bits.append(options["horizontal_char"]) + if options["vrules"] in (ALL, FRAME): + bits.pop() + bits.append(options[where + "right_junction_char"]) + + if options["preserve_internal_border"] and not options["border"]: + bits = bits[1:-1] + + return "".join(bits) + + def _stringify_title(self, title, options): + lines = [] + lpad, rpad = self._get_padding_widths(options) + if options["border"]: + if options["vrules"] == ALL: + options["vrules"] = FRAME + lines.append(self._stringify_hrule(options, "top_")) + options["vrules"] = ALL + elif options["vrules"] == FRAME: + lines.append(self._stringify_hrule(options, "top_")) + bits = [] + endpoint = ( + options["vertical_char"] + if options["vrules"] in (ALL, FRAME) and options["border"] + else " " + ) + bits.append(endpoint) + title = " " * lpad + title + " " * rpad + bits.append(self._justify(title, len(self._hrule) - 2, "c")) + bits.append(endpoint) + lines.append("".join(bits)) + return "\n".join(lines) + + def _stringify_header(self, options): + bits = [] + lpad, rpad = self._get_padding_widths(options) + if options["border"]: + if options["hrules"] in (ALL, FRAME): + bits.append(self._stringify_hrule(options, "top_")) + if options["title"] and options["vrules"] in (ALL, FRAME): + bits[-1] = ( + self.left_junction_char + + bits[-1][1:-1] + + self.right_junction_char + ) + bits.append("\n") + if options["vrules"] in (ALL, FRAME): + bits.append(options["vertical_char"]) + else: + bits.append(" ") + # For tables with no data or field names + if not self._field_names: + if options["vrules"] in (ALL, FRAME): + bits.append(options["vertical_char"]) + else: + bits.append(" ") + for field, width in zip(self._field_names, self._widths): + if options["fields"] and field not in options["fields"]: + continue + if self._header_style == "cap": + fieldname = field.capitalize() + elif self._header_style == "title": + fieldname = field.title() + elif self._header_style == "upper": + fieldname = field.upper() + elif self._header_style == "lower": + fieldname = field.lower() + else: + fieldname = field + if _str_block_width(fieldname) > width: + fieldname = fieldname[:width] + bits.append( + " " * lpad + + self._justify(fieldname, width, self._align[field]) + + " " * rpad + ) + if options["border"] or options["preserve_internal_border"]: + if options["vrules"] == ALL: + bits.append(options["vertical_char"]) + else: + bits.append(" ") + + # If only preserve_internal_border is true, then we just appended + # a vertical character at the end when we wanted a space + if not options["border"] and options["preserve_internal_border"]: + bits.pop() + bits.append(" ") + # If vrules is FRAME, then we just appended a space at the end + # of the last field, when we really want a vertical character + if options["border"] and options["vrules"] == FRAME: + bits.pop() + bits.append(options["vertical_char"]) + if (options["border"] or options["preserve_internal_border"]) and options[ + "hrules" + ] != NONE: + bits.append("\n") + bits.append(self._hrule) + return "".join(bits) + + def _stringify_row(self, row, options, hrule): + for index, field, value, width in zip( + range(0, len(row)), self._field_names, row, self._widths + ): + # Enforce max widths + lines = value.split("\n") + new_lines = [] + for line in lines: + if line == "None" and self.none_format.get(field) is not None: + line = self.none_format[field] + if _str_block_width(line) > width: + line = textwrap.fill(line, width) + new_lines.append(line) + lines = new_lines + value = "\n".join(lines) + row[index] = value + + row_height = 0 + for c in row: + h = _get_size(c)[1] + if h > row_height: + row_height = h + + bits = [] + lpad, rpad = self._get_padding_widths(options) + for y in range(0, row_height): + bits.append([]) + if options["border"]: + if options["vrules"] in (ALL, FRAME): + bits[y].append(self.vertical_char) + else: + bits[y].append(" ") + + for field, value, width in zip(self._field_names, row, self._widths): + valign = self._valign[field] + lines = value.split("\n") + d_height = row_height - len(lines) + if d_height: + if valign == "m": + lines = ( + [""] * int(d_height / 2) + + lines + + [""] * (d_height - int(d_height / 2)) + ) + elif valign == "b": + lines = [""] * d_height + lines + else: + lines = lines + [""] * d_height + + y = 0 + for line in lines: + if options["fields"] and field not in options["fields"]: + continue + + bits[y].append( + " " * lpad + + self._justify(line, width, self._align[field]) + + " " * rpad + ) + if options["border"] or options["preserve_internal_border"]: + if options["vrules"] == ALL: + bits[y].append(self.vertical_char) + else: + bits[y].append(" ") + y += 1 + + # If only preserve_internal_border is true, then we just appended + # a vertical character at the end when we wanted a space + if not options["border"] and options["preserve_internal_border"]: + bits[-1].pop() + bits[-1].append(" ") + + # If vrules is FRAME, then we just appended a space at the end + # of the last field, when we really want a vertical character + for y in range(0, row_height): + if options["border"] and options["vrules"] == FRAME: + bits[y].pop() + bits[y].append(options["vertical_char"]) + + if options["border"] and options["hrules"] == ALL: + bits[row_height - 1].append("\n") + bits[row_height - 1].append(hrule) + + for y in range(0, row_height): + bits[y] = "".join(bits[y]) + + return "\n".join(bits) + + def paginate(self, page_length: int = 58, line_break: str = "\f", **kwargs): + pages = [] + kwargs["start"] = kwargs.get("start", 0) + true_end = kwargs.get("end", self.rowcount) + while True: + kwargs["end"] = min(kwargs["start"] + page_length, true_end) + pages.append(self.get_string(**kwargs)) + if kwargs["end"] == true_end: + break + kwargs["start"] += page_length + return line_break.join(pages) + + ############################## + # CSV STRING METHODS # + ############################## + def get_csv_string(self, **kwargs) -> str: + """Return string representation of CSV formatted table in the current state + + Keyword arguments are first interpreted as table formatting options, and + then any unused keyword arguments are passed to csv.writer(). For + example, get_csv_string(header=False, delimiter='\t') would use + header as a PrettyTable formatting option (skip the header row) and + delimiter as a csv.writer keyword argument. + """ + + options = self._get_options(kwargs) + csv_options = { + key: value for key, value in kwargs.items() if key not in options + } + csv_buffer = io.StringIO() + csv_writer = csv.writer(csv_buffer, **csv_options) + + if options.get("header"): + csv_writer.writerow(self._field_names) + for row in self._get_rows(options): + csv_writer.writerow(row) + + return csv_buffer.getvalue() + + ############################## + # JSON STRING METHODS # + ############################## + def get_json_string(self, **kwargs) -> str: + """Return string representation of JSON formatted table in the current state + + Keyword arguments are first interpreted as table formatting options, and + then any unused keyword arguments are passed to json.dumps(). For + example, get_json_string(header=False, indent=2) would use header as + a PrettyTable formatting option (skip the header row) and indent as a + json.dumps keyword argument. + """ + + options = self._get_options(kwargs) + json_options: Any = dict(indent=4, separators=(",", ": "), sort_keys=True) + json_options.update( + {key: value for key, value in kwargs.items() if key not in options} + ) + objects = [] + + if options.get("header"): + objects.append(self.field_names) + for row in self._get_rows(options): + objects.append(dict(zip(self._field_names, row))) + + return json.dumps(objects, **json_options) + + ############################## + # HTML STRING METHODS # + ############################## + + def get_html_string(self, **kwargs) -> str: + """Return string representation of HTML formatted version of table in current + state. + + Arguments: + + title - optional table title + start - index of first data row to include in output + end - index of last data row to include in output PLUS ONE (list slice style) + fields - names of fields (columns) to include + header - print a header showing field names (True or False) + border - print a border around the table (True or False) + preserve_internal_border - print a border inside the table even if + border is disabled (True or False) + hrules - controls printing of horizontal rules after rows. + Allowed values: ALL, FRAME, HEADER, NONE + vrules - controls printing of vertical rules between columns. + Allowed values: FRAME, ALL, NONE + int_format - controls formatting of integer data + float_format - controls formatting of floating point data + custom_format - controls formatting of any column using callable + padding_width - number of spaces on either side of column data (only used if + left and right paddings are None) + left_padding_width - number of spaces on left hand side of column data + right_padding_width - number of spaces on right hand side of column data + sortby - name of field to sort rows by + sort_key - sorting key function, applied to data points before sorting + attributes - dictionary of name/value pairs to include as HTML attributes in the +
tag + format - Controls whether or not HTML tables are formatted to match + styling options (True or False) + xhtml - print
tags if True,
tags if False""" + + options = self._get_options(kwargs) + + if options["format"]: + string = self._get_formatted_html_string(options) + else: + string = self._get_simple_html_string(options) + + return string + + def _get_simple_html_string(self, options): + lines = [] + if options["xhtml"]: + linebreak = "
" + else: + linebreak = "
" + + open_tag = ["") + lines.append("".join(open_tag)) + + # Title + title = options["title"] or self._title + if title: + lines.append(f" ") + + # Headers + if options["header"]: + lines.append(" ") + lines.append(" ") + for field in self._field_names: + if options["fields"] and field not in options["fields"]: + continue + lines.append( + " " % escape(field).replace("\n", linebreak) + ) + lines.append(" ") + lines.append(" ") + + # Data + lines.append(" ") + rows = self._get_rows(options) + formatted_rows = self._format_rows(rows) + for row in formatted_rows: + lines.append(" ") + for field, datum in zip(self._field_names, row): + if options["fields"] and field not in options["fields"]: + continue + lines.append( + " " % escape(datum).replace("\n", linebreak) + ) + lines.append(" ") + lines.append(" ") + lines.append("
{title}
%s
%s
") + + return "\n".join(lines) + + def _get_formatted_html_string(self, options): + lines = [] + lpad, rpad = self._get_padding_widths(options) + if options["xhtml"]: + linebreak = "
" + else: + linebreak = "
" + + open_tag = ["") + lines.append("".join(open_tag)) + + # Title + title = options["title"] or self._title + if title: + lines.append(f" {title}") + + # Headers + if options["header"]: + lines.append(" ") + lines.append(" ") + for field in self._field_names: + if options["fields"] and field not in options["fields"]: + continue + lines.append( + ' %s' # noqa: E501 + % (lpad, rpad, escape(field).replace("\n", linebreak)) + ) + lines.append(" ") + lines.append(" ") + + # Data + lines.append(" ") + rows = self._get_rows(options) + formatted_rows = self._format_rows(rows) + aligns = [] + valigns = [] + for field in self._field_names: + aligns.append( + {"l": "left", "r": "right", "c": "center"}[self._align[field]] + ) + valigns.append( + {"t": "top", "m": "middle", "b": "bottom"}[self._valign[field]] + ) + for row in formatted_rows: + lines.append(" ") + for field, datum, align, valign in zip( + self._field_names, row, aligns, valigns + ): + if options["fields"] and field not in options["fields"]: + continue + lines.append( + ' %s' # noqa: E501 + % ( + lpad, + rpad, + align, + valign, + escape(datum).replace("\n", linebreak), + ) + ) + lines.append(" ") + lines.append(" ") + lines.append("") + + return "\n".join(lines) + + ############################## + # LATEX STRING METHODS # + ############################## + + def get_latex_string(self, **kwargs) -> str: + """Return string representation of LaTex formatted version of table in current + state. + + Arguments: + + start - index of first data row to include in output + end - index of last data row to include in output PLUS ONE (list slice style) + fields - names of fields (columns) to include + header - print a header showing field names (True or False) + border - print a border around the table (True or False) + preserve_internal_border - print a border inside the table even if + border is disabled (True or False) + hrules - controls printing of horizontal rules after rows. + Allowed values: ALL, FRAME, HEADER, NONE + vrules - controls printing of vertical rules between columns. + Allowed values: FRAME, ALL, NONE + int_format - controls formatting of integer data + float_format - controls formatting of floating point data + sortby - name of field to sort rows by + sort_key - sorting key function, applied to data points before sorting + format - Controls whether or not HTML tables are formatted to match + styling options (True or False) + """ + options = self._get_options(kwargs) + + if options["format"]: + string = self._get_formatted_latex_string(options) + else: + string = self._get_simple_latex_string(options) + return string + + def _get_simple_latex_string(self, options): + lines = [] + + wanted_fields = [] + if options["fields"]: + wanted_fields = [ + field for field in self._field_names if field in options["fields"] + ] + else: + wanted_fields = self._field_names + + alignments = "".join([self._align[field] for field in wanted_fields]) + + begin_cmd = "\\begin{tabular}{%s}" % alignments + lines.append(begin_cmd) + + # Headers + if options["header"]: + lines.append(" & ".join(wanted_fields) + " \\\\") + + # Data + rows = self._get_rows(options) + formatted_rows = self._format_rows(rows) + for row in formatted_rows: + wanted_data = [ + d for f, d in zip(self._field_names, row) if f in wanted_fields + ] + lines.append(" & ".join(wanted_data) + " \\\\") + + lines.append("\\end{tabular}") + + return "\r\n".join(lines) + + def _get_formatted_latex_string(self, options): + lines = [] + + wanted_fields = [] + if options["fields"]: + wanted_fields = [ + field for field in self._field_names if field in options["fields"] + ] + else: + wanted_fields = self._field_names + + wanted_alignments = [self._align[field] for field in wanted_fields] + if options["border"] and options["vrules"] == ALL: + alignment_str = "|".join(wanted_alignments) + elif not options["border"] and options["preserve_internal_border"]: + alignment_str = "|".join(wanted_alignments) + else: + alignment_str = "".join(wanted_alignments) + + if options["border"] and options["vrules"] in [ALL, FRAME]: + alignment_str = "|" + alignment_str + "|" + + begin_cmd = "\\begin{tabular}{%s}" % alignment_str + lines.append(begin_cmd) + if options["border"] and options["hrules"] in [ALL, FRAME]: + lines.append("\\hline") + + # Headers + if options["header"]: + lines.append(" & ".join(wanted_fields) + " \\\\") + if (options["border"] or options["preserve_internal_border"]) and options[ + "hrules" + ] in [ALL, HEADER]: + lines.append("\\hline") + + # Data + rows = self._get_rows(options) + formatted_rows = self._format_rows(rows) + rows = self._get_rows(options) + for row in formatted_rows: + wanted_data = [ + d for f, d in zip(self._field_names, row) if f in wanted_fields + ] + lines.append(" & ".join(wanted_data) + " \\\\") + if options["border"] and options["hrules"] == ALL: + lines.append("\\hline") + + if options["border"] and options["hrules"] == FRAME: + lines.append("\\hline") + + lines.append("\\end{tabular}") + + return "\r\n".join(lines) + + +############################## +# UNICODE WIDTH FUNCTION # +############################## + + +def _str_block_width(val): + return wcwidth.wcswidth(_re.sub("", val)) + + +############################## +# TABLE FACTORIES # +############################## + + +def from_csv(fp, field_names: Any | None = None, **kwargs): + fmtparams = {} + for param in [ + "delimiter", + "doublequote", + "escapechar", + "lineterminator", + "quotechar", + "quoting", + "skipinitialspace", + "strict", + ]: + if param in kwargs: + fmtparams[param] = kwargs.pop(param) + if fmtparams: + reader = csv.reader(fp, **fmtparams) + else: + dialect = csv.Sniffer().sniff(fp.read(1024)) + fp.seek(0) + reader = csv.reader(fp, dialect) + + table = PrettyTable(**kwargs) + if field_names: + table.field_names = field_names + else: + table.field_names = [x.strip() for x in next(reader)] + + for row in reader: + table.add_row([x.strip() for x in row]) + + return table + + +def from_db_cursor(cursor, **kwargs): + if cursor.description: + table = PrettyTable(**kwargs) + table.field_names = [col[0] for col in cursor.description] + for row in cursor.fetchall(): + table.add_row(row) + return table + + +def from_json(json_string, **kwargs): + table = PrettyTable(**kwargs) + objects = json.loads(json_string) + table.field_names = objects[0] + for obj in objects[1:]: + row = [obj[key] for key in table.field_names] + table.add_row(row) + return table + + +class TableHandler(HTMLParser): + def __init__(self, **kwargs) -> None: + HTMLParser.__init__(self) + self.kwargs = kwargs + self.tables: list[list] = [] + self.last_row: list[str] = [] + self.rows: list[Any] = [] + self.max_row_width = 0 + self.active = None + self.last_content = "" + self.is_last_row_header = False + self.colspan = 0 + + def handle_starttag(self, tag, attrs) -> None: + self.active = tag + if tag == "th": + self.is_last_row_header = True + for key, value in attrs: + if key == "colspan": + self.colspan = int(value) + + def handle_endtag(self, tag) -> None: + if tag in ["th", "td"]: + stripped_content = self.last_content.strip() + self.last_row.append(stripped_content) + if self.colspan: + for i in range(1, self.colspan): + self.last_row.append("") + self.colspan = 0 + + if tag == "tr": + self.rows.append((self.last_row, self.is_last_row_header)) + self.max_row_width = max(self.max_row_width, len(self.last_row)) + self.last_row = [] + self.is_last_row_header = False + if tag == "table": + table = self.generate_table(self.rows) + self.tables.append(table) + self.rows = [] + self.last_content = " " + self.active = None + + def handle_data(self, data) -> None: + self.last_content += data + + def generate_table(self, rows): + """ + Generates from a list of rows a PrettyTable object. + """ + table = PrettyTable(**self.kwargs) + for row in self.rows: + if len(row[0]) < self.max_row_width: + appends = self.max_row_width - len(row[0]) + for i in range(1, appends): + row[0].append("-") + + if row[1]: + self.make_fields_unique(row[0]) + table.field_names = row[0] + else: + table.add_row(row[0]) + return table + + def make_fields_unique(self, fields) -> None: + """ + iterates over the row and make each field unique + """ + for i in range(0, len(fields)): + for j in range(i + 1, len(fields)): + if fields[i] == fields[j]: + fields[j] += "'" + + +def from_html(html_code, **kwargs): + """ + Generates a list of PrettyTables from a string of HTML code. Each in + the HTML becomes one PrettyTable object. + """ + + parser = TableHandler(**kwargs) + parser.feed(html_code) + return parser.tables + + +def from_html_one(html_code, **kwargs): + """ + Generates a PrettyTables from a string of HTML code which contains only a + single
+ """ + + tables = from_html(html_code, **kwargs) + try: + assert len(tables) == 1 + except AssertionError: + raise ValueError( + "More than one
in provided HTML code. Use from_html instead." + ) + return tables[0] diff --git a/main.py b/main.py index de91651..83ac969 100644 --- a/main.py +++ b/main.py @@ -18,13 +18,17 @@ def input_handler(first_register: bu.BasicRegister, second_register: bu.BasicReg case "m": match input("Choose method to use (1-4):\n>>> "): case "1": - print(f"Multiplication: {bu.binary_multiplication_method_1(first_register, second_register)}") + result, data_table = bu.binary_multiplication_method_1(first_register, second_register) + print(f"Multiplication:\n{bu.format_device_state_table(data_table)}\nResult: {result.compact_str()}") case "2": - print(f"Multiplication: {bu.binary_multiplication_method_2(first_register, second_register)}") + result, data_table = bu.binary_multiplication_method_2(first_register, second_register) + print(f"Multiplication:\n{bu.format_device_state_table(data_table)}\nResult: {result.compact_str()}") case "3": - print(f"Multiplication: {bu.binary_multiplication_method_3(first_register, second_register)}") + result, data_table = bu.binary_multiplication_method_3(first_register, second_register) + print(f"Multiplication:\n{bu.format_device_state_table(data_table)}\nResult: {result.compact_str()}") case "4": - print(f"Multiplication: {bu.binary_multiplication_method_4(first_register, second_register)}") + result, data_table = bu.binary_multiplication_method_4(first_register, second_register) + print(f"Multiplication:\n{bu.format_device_state_table(data_table)}\nResult: {result.compact_str()}") case _: print("Such method does not exist, try again.") case "d":