From 3ff00e4a4a402ca770f9b67b0c90443f043fa717 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Dec 2022 18:49:03 -0700 Subject: [PATCH 1/7] [pre-commit.ci] pre-commit autoupdate (#88) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5c6b8e0..c07673c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: name: Running black in all files. - repo: https://github.com/pycqa/isort - rev: 5.11.1 + rev: v5.11.3 hooks: - id: isort args: ["--profile", "black", "--extend-skip", "table2ascii"] From 8d799b5cf9dd7d820502868dd11e7ac78907572a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 Dec 2022 19:08:45 -0700 Subject: [PATCH 2/7] [pre-commit.ci] pre-commit autoupdate (#89) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c07673c..293d837 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: name: Running black in all files. - repo: https://github.com/pycqa/isort - rev: v5.11.3 + rev: 5.11.4 hooks: - id: isort args: ["--profile", "black", "--extend-skip", "table2ascii"] From 12bab03ebe9c67712a13ff56e2758e2ff71d0107 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Wed, 28 Dec 2022 14:11:25 -0700 Subject: [PATCH 3/7] feat: Added decimal alignment option (#90) --- README.md | 50 ++++---- docs/source/usage.rst | 208 +++++++++++++++++----------------- table2ascii/alignment.py | 32 ++++-- table2ascii/table_to_ascii.py | 83 ++++++++++++-- tests/test_alignments.py | 25 +++- 5 files changed, 254 insertions(+), 144 deletions(-) diff --git a/README.md b/README.md index ffc0261..de9e288 100644 --- a/README.md +++ b/README.md @@ -69,22 +69,26 @@ print(output) from table2ascii import table2ascii, Alignment output = table2ascii( - header=["#", "G", "H", "R", "S"], - body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], - first_col_heading=True, - column_widths=[5, 5, 5, 5, 5], - alignments=[Alignment.LEFT] + [Alignment.RIGHT] * 4, + header=["Product", "Category", "Price", "Rating"], + body=[ + ["Milk", "Dairy", "$2.99", "6.283"], + ["Cheese", "Dairy", "$10.99", "8.2"], + ["Apples", "Produce", "$0.99", "10.00"], + ], + column_widths=[12, 12, 12, 12], + alignments=[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT, Alignment.DECIMAL], ) print(output) """ -╔═════╦═══════════════════════╗ -║ # ║ G H R S ║ -╟─────╫───────────────────────╢ -║ 1 ║ 30 40 35 30 ║ -║ 2 ║ 30 40 35 30 ║ -╚═════╩═══════════════════════╝ +╔═══════════════════════════════════════════════════╗ +║ Product Category Price Rating ║ +╟───────────────────────────────────────────────────╢ +║ Milk Dairy $2.99 6.283 ║ +║ Cheese Dairy $10.99 8.2 ║ +║ Apples Produce $0.99 10.00 ║ +╚═══════════════════════════════════════════════════╝ """ ``` @@ -199,18 +203,18 @@ All parameters are optional. At least one of `header`, `body`, and `footer` must Refer to the [documentation](https://table2ascii.readthedocs.io/en/stable/api.html#table2ascii) for more information. -| Option | Type | Default | Description | -| :-----------------: | :----------------------------: | :-------------------: | :-------------------------------------------------------------------------------: | -| `header` | `Sequence[SupportsStr]` | `None` | First table row seperated by header row separator. Values should support `str()` | -| `body` | `Sequence[Sequence[Sequence]]` | `None` | 2D List of rows for the main section of the table. Values should support `str()` | -| `footer` | `Sequence[Sequence]` | `None` | Last table row seperated by header row separator. Values should support `str()` | -| `column_widths` | `Sequence[Optional[int]]` | `None` (automatic) | List of column widths in characters for each column | -| `alignments` | `Sequence[Alignment]` | `None` (all centered) | Column alignments
(ex. `[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT]`) | -| `style` | `TableStyle` | `double_thin_compact` | Table style to use for the table\* | -| `first_col_heading` | `bool` | `False` | Whether to add a heading column separator after the first column | -| `last_col_heading` | `bool` | `False` | Whether to add a heading column separator before the last column | -| `cell_padding` | `int` | `1` | The minimum number of spaces to add between the cell content and the cell border | -| `use_wcwidth` | `bool` | `True` | Whether to use [wcwidth][wcwidth] instead of `len()` to calculate cell width | +| Option | Type | Default | Description | +| :-----------------: | :----------------------------: | :-------------------: | :--------------------------------------------------------------------------------------------------: | +| `header` | `Sequence[SupportsStr]` | `None` | First table row seperated by header row separator. Values should support `str()` | +| `body` | `Sequence[Sequence[Sequence]]` | `None` | 2D List of rows for the main section of the table. Values should support `str()` | +| `footer` | `Sequence[Sequence]` | `None` | Last table row seperated by header row separator. Values should support `str()` | +| `column_widths` | `Sequence[Optional[int]]` | `None` (automatic) | List of column widths in characters for each column | +| `alignments` | `Sequence[Alignment]` | `None` (all centered) | Column alignments
(ex. `[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT, Alignment.DECIMAL]`) | +| `style` | `TableStyle` | `double_thin_compact` | Table style to use for the table\* | +| `first_col_heading` | `bool` | `False` | Whether to add a heading column separator after the first column | +| `last_col_heading` | `bool` | `False` | Whether to add a heading column separator before the last column | +| `cell_padding` | `int` | `1` | The minimum number of spaces to add between the cell content and the cell border | +| `use_wcwidth` | `bool` | `True` | Whether to use [wcwidth][wcwidth] instead of `len()` to calculate cell width | [wcwidth]: https://pypi.org/project/wcwidth/ diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 70f8487..823c23e 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -6,115 +6,119 @@ Convert lists to ASCII tables .. code:: py - from table2ascii import table2ascii - - output = table2ascii( - header=["#", "G", "H", "R", "S"], - body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], - footer=["SUM", "130", "140", "135", "130"], - ) - - print(output) - - """ - ╔═════════════════════════════╗ - ║ # G H R S ║ - ╟─────────────────────────────╢ - ║ 1 30 40 35 30 ║ - ║ 2 30 40 35 30 ║ - ╟─────────────────────────────╢ - ║ SUM 130 140 135 130 ║ - ╚═════════════════════════════╝ - """ + from table2ascii import table2ascii + + output = table2ascii( + header=["#", "G", "H", "R", "S"], + body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], + footer=["SUM", "130", "140", "135", "130"], + ) + + print(output) + + """ + ╔═════════════════════════════╗ + ║ # G H R S ║ + ╟─────────────────────────────╢ + ║ 1 30 40 35 30 ║ + ║ 2 30 40 35 30 ║ + ╟─────────────────────────────╢ + ║ SUM 130 140 135 130 ║ + ╚═════════════════════════════╝ + """ Set first or last column headings ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: py - from table2ascii import table2ascii + from table2ascii import table2ascii - output = table2ascii( - body=[["Assignment", "30", "40", "35", "30"], ["Bonus", "10", "20", "5", "10"]], - first_col_heading=True, - ) + output = table2ascii( + body=[["Assignment", "30", "40", "35", "30"], ["Bonus", "10", "20", "5", "10"]], + first_col_heading=True, + ) - print(output) + print(output) - """ - ╔════════════╦═══════════════════╗ - ║ Assignment ║ 30 40 35 30 ║ - ║ Bonus ║ 10 20 5 10 ║ - ╚════════════╩═══════════════════╝ - """ + """ + ╔════════════╦═══════════════════╗ + ║ Assignment ║ 30 40 35 30 ║ + ║ Bonus ║ 10 20 5 10 ║ + ╚════════════╩═══════════════════╝ + """ Set column widths and alignments ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: py - from table2ascii import table2ascii, Alignment + from table2ascii import table2ascii, Alignment - output = table2ascii( - header=["#", "G", "H", "R", "S"], - body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], - first_col_heading=True, - column_widths=[5, 5, 5, 5, 5], - alignments=[Alignment.LEFT] + [Alignment.RIGHT] * 4, - ) + output = table2ascii( + header=["Product", "Category", "Price", "Rating"], + body=[ + ["Milk", "Dairy", "$2.99", "6.283"], + ["Cheese", "Dairy", "$10.99", "8.2"], + ["Apples", "Produce", "$0.99", "10.00"], + ], + column_widths=[12, 12, 12, 12], + alignments=[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT, Alignment.DECIMAL], + ) - print(output) + print(output) - """ - ╔═════╦═══════════════════════╗ - ║ # ║ G H R S ║ - ╟─────╫───────────────────────╢ - ║ 1 ║ 30 40 35 30 ║ - ║ 2 ║ 30 40 35 30 ║ - ╚═════╩═══════════════════════╝ - """ + """ + ╔═══════════════════════════════════════════════════╗ + ║ Product Category Price Rating ║ + ╟───────────────────────────────────────────────────╢ + ║ Milk Dairy $2.99 6.283 ║ + ║ Cheese Dairy $10.99 8.2 ║ + ║ Apples Produce $0.99 10.00 ║ + ╚═══════════════════════════════════════════════════╝ + """ Use a preset style ~~~~~~~~~~~~~~~~~~ .. code:: py - from table2ascii import table2ascii, Alignment, PresetStyle - - output = table2ascii( - header=["First", "Second", "Third", "Fourth"], - body=[["10", "30", "40", "35"], ["20", "10", "20", "5"]], - column_widths=[10, 10, 10, 10], - style=PresetStyle.ascii_box - ) - - print(output) - - """ - +----------+----------+----------+----------+ - | First | Second | Third | Fourth | - +----------+----------+----------+----------+ - | 10 | 30 | 40 | 35 | - +----------+----------+----------+----------+ - | 20 | 10 | 20 | 5 | - +----------+----------+----------+----------+ - """ - - output = table2ascii( - header=["First", "Second", "Third", "Fourth"], - body=[["10", "30", "40", "35"], ["20", "10", "20", "5"]], - style=PresetStyle.plain, - cell_padding=0, - alignments=[Alignment.LEFT] * 4, - ) - - print(output) - - """ - First Second Third Fourth - 10 30 40 35 - 20 10 20 5 - """ + from table2ascii import table2ascii, Alignment, PresetStyle + + output = table2ascii( + header=["First", "Second", "Third", "Fourth"], + body=[["10", "30", "40", "35"], ["20", "10", "20", "5"]], + column_widths=[10, 10, 10, 10], + style=PresetStyle.ascii_box + ) + + print(output) + + """ + +----------+----------+----------+----------+ + | First | Second | Third | Fourth | + +----------+----------+----------+----------+ + | 10 | 30 | 40 | 35 | + +----------+----------+----------+----------+ + | 20 | 10 | 20 | 5 | + +----------+----------+----------+----------+ + """ + + output = table2ascii( + header=["First", "Second", "Third", "Fourth"], + body=[["10", "30", "40", "35"], ["20", "10", "20", "5"]], + style=PresetStyle.plain, + cell_padding=0, + alignments=[Alignment.LEFT] * 4, + ) + + print(output) + + """ + First Second Third Fourth + 10 30 40 35 + 20 10 20 5 + """ Define a custom style ~~~~~~~~~~~~~~~~~~~~~ @@ -123,27 +127,27 @@ Check :ref:`TableStyle` for more info. .. code:: py - from table2ascii import table2ascii, TableStyle + from table2ascii import table2ascii, TableStyle - my_style = TableStyle.from_string("*-..*||:+-+:+ *''*") + my_style = TableStyle.from_string("*-..*||:+-+:+ *''*") - output = table2ascii( - header=["First", "Second", "Third"], - body=[["10", "30", "40"], ["20", "10", "20"], ["30", "20", "30"]], - style=my_style, - ) + output = table2ascii( + header=["First", "Second", "Third"], + body=[["10", "30", "40"], ["20", "10", "20"], ["30", "20", "30"]], + style=my_style, + ) - print(output) + print(output) - """ - *-------.--------.-------* - | First : Second : Third | - +-------:--------:-------+ - | 10 : 30 : 40 | - | 20 : 10 : 20 | - | 30 : 20 : 30 | - *-------'--------'-------* - """ + """ + *-------.--------.-------* + | First : Second : Third | + +-------:--------:-------+ + | 10 : 30 : 40 | + | 20 : 10 : 20 | + | 30 : 20 : 30 | + *-------'--------'-------* + """ Merge adjacent cells ~~~~~~~~~~~~~~~~~~~~ diff --git a/table2ascii/alignment.py b/table2ascii/alignment.py index 0a8e5f7..2ae487f 100644 --- a/table2ascii/alignment.py +++ b/table2ascii/alignment.py @@ -9,26 +9,40 @@ class Alignment(IntEnum): from table2ascii import Alignment, table2ascii table2ascii( - header=["Product", "Category", "Price", "In Stock"], + header=["Product", "Category", "Price", "Rating"], body=[ - ["Milk", "Dairy", "$2.99", "Yes"], - ["Cheese", "Dairy", "$10.99", "No"], - ["Apples", "Produce", "$0.99", "Yes"], + ["Milk", "Dairy", "$2.99", "6.28318"], + ["Cheese", "Dairy", "$10.99", "8.2"], + ["Apples", "Produce", "$0.99", "10.00"], ], - alignments=[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT, Alignment.LEFT], + alignments=[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT, Alignment.DECIMAL], ) \"\"\" ╔════════════════════════════════════════╗ - ║ Product Category Price In Stock ║ + ║ Product Category Price Rating ║ ╟────────────────────────────────────────╢ - ║ Milk Dairy $2.99 Yes ║ - ║ Cheese Dairy $10.99 No ║ - ║ Apples Produce $0.99 Yes ║ + ║ Milk Dairy $2.99 6.28318 ║ + ║ Cheese Dairy $10.99 8.2 ║ + ║ Apples Produce $0.99 10.00 ║ ╚════════════════════════════════════════╝ \"\"\" + + .. note:: + + If the :attr:`DECIMAL` alignment type is used, any cell values that are + not valid decimal numbers will be aligned to the center. Decimal numbers + include integers, floats, and strings containing only + :meth:`decimal ` characters and at most one decimal point. + + .. versionchanged:: 1.1.0 + + Added :attr:`DECIMAL` alignment -- align decimal numbers such that + the decimal point is aligned with the decimal point of all other numbers + in the same column. """ LEFT = 0 CENTER = 1 RIGHT = 2 + DECIMAL = 3 diff --git a/table2ascii/table_to_ascii.py b/table2ascii/table_to_ascii.py index 8116467..0510bd1 100644 --- a/table2ascii/table_to_ascii.py +++ b/table2ascii/table_to_ascii.py @@ -67,15 +67,20 @@ def __init__( if not header and not body and not footer: raise NoHeaderBodyOrFooterError() - # calculate or use given column widths - self.__column_widths = self.__calculate_column_widths(options.column_widths) - self.__alignments = options.alignments or [Alignment.CENTER] * self.__columns # check if alignments specified have a different number of columns if options.alignments and len(options.alignments) != self.__columns: raise AlignmentCountMismatchError(options.alignments, self.__columns) + # keep track of the number widths and positions of the decimal points for decimal alignment + decimal_widths, decimal_positions = self.__calculate_decimal_widths_and_positions() + self.__decimal_widths: list[int] = decimal_widths + self.__decimal_positions: list[int] = decimal_positions + + # calculate or use given column widths + self.__column_widths = self.__calculate_column_widths(options.column_widths) + # check if the cell padding is valid if self.__cell_padding < 0: raise InvalidCellPaddingError(self.__cell_padding) @@ -125,10 +130,45 @@ def get_column_width(row: Sequence[SupportsStr], column: int) -> int: header_size = get_column_width(self.__header, i) if self.__header else 0 body_size = max(get_column_width(row, i) for row in self.__body) if self.__body else 0 footer_size = get_column_width(self.__footer, i) if self.__footer else 0 + min_text_width = max(header_size, body_size, footer_size, self.__decimal_widths[i]) # get the max and add 2 for padding each side with a space depending on cell padding - column_widths.append(max(header_size, body_size, footer_size) + self.__cell_padding * 2) + column_widths.append(min_text_width + self.__cell_padding * 2) return column_widths + def __calculate_decimal_widths_and_positions(self) -> tuple[list[int], list[int]]: + """Calculate the positions of the decimal points for decimal alignment. + + Returns: + A tuple of the widths of the decimal numbers in each column + and the positions of the decimal points in each column + """ + decimal_widths: list[int] = [0] * self.__columns + decimal_positions: list[int] = [0] * self.__columns + for i in range(self.__columns): + if self.__alignments[i] != Alignment.DECIMAL: + continue + # list all values in the i-th column of header, body, and footer + values = [str(self.__header[i])] if self.__header else [] + values += [str(row[i]) for row in self.__body] if self.__body else [] + values += [str(self.__footer[i])] if self.__footer else [] + # filter out values that are not numbers and split at the decimal point + split_values = [ + self.__split_decimal(value) for value in values if self.__is_number(value) + ] + # skip if there are no decimal values + if len(split_values) == 0: + continue + # get the max number of digits before and after the decimal point + max_before_decimal = max(self.__str_width(parts[0]) for parts in split_values) + max_after_decimal = max(self.__str_width(parts[1]) for parts in split_values) + # add 1 for the decimal point if there are any decimal point values + has_decimal = any(self.__is_number(value) and "." in value for value in values) + # store the total width of the decimal numbers in the column + decimal_widths[i] = max_before_decimal + max_after_decimal + int(has_decimal) + # store the max digits before the decimal point for decimal alignment + decimal_positions[i] = max_before_decimal + return decimal_widths, decimal_positions + def __calculate_column_widths( self, user_column_widths: Sequence[int | None] | None ) -> list[int]: @@ -169,30 +209,42 @@ def __fix_rows_beginning_with_merge(self) -> None: if self.__footer and self.__footer[0] == Merge.LEFT: self.__footer[0] = "" - def __pad(self, cell_value: SupportsStr, width: int, alignment: Alignment) -> str: + def __pad(self, cell_value: SupportsStr, width: int, col_index: int) -> str: """Pad a string of text to a given width with specified alignment Args: cell_value: The text in the cell to pad width: The width in characters to pad to - alignment: The alignment to use + col_index: The index of the column Returns: The padded text """ + alignment = self.__alignments[col_index] text = str(cell_value) + # if using decimal alignment, pad such that the decimal point + # is aligned to the column's decimal position + if alignment == Alignment.DECIMAL and self.__is_number(text): + decimal_position = self.__decimal_positions[col_index] + decimal_max_width = self.__decimal_widths[col_index] + text_before_decimal = self.__split_decimal(text)[0] + before = " " * (decimal_position - self.__str_width(text_before_decimal)) + after = " " * (decimal_max_width - self.__str_width(text) - len(before)) + text = f"{before}{text}{after}" + # add minimum cell padding around the text padding = " " * self.__cell_padding padded_text = f"{padding}{text}{padding}" text_width = self.__str_width(padded_text) + # pad the text based on the alignment if alignment == Alignment.LEFT: # pad with spaces on the end return padded_text + (" " * (width - text_width)) - if alignment == Alignment.CENTER: + elif alignment in (Alignment.CENTER, Alignment.DECIMAL): # pad with spaces, half on each side before = " " * floor((width - text_width) / 2) after = " " * ceil((width - text_width) / 2) return before + padded_text + after - if alignment == Alignment.RIGHT: + elif alignment == Alignment.RIGHT: # pad with spaces at the beginning return (" " * (width - text_width)) + padded_text raise InvalidAlignmentError(alignment) @@ -403,7 +455,7 @@ def __get_padded_cell_line_content( return self.__pad( cell_value=col_content, width=pad_width, - alignment=self.__alignments[col_index], + col_index=col_index, ) def __top_edge_to_ascii(self) -> str: @@ -530,6 +582,19 @@ def __str_width(self, text: str) -> int: # if use_wcwidth is False or wcswidth fails, fall back to len return width if width >= 0 else len(text) + @staticmethod + def __is_number(text: str) -> bool: + """Returns True if the string is a number, with or without a decimal point""" + return text.replace(".", "", 1).isdecimal() + + @staticmethod + def __split_decimal(text: str) -> tuple[str, str]: + """Splits a string into a tuple of the integer and decimal parts""" + if "." in text: + before, after = text.split(".", 1) + return before, after + return text, "" + def to_ascii(self) -> str: """Generates a formatted ASCII table diff --git a/tests/test_alignments.py b/tests/test_alignments.py index 3e8b874..107c32b 100644 --- a/tests/test_alignments.py +++ b/tests/test_alignments.py @@ -1,6 +1,6 @@ import pytest -from table2ascii import Alignment, table2ascii as t2a +from table2ascii import Alignment, PresetStyle, table2ascii as t2a from table2ascii.exceptions import AlignmentCountMismatchError, InvalidAlignmentError @@ -97,3 +97,26 @@ def test_alignments_multiline_data(): "╚═══════════════════════════════════════════╝" ) assert text == expected + + +def test_decimal_alignment(): + text = t2a( + header=["1.1.1", "G", "Long Header", "H", "R", "3.8"], + body=[[100.00001, 2, 3.14, 33, "AB", "1.5"], [10.0001, 22.0, 2.718, 3, "CD", "3.03"]], + footer=[10000.01, "123", 10.0, 0, "E", "A"], + alignments=[Alignment.DECIMAL] * 6, + first_col_heading=True, + style=PresetStyle.double_thin_box, + ) + expected = ( + "╔═════════════╦═══════╤═════════════╤════╤════╤═════════╗\n" + "║ 1.1.1 ║ G │ Long Header │ H │ R │ 3.8 ║\n" + "╠═════════════╬═══════╪═════════════╪════╪════╪═════════╣\n" + "║ 100.00001 ║ 2 │ 3.14 │ 33 │ AB │ 1.5 ║\n" + "╟─────────────╫───────┼─────────────┼────┼────┼─────────╢\n" + "║ 10.0001 ║ 22.0 │ 2.718 │ 3 │ CD │ 3.03 ║\n" + "╠═════════════╬═══════╪═════════════╪════╪════╪═════════╣\n" + "║ 10000.01 ║ 123 │ 10.0 │ 0 │ E │ A ║\n" + "╚═════════════╩═══════╧═════════════╧════╧════╧═════════╝" + ) + assert text == expected From 3e8b7ca53b2f8b6ffd2ecc6fb1c201de4a657eeb Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Wed, 28 Dec 2022 14:47:49 -0700 Subject: [PATCH 4/7] chore: Make version static in pyproject.toml (#87) --- .github/workflows/lint.yml | 2 +- CONTRIBUTING.md | 12 ++++++++++++ pyproject.toml | 3 ++- setup.py | 14 +------------- table2ascii/__init__.py | 9 ++++++++- table2ascii/annotations.py | 5 +---- 6 files changed, 25 insertions(+), 20 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 861e42d..01ee7f7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -94,7 +94,7 @@ jobs: setup.py - name: Install dependencies - run: python -m pip install -e ".[dev]" -e ".[docs]" + run: python -m pip install -e ".[dev,docs]" - name: Run mypy run: mypy . diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 91b025b..d99b2e0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,6 +24,18 @@ Install documentation dependencies with: pip install -e ".[docs]" ``` +Install runtime dependencies with: + +```bash +pip install -e . +``` + +All dependencies can be installed at once with: + +```bash +pip install -e ".[dev,docs]" +``` + ### Running the Tests Run the following command to run the [Tox](https://github.com/tox-dev/tox) test script which will verify that the tested functionality is still working. diff --git a/pyproject.toml b/pyproject.toml index 9db538c..7b8a95a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,8 +5,8 @@ build-backend = "setuptools.build_meta" [project] name = "table2ascii" +version = "1.1.0" authors = [{name = "Jonah Lawrence", email = "jonah@freshidea.com"}] -dynamic = ["version"] description = "Convert 2D Python lists into Unicode/ASCII tables" readme = "README.md" requires-python = ">=3.7" @@ -39,6 +39,7 @@ classifiers = [ ] dependencies = [ "typing-extensions>=3.7.4; python_version<'3.8'", + "importlib-metadata<5,>=1; python_version<'3.8'", "wcwidth<1", ] diff --git a/setup.py b/setup.py index 5fbf35d..d32b777 100644 --- a/setup.py +++ b/setup.py @@ -1,16 +1,4 @@ # /usr/bin/env python -import re - from setuptools import setup - -def version(): - version = "" - with open("table2ascii/__init__.py") as f: - version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE) - if not version: - raise RuntimeError("version is not set") - return version.group(1) - - -setup(name="table2ascii", version=version()) +setup() diff --git a/table2ascii/__init__.py b/table2ascii/__init__.py index 19fa31a..a86dc02 100644 --- a/table2ascii/__init__.py +++ b/table2ascii/__init__.py @@ -1,6 +1,8 @@ """ table2ascii - Library for converting 2D Python lists to fancy ASCII/Unicode tables """ +import sys +from typing import TYPE_CHECKING from .alignment import Alignment from .merge import Merge @@ -8,7 +10,12 @@ from .table_style import TableStyle from .table_to_ascii import table2ascii -__version__ = "1.0.4" +if TYPE_CHECKING or sys.version_info >= (3, 8): + from importlib import metadata +else: + import importlib_metadata as metadata + +__version__ = metadata.version(__name__) __all__ = [ "Alignment", diff --git a/table2ascii/annotations.py b/table2ascii/annotations.py index 241e787..60a4e3d 100644 --- a/table2ascii/annotations.py +++ b/table2ascii/annotations.py @@ -2,14 +2,11 @@ from abc import abstractmethod from typing import TYPE_CHECKING -if sys.version_info >= (3, 8): +if TYPE_CHECKING or sys.version_info >= (3, 8): from typing import Protocol, runtime_checkable else: from typing_extensions import Protocol, runtime_checkable -if TYPE_CHECKING: - from typing import Protocol - @runtime_checkable class SupportsStr(Protocol): From dfbca8c35e66c9b370ffe39991a4af1578229ef5 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Wed, 28 Dec 2022 15:48:47 -0700 Subject: [PATCH 5/7] feat: Ability to align all columns with a single Alignment (#91) --- README.md | 26 +++++++++++++------------- docs/source/usage.rst | 2 +- table2ascii/alignment.py | 25 ++++++++++++++++++++++++- table2ascii/options.py | 2 +- table2ascii/table_to_ascii.py | 30 +++++++++++++++++++++--------- tests/test_alignments.py | 34 ++++++++++++++++++++++++++++++++++ 6 files changed, 94 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index de9e288..ca0e1c6 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ output = table2ascii( body=[["10", "30", "40", "35"], ["20", "10", "20", "5"]], style=PresetStyle.plain, cell_padding=0, - alignments=[Alignment.LEFT] * 4, + alignments=Alignment.LEFT, ) print(output) @@ -203,18 +203,18 @@ All parameters are optional. At least one of `header`, `body`, and `footer` must Refer to the [documentation](https://table2ascii.readthedocs.io/en/stable/api.html#table2ascii) for more information. -| Option | Type | Default | Description | -| :-----------------: | :----------------------------: | :-------------------: | :--------------------------------------------------------------------------------------------------: | -| `header` | `Sequence[SupportsStr]` | `None` | First table row seperated by header row separator. Values should support `str()` | -| `body` | `Sequence[Sequence[Sequence]]` | `None` | 2D List of rows for the main section of the table. Values should support `str()` | -| `footer` | `Sequence[Sequence]` | `None` | Last table row seperated by header row separator. Values should support `str()` | -| `column_widths` | `Sequence[Optional[int]]` | `None` (automatic) | List of column widths in characters for each column | -| `alignments` | `Sequence[Alignment]` | `None` (all centered) | Column alignments
(ex. `[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT, Alignment.DECIMAL]`) | -| `style` | `TableStyle` | `double_thin_compact` | Table style to use for the table\* | -| `first_col_heading` | `bool` | `False` | Whether to add a heading column separator after the first column | -| `last_col_heading` | `bool` | `False` | Whether to add a heading column separator before the last column | -| `cell_padding` | `int` | `1` | The minimum number of spaces to add between the cell content and the cell border | -| `use_wcwidth` | `bool` | `True` | Whether to use [wcwidth][wcwidth] instead of `len()` to calculate cell width | +| Option | Supported Types | Description | +| :-----------------: | :-----------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------: | +| `header` | `Sequence[SupportsStr]`, `None`
(Default: `None`) | First table row seperated by header row separator. Values should support `str()` | +| `body` | `Sequence[Sequence[SupportsStr]]`, `None`
(Default: `None`) | 2D List of rows for the main section of the table. Values should support `str()` | +| `footer` | `Sequence[SupportsStr]`, `None`
(Default: `None`) | Last table row seperated by header row separator. Values should support `str()` | +| `column_widths` | `Sequence[Optional[int]]`, `None`
(Default: `None` / automatic) | List of column widths in characters for each column | +| `alignments` | `Sequence[Alignment]`, `Alignment`, `None`
(Default: `None` / all centered) | Column alignments
(ex. `[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT, Alignment.DECIMAL]`) | +| `style` | `TableStyle`
(Default: `double_thin_compact`) | Table style to use for the table\* | +| `first_col_heading` | `bool`
(Default: `False`) | Whether to add a heading column separator after the first column | +| `last_col_heading` | `bool`
(Default: `False`) | Whether to add a heading column separator before the last column | +| `cell_padding` | `int`
(Default: `1`) | The minimum number of spaces to add between the cell content and the cell border | +| `use_wcwidth` | `bool`
(Default: `True`) | Whether to use [wcwidth][wcwidth] instead of `len()` to calculate cell width | [wcwidth]: https://pypi.org/project/wcwidth/ diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 823c23e..d1eb820 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -109,7 +109,7 @@ Use a preset style body=[["10", "30", "40", "35"], ["20", "10", "20", "5"]], style=PresetStyle.plain, cell_padding=0, - alignments=[Alignment.LEFT] * 4, + alignments=Alignment.LEFT, ) print(output) diff --git a/table2ascii/alignment.py b/table2ascii/alignment.py index 2ae487f..041d167 100644 --- a/table2ascii/alignment.py +++ b/table2ascii/alignment.py @@ -4,7 +4,7 @@ class Alignment(IntEnum): """Enum for text alignment types within a table cell - Example:: + A list of alignment types can be used to align each column individually:: from table2ascii import Alignment, table2ascii @@ -15,6 +15,8 @@ class Alignment(IntEnum): ["Cheese", "Dairy", "$10.99", "8.2"], ["Apples", "Produce", "$0.99", "10.00"], ], + # Align the first column to the left, the second to the center, + # the third to the right, and the fourth to the decimal point alignments=[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT, Alignment.DECIMAL], ) @@ -28,6 +30,27 @@ class Alignment(IntEnum): ╚════════════════════════════════════════╝ \"\"\" + A single alignment type can be used for all columns:: + + table2ascii( + header=["First Name", "Last Name", "Age"], + body=[ + ["John", "Smith", 30], + ["Jane", "Doe", 28], + ], + # Align all columns to the left + alignments=Alignment.LEFT, + ) + + \"\"\" + ╔══════════════════════════════╗ + ║ First Name Last Name Age ║ + ╟──────────────────────────────╢ + ║ John Smith 30 ║ + ║ Jane Doe 28 ║ + ╚══════════════════════════════╝ + \"\"\" + .. note:: If the :attr:`DECIMAL` alignment type is used, any cell values that are diff --git a/table2ascii/options.py b/table2ascii/options.py index 36f6ee0..e88bd44 100644 --- a/table2ascii/options.py +++ b/table2ascii/options.py @@ -19,7 +19,7 @@ class Options: first_col_heading: bool last_col_heading: bool column_widths: Sequence[int | None] | None - alignments: Sequence[Alignment] | None + alignments: Sequence[Alignment] | Alignment | None cell_padding: int style: TableStyle use_wcwidth: bool diff --git a/table2ascii/table_to_ascii.py b/table2ascii/table_to_ascii.py index 0510bd1..46603d7 100644 --- a/table2ascii/table_to_ascii.py +++ b/table2ascii/table_to_ascii.py @@ -67,11 +67,16 @@ def __init__( if not header and not body and not footer: raise NoHeaderBodyOrFooterError() - self.__alignments = options.alignments or [Alignment.CENTER] * self.__columns + alignments = options.alignments if options.alignments is not None else Alignment.CENTER + + # if alignments is a single Alignment, convert it to a list of that Alignment + self.__alignments: list[Alignment] = ( + [alignments] * self.__columns if isinstance(alignments, Alignment) else list(alignments) + ) # check if alignments specified have a different number of columns - if options.alignments and len(options.alignments) != self.__columns: - raise AlignmentCountMismatchError(options.alignments, self.__columns) + if len(self.__alignments) != self.__columns: + raise AlignmentCountMismatchError(self.__alignments, self.__columns) # keep track of the number widths and positions of the decimal points for decimal alignment decimal_widths, decimal_positions = self.__calculate_decimal_widths_and_positions() @@ -634,16 +639,13 @@ def table2ascii( first_col_heading: bool = False, last_col_heading: bool = False, column_widths: Sequence[int | None] | None = None, - alignments: Sequence[Alignment] | None = None, + alignments: Sequence[Alignment] | Alignment | None = None, cell_padding: int = 1, style: TableStyle = PresetStyle.double_thin_compact, use_wcwidth: bool = True, ) -> str: """Convert a 2D Python table to ASCII text - .. versionchanged:: 1.0.0 - Added the ``use_wcwidth`` parameter defaulting to :py:obj:`True`. - Args: header: List of column values in the table's header row. All values should be :class:`str` or support :class:`str` conversion. If not specified, the table will not have a header row. @@ -660,8 +662,10 @@ def table2ascii( is passed instead of a :class:`~collections.abc.Sequence`, all columns will be automatically sized. Defaults to :py:obj:`None`. alignments: List of alignments for each column - (ex. ``[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT]``). If not specified or set to - :py:obj:`None`, all columns will be center-aligned. Defaults to :py:obj:`None`. + (ex. ``[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT, Alignment.DECIMAL]``) + or a single alignment to apply to all columns (ex. ``Alignment.LEFT``). + If not specified or set to :py:obj:`None`, all columns will be center-aligned. + Defaults to :py:obj:`None`. cell_padding: The minimum number of spaces to add between the cell content and the column separator. If set to ``0``, the cell content will be flush against the column separator. Defaults to ``1``. @@ -673,6 +677,14 @@ def table2ascii( zero-width space, etc.), whereas :func:`len` determines the width solely based on the number of characters in the string. Defaults to :py:obj:`True`. + .. versionchanged:: 1.1.0 + + ``alignments`` can now also be specified as a single :class:`Alignment` value to apply to all columns. + + .. versionchanged:: 1.0.0 + + Added the ``use_wcwidth`` parameter defaulting to :py:obj:`True`. + Returns: The generated ASCII table """ diff --git a/tests/test_alignments.py b/tests/test_alignments.py index 107c32b..ff684e1 100644 --- a/tests/test_alignments.py +++ b/tests/test_alignments.py @@ -120,3 +120,37 @@ def test_decimal_alignment(): "╚═════════════╩═══════╧═════════════╧════╧════╧═════════╝" ) assert text == expected + + +def test_single_decimal_alignment(): + text = t2a( + header=["1.1.1", "G", "Long Header"], + body=[[100.00001, 2, 3.14], [10.0001, 22.0, 2.718]], + alignments=Alignment.DECIMAL, + ) + expected = ( + "╔════════════════════════════════╗\n" + "║ 1.1.1 G Long Header ║\n" + "╟────────────────────────────────╢\n" + "║ 100.00001 2 3.14 ║\n" + "║ 10.0001 22.0 2.718 ║\n" + "╚════════════════════════════════╝" + ) + assert text == expected + + +def test_single_left_alignment(): + text = t2a( + header=["1.1.1", "G", "Long Header"], + body=[[100.00001, 2, 3.14], [10.0001, 22.0, 2.718]], + alignments=Alignment.LEFT, + ) + expected = ( + "╔════════════════════════════════╗\n" + "║ 1.1.1 G Long Header ║\n" + "╟────────────────────────────────╢\n" + "║ 100.00001 2 3.14 ║\n" + "║ 10.0001 22.0 2.718 ║\n" + "╚════════════════════════════════╝" + ) + assert text == expected From 1a9d24d1c6551b0ea3760ed629dc7597edbd384f Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Wed, 28 Dec 2022 17:21:09 -0700 Subject: [PATCH 6/7] feat: Support for aligning numbers separately from strings (#92) --- docs/source/_static/css/custom.css | 5 ++ table2ascii/alignment.py | 20 ++++--- table2ascii/options.py | 5 ++ table2ascii/table_to_ascii.py | 88 +++++++++++++++++++++--------- tests/test_alignments.py | 38 ++++++++++++- 5 files changed, 120 insertions(+), 36 deletions(-) diff --git a/docs/source/_static/css/custom.css b/docs/source/_static/css/custom.css index c0c2c34..a3475f8 100644 --- a/docs/source/_static/css/custom.css +++ b/docs/source/_static/css/custom.css @@ -18,4 +18,9 @@ /* Change code block font */ :root { --pst-font-family-monospace: "Hack", "Source Code Pro", "SFMono-Regular", "Menlo", "Monaco", "Consolas", "Liberation Mono", "Courier New", "Courier", monospace; +} + +/* Adjust margin on version directives within parameter lists */ +div.versionchanged p, div.versionadded p { + margin-bottom: 10px; } \ No newline at end of file diff --git a/table2ascii/alignment.py b/table2ascii/alignment.py index 041d167..f11fa39 100644 --- a/table2ascii/alignment.py +++ b/table2ascii/alignment.py @@ -30,7 +30,7 @@ class Alignment(IntEnum): ╚════════════════════════════════════════╝ \"\"\" - A single alignment type can be used for all columns:: + A single alignment type can be used to align all columns:: table2ascii( header=["First Name", "Last Name", "Age"], @@ -38,25 +38,27 @@ class Alignment(IntEnum): ["John", "Smith", 30], ["Jane", "Doe", 28], ], - # Align all columns to the left - alignments=Alignment.LEFT, + alignments=Alignment.LEFT, # Align all columns to the left + number_alignments=Alignment.RIGHT, # Align all numeric values to the right ) \"\"\" ╔══════════════════════════════╗ ║ First Name Last Name Age ║ ╟──────────────────────────────╢ - ║ John Smith 30 ║ - ║ Jane Doe 28 ║ + ║ John Smith 30 ║ + ║ Jane Doe 28 ║ ╚══════════════════════════════╝ \"\"\" .. note:: - If the :attr:`DECIMAL` alignment type is used, any cell values that are - not valid decimal numbers will be aligned to the center. Decimal numbers - include integers, floats, and strings containing only - :meth:`decimal ` characters and at most one decimal point. + If :attr:`DECIMAL` is used in the ``number_alignments`` argument to :func:`table2ascii`, + all non-numeric values will be aligned according to the ``alignments`` argument. + If the :attr:`DECIMAL` alignment type is used in the ``alignments`` argument, + all non-numeric values will be aligned to the center. + Numeric values include integers, floats, and strings containing only :meth:`decimal ` + characters and at most one decimal point. .. versionchanged:: 1.1.0 diff --git a/table2ascii/options.py b/table2ascii/options.py index e88bd44..28779c2 100644 --- a/table2ascii/options.py +++ b/table2ascii/options.py @@ -11,6 +11,10 @@ class Options: """Class for storing options that the user sets + .. versionchanged:: 1.1.0 + + Added ``number_alignments`` option + .. versionchanged:: 1.0.0 Added ``use_wcwidth`` option @@ -20,6 +24,7 @@ class Options: last_col_heading: bool column_widths: Sequence[int | None] | None alignments: Sequence[Alignment] | Alignment | None + number_alignments: Sequence[Alignment] | Alignment | None cell_padding: int style: TableStyle use_wcwidth: bool diff --git a/table2ascii/table_to_ascii.py b/table2ascii/table_to_ascii.py index 46603d7..c4359a8 100644 --- a/table2ascii/table_to_ascii.py +++ b/table2ascii/table_to_ascii.py @@ -67,16 +67,12 @@ def __init__( if not header and not body and not footer: raise NoHeaderBodyOrFooterError() - alignments = options.alignments if options.alignments is not None else Alignment.CENTER - - # if alignments is a single Alignment, convert it to a list of that Alignment - self.__alignments: list[Alignment] = ( - [alignments] * self.__columns if isinstance(alignments, Alignment) else list(alignments) + self.__alignments = self.__determine_alignments( + options.alignments, default=Alignment.CENTER + ) + self.__number_alignments = self.__determine_alignments( + options.number_alignments, default=self.__alignments ) - - # check if alignments specified have a different number of columns - if len(self.__alignments) != self.__columns: - raise AlignmentCountMismatchError(self.__alignments, self.__columns) # keep track of the number widths and positions of the decimal points for decimal alignment decimal_widths, decimal_positions = self.__calculate_decimal_widths_and_positions() @@ -107,6 +103,33 @@ def __count_columns(self) -> int: return len(self.__body[0]) return 0 + def __determine_alignments( + self, + user_alignments: Sequence[Alignment] | Alignment | None, + *, + default: Sequence[Alignment] | Alignment, + ) -> list[Alignment]: + """Determine the alignments for each column based on the user provided alignments option. + + Args: + user_alignments: The alignments specified by the user + default: The default alignments to use if user_alignments is None + + Returns: + The alignments for each column in the table + """ + alignments = user_alignments if user_alignments is not None else default + + # if alignments is a single Alignment, convert it to a list of that Alignment + if isinstance(alignments, Alignment): + alignments = [alignments] * self.__columns + + # check if alignments specified have a different number of columns + if len(alignments) != self.__columns: + raise AlignmentCountMismatchError(alignments, self.__columns) + + return list(alignments) + def __auto_column_widths(self) -> list[int]: """Get the minimum number of characters needed for the values in each column in the table with 1 space of padding on each side. @@ -150,7 +173,8 @@ def __calculate_decimal_widths_and_positions(self) -> tuple[list[int], list[int] decimal_widths: list[int] = [0] * self.__columns decimal_positions: list[int] = [0] * self.__columns for i in range(self.__columns): - if self.__alignments[i] != Alignment.DECIMAL: + # skip if the column is not decimal aligned + if self.__number_alignments[i] != Alignment.DECIMAL: continue # list all values in the i-th column of header, body, and footer values = [str(self.__header[i])] if self.__header else [] @@ -227,15 +251,20 @@ def __pad(self, cell_value: SupportsStr, width: int, col_index: int) -> str: """ alignment = self.__alignments[col_index] text = str(cell_value) - # if using decimal alignment, pad such that the decimal point - # is aligned to the column's decimal position - if alignment == Alignment.DECIMAL and self.__is_number(text): - decimal_position = self.__decimal_positions[col_index] - decimal_max_width = self.__decimal_widths[col_index] - text_before_decimal = self.__split_decimal(text)[0] - before = " " * (decimal_position - self.__str_width(text_before_decimal)) - after = " " * (decimal_max_width - self.__str_width(text) - len(before)) - text = f"{before}{text}{after}" + # set alignment for numeric values + if self.__is_number(text): + # if the number alignment is decimal, pad such that the decimal point + # is aligned to the column's decimal position and use the default alignment + if self.__number_alignments[col_index] == Alignment.DECIMAL: + decimal_position = self.__decimal_positions[col_index] + decimal_max_width = self.__decimal_widths[col_index] + text_before_decimal = self.__split_decimal(text)[0] + before = " " * (decimal_position - self.__str_width(text_before_decimal)) + after = " " * (decimal_max_width - self.__str_width(text) - len(before)) + text = f"{before}{text}{after}" + # otherwise use the number alignment as the alignment for the cell + else: + alignment = self.__number_alignments[col_index] # add minimum cell padding around the text padding = " " * self.__cell_padding padded_text = f"{padding}{text}{padding}" @@ -640,6 +669,7 @@ def table2ascii( last_col_heading: bool = False, column_widths: Sequence[int | None] | None = None, alignments: Sequence[Alignment] | Alignment | None = None, + number_alignments: Sequence[Alignment] | Alignment | None = None, cell_padding: int = 1, style: TableStyle = PresetStyle.double_thin_compact, use_wcwidth: bool = True, @@ -666,6 +696,17 @@ def table2ascii( or a single alignment to apply to all columns (ex. ``Alignment.LEFT``). If not specified or set to :py:obj:`None`, all columns will be center-aligned. Defaults to :py:obj:`None`. + + .. versionchanged:: 1.1.0 + ``alignments`` can now also be specified as a single :class:`Alignment` value to apply to all columns. + number_alignments: List of alignments for numeric values in each column or a single alignment + to apply to all columns. This argument can be used to override the alignment of numbers and + is ignored for non-numeric values. Numeric values include integers, floats, and strings containing only + :meth:`decimal ` characters and at most one decimal point. + If not specified or set to :py:obj:`None`, numbers will be aligned based on the ``alignments`` argument. + Defaults to :py:obj:`None`. + + .. versionadded:: 1.1.0 cell_padding: The minimum number of spaces to add between the cell content and the column separator. If set to ``0``, the cell content will be flush against the column separator. Defaults to ``1``. @@ -677,13 +718,7 @@ def table2ascii( zero-width space, etc.), whereas :func:`len` determines the width solely based on the number of characters in the string. Defaults to :py:obj:`True`. - .. versionchanged:: 1.1.0 - - ``alignments`` can now also be specified as a single :class:`Alignment` value to apply to all columns. - - .. versionchanged:: 1.0.0 - - Added the ``use_wcwidth`` parameter defaulting to :py:obj:`True`. + .. versionadded:: 1.0.0 Returns: The generated ASCII table @@ -697,6 +732,7 @@ def table2ascii( last_col_heading=last_col_heading, column_widths=column_widths, alignments=alignments, + number_alignments=number_alignments, cell_padding=cell_padding, style=style, use_wcwidth=use_wcwidth, diff --git a/tests/test_alignments.py b/tests/test_alignments.py index ff684e1..67aa5d3 100644 --- a/tests/test_alignments.py +++ b/tests/test_alignments.py @@ -25,7 +25,7 @@ def test_first_left_four_right(): assert text == expected -def test_wrong_number_alignments(): +def test_wrong_number_of_alignments(): with pytest.raises(AlignmentCountMismatchError): t2a( header=["#", "G", "H", "R", "S"], @@ -154,3 +154,39 @@ def test_single_left_alignment(): "╚════════════════════════════════╝" ) assert text == expected + + +def test_number_alignments(): + text = t2a( + header=["1.1.1", "G", "Long Header", "Another Long Header"], + body=[[100.00001, 2, 3.14, 6.28], [10.0001, 22.0, 2.718, 1.618]], + alignments=[Alignment.LEFT, Alignment.RIGHT, Alignment.CENTER, Alignment.RIGHT], + number_alignments=[Alignment.DECIMAL, Alignment.LEFT, Alignment.RIGHT, Alignment.DECIMAL], + ) + expected = ( + "╔══════════════════════════════════════════════════════╗\n" + "║ 1.1.1 G Long Header Another Long Header ║\n" + "╟──────────────────────────────────────────────────────╢\n" + "║ 100.00001 2 3.14 6.28 ║\n" + "║ 10.0001 22.0 2.718 1.618 ║\n" + "╚══════════════════════════════════════════════════════╝" + ) + assert text == expected + + +def test_single_number_alignments(): + text = t2a( + header=["1.1.1", "G", "Long Header", "S"], + body=[[100.00001, 2, 3.14, 6.28], [10.0001, 22.0, 2.718, 1.618]], + alignments=[Alignment.LEFT, Alignment.CENTER, Alignment.CENTER, Alignment.RIGHT], + number_alignments=Alignment.RIGHT, + ) + expected = ( + "╔════════════════════════════════════════╗\n" + "║ 1.1.1 G Long Header S ║\n" + "╟────────────────────────────────────────╢\n" + "║ 100.00001 2 3.14 6.28 ║\n" + "║ 10.0001 22.0 2.718 1.618 ║\n" + "╚════════════════════════════════════════╝" + ) + assert text == expected From c07698951e2c81050af5aa4eb44d05b5a0500ed8 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Wed, 28 Dec 2022 17:32:12 -0700 Subject: [PATCH 7/7] docs(readme): Add numeric_aligments to readme (#93) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ca0e1c6..b9ff0a7 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,7 @@ Refer to the [documentation](https://table2ascii.readthedocs.io/en/stable/api.ht | `footer` | `Sequence[SupportsStr]`, `None`
(Default: `None`) | Last table row seperated by header row separator. Values should support `str()` | | `column_widths` | `Sequence[Optional[int]]`, `None`
(Default: `None` / automatic) | List of column widths in characters for each column | | `alignments` | `Sequence[Alignment]`, `Alignment`, `None`
(Default: `None` / all centered) | Column alignments
(ex. `[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT, Alignment.DECIMAL]`) | +| `number_alignments` | `Sequence[Alignment]`, `Alignment`, `None`
(Default: `None`) | Column alignments for numeric values. `alignments` will be used if not specified. | | `style` | `TableStyle`
(Default: `double_thin_compact`) | Table style to use for the table\* | | `first_col_heading` | `bool`
(Default: `False`) | Whether to add a heading column separator after the first column | | `last_col_heading` | `bool`
(Default: `False`) | Whether to add a heading column separator before the last column | pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy