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 |