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 01/27] [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 02/27] [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 03/27] 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 04/27] 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 05/27] 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 06/27] 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 07/27] 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 | From 68982e976f74345eff52a9b353241860bd87a983 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Thu, 29 Dec 2022 00:29:09 -0700 Subject: [PATCH 08/27] docs: Fix unlinked references and updated docstrings (#94) --- docs/source/api.rst | 33 +++++++++++++++++----------- table2ascii/__init__.py | 30 +++++++++++++++++++++++++ table2ascii/annotations.py | 3 ++- table2ascii/exceptions.py | 41 ++++++++++++++++++++--------------- table2ascii/table_style.py | 2 +- table2ascii/table_to_ascii.py | 16 +++++++++----- 6 files changed, 86 insertions(+), 39 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index 170e480..6cb1a29 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -39,31 +39,38 @@ TableStyle Exceptions ~~~~~~~~~~ -.. autoexception:: table2ascii.exceptions.Table2AsciiError +.. autoexception:: Table2AsciiError -.. autoexception:: table2ascii.exceptions.TableOptionError +.. autoexception:: TableOptionError -.. autoexception:: table2ascii.exceptions.ColumnCountMismatchError +.. autoexception:: ColumnCountMismatchError -.. autoexception:: table2ascii.exceptions.FooterColumnCountMismatchError +.. autoexception:: FooterColumnCountMismatchError -.. autoexception:: table2ascii.exceptions.BodyColumnCountMismatchError +.. autoexception:: BodyColumnCountMismatchError -.. autoexception:: table2ascii.exceptions.AlignmentCountMismatchError +.. autoexception:: AlignmentCountMismatchError -.. autoexception:: table2ascii.exceptions.InvalidCellPaddingError +.. autoexception:: InvalidCellPaddingError -.. autoexception:: table2ascii.exceptions.ColumnWidthsCountMismatchError +.. autoexception:: ColumnWidthsCountMismatchError -.. autoexception:: table2ascii.exceptions.ColumnWidthTooSmallError +.. autoexception:: ColumnWidthTooSmallError -.. autoexception:: table2ascii.exceptions.InvalidColumnWidthError +.. autoexception:: InvalidColumnWidthError -.. autoexception:: table2ascii.exceptions.InvalidAlignmentError +.. autoexception:: InvalidAlignmentError -.. autoexception:: table2ascii.exceptions.TableStyleTooLongError +.. autoexception:: TableStyleTooLongError Warnings ~~~~~~~~ -.. autoclass:: table2ascii.exceptions.TableStyleTooShortWarning +.. autoclass:: TableStyleTooShortWarning + +Annotations +~~~~~~~~~~~ + +.. autoclass:: SupportsStr + + .. automethod:: SupportsStr.__str__ diff --git a/table2ascii/__init__.py b/table2ascii/__init__.py index a86dc02..bc53a37 100644 --- a/table2ascii/__init__.py +++ b/table2ascii/__init__.py @@ -5,6 +5,22 @@ from typing import TYPE_CHECKING from .alignment import Alignment +from .annotations import SupportsStr +from .exceptions import ( + AlignmentCountMismatchError, + BodyColumnCountMismatchError, + ColumnCountMismatchError, + ColumnWidthsCountMismatchError, + ColumnWidthTooSmallError, + FooterColumnCountMismatchError, + InvalidAlignmentError, + InvalidCellPaddingError, + InvalidColumnWidthError, + Table2AsciiError, + TableOptionError, + TableStyleTooLongError, + TableStyleTooShortWarning, +) from .merge import Merge from .preset_style import PresetStyle from .table_style import TableStyle @@ -23,4 +39,18 @@ "PresetStyle", "TableStyle", "table2ascii", + "AlignmentCountMismatchError", + "BodyColumnCountMismatchError", + "ColumnCountMismatchError", + "ColumnWidthsCountMismatchError", + "ColumnWidthTooSmallError", + "FooterColumnCountMismatchError", + "InvalidAlignmentError", + "InvalidCellPaddingError", + "InvalidColumnWidthError", + "Table2AsciiError", + "TableOptionError", + "TableStyleTooLongError", + "TableStyleTooShortWarning", + "SupportsStr", ] diff --git a/table2ascii/annotations.py b/table2ascii/annotations.py index 60a4e3d..4434ced 100644 --- a/table2ascii/annotations.py +++ b/table2ascii/annotations.py @@ -10,8 +10,9 @@ @runtime_checkable class SupportsStr(Protocol): - """An ABC with one abstract method __str__.""" + """An abstract base class (ABC) with one abstract method :meth:`__str__`""" @abstractmethod def __str__(self) -> str: + """Return a string representation of the object""" pass diff --git a/table2ascii/exceptions.py b/table2ascii/exceptions.py index 0c57134..b4b6314 100644 --- a/table2ascii/exceptions.py +++ b/table2ascii/exceptions.py @@ -40,8 +40,9 @@ class FooterColumnCountMismatchError(ColumnCountMismatchError): This class is a subclass of :class:`ColumnCountMismatchError`. Attributes: - footer (Sequence[SupportsStr]): The footer that caused the error - expected_columns (int): The number of columns that were expected + footer (:class:`Sequence `\ [:class:`SupportsStr`]): + The footer that caused the error + expected_columns (:class:`int`): The number of columns that were expected """ def __init__(self, footer: Sequence[SupportsStr], expected_columns: int): @@ -63,9 +64,11 @@ class BodyColumnCountMismatchError(ColumnCountMismatchError): This class is a subclass of :class:`ColumnCountMismatchError`. Attributes: - body (Sequence[Sequence[SupportsStr]]): The body that caused the error - expected_columns (int): The number of columns that were expected - first_invalid_row (Sequence[SupportsStr]): The first row with an invalid column count + body (:class:`Sequence `\ [\ :class:`Sequence `\ [:class:`SupportsStr`]]): + The body that caused the error + expected_columns (:class:`int`): The number of columns that were expected + first_invalid_row (:class:`Sequence `\ [:class:`SupportsStr`]): + The first row with an invalid column count """ def __init__(self, body: Sequence[Sequence[SupportsStr]], expected_columns: int): @@ -90,8 +93,9 @@ class AlignmentCountMismatchError(ColumnCountMismatchError): This class is a subclass of :class:`ColumnCountMismatchError`. Attributes: - alignments (Sequence[Alignment]): The alignments that caused the error - expected_columns (int): The number of columns that were expected + alignments (:class:`Sequence `\ [:class:`Alignment`]): + The alignments that caused the error + expected_columns (:class:`int`): The number of columns that were expected """ def __init__(self, alignments: Sequence[Alignment], expected_columns: int): @@ -113,8 +117,9 @@ class ColumnWidthsCountMismatchError(ColumnCountMismatchError): This class is a subclass of :class:`ColumnCountMismatchError`. Attributes: - column_widths (Sequence[Optional[int]]): The column widths that caused the error - expected_columns (int): The number of columns that were expected + column_widths (:class:`Sequence `\ [:data:`Optional `\ [:class:`int`]]): + The column widths that caused the error + expected_columns (:class:`int`): The number of columns that were expected """ def __init__(self, column_widths: Sequence[int | None], expected_columns: int): @@ -148,7 +153,7 @@ class InvalidCellPaddingError(TableOptionError): This class is a subclass of :class:`TableOptionError`. Attributes: - padding (int): The padding that caused the error + padding (:class:`int`): The padding that caused the error """ def __init__(self, padding: int): @@ -169,9 +174,9 @@ class ColumnWidthTooSmallError(TableOptionError): This class is a subclass of :class:`TableOptionError`. Attributes: - column_index (int): The index of the column that caused the error - column_width (int): The column width that caused the error - min_width (int): The minimum width that is allowed + column_index (:class:`int`): The index of the column that caused the error + column_width (:class:`int`): The column width that caused the error + min_width (:class:`int`): The minimum width that is allowed """ def __init__(self, column_index: int, column_width: int, min_width: int | None = None): @@ -208,7 +213,7 @@ class InvalidAlignmentError(TableOptionError): This class is a subclass of :class:`TableOptionError`. Attributes: - alignment (Any): The alignment value that caused the error + alignment (:data:`Any `): The alignment value that caused the error """ def __init__(self, alignment: Any): @@ -230,8 +235,8 @@ class TableStyleTooLongError(Table2AsciiError, ValueError): This class is a subclass of :class:`Table2AsciiError` and :class:`ValueError`. Attributes: - string (str): The string that caused the error - max_characters (int): The maximum number of characters that are allowed + string (:class:`str`): The string that caused the error + max_characters (:class:`int`): The maximum number of characters that are allowed """ def __init__(self, string: str, max_characters: int): @@ -256,8 +261,8 @@ class TableStyleTooShortWarning(UserWarning): It can be silenced using :func:`warnings.filterwarnings`. Attributes: - string (str): The string that caused the warning - max_characters (int): The number of characters that :class:`TableStyle` accepts + string (:class:`str`): The string that caused the warning + max_characters (:class:`int`): The number of characters that :class:`TableStyle` accepts """ def __init__(self, string: str, max_characters: int): diff --git a/table2ascii/table_style.py b/table2ascii/table_style.py index 8ecc12e..23db0ac 100644 --- a/table2ascii/table_style.py +++ b/table2ascii/table_style.py @@ -145,7 +145,7 @@ def set(self, **kwargs: str) -> "TableStyle": Example:: - TableStyle().set(top_left_corner="╔", top_and_bottom_edge="═") + TableStyle.from_string("~" * 30).set(left_and_right_edge="", col_sep="") """ for key, value in kwargs.items(): setattr(self, key, value) diff --git a/table2ascii/table_to_ascii.py b/table2ascii/table_to_ascii.py index c4359a8..dd092e2 100644 --- a/table2ascii/table_to_ascii.py +++ b/table2ascii/table_to_ascii.py @@ -677,23 +677,27 @@ def table2ascii( """Convert a 2D Python table to ASCII text Args: - header: List of column values in the table's header row. All values should be :class:`str` + header (:data:`Optional `\ [:class:`Sequence `\ [:class:`SupportsStr`]]): + 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. - body: 2-dimensional list of values in the table's body. All values should be :class:`str` + body (:data:`Optional `\ [:class:`Sequence `\ [:class:`Sequence `\ [:class:`SupportsStr`]]]): + 2-dimensional list of values in the table's body. All values should be :class:`str` or support :class:`str` conversion. If not specified, the table will not have a body. - footer: List of column values in the table's footer row. All values should be :class:`str` + footer (:data:`Optional `\ [:class:`Sequence `\ [:class:`SupportsStr`]]): + List of column values in the table's footer row. All values should be :class:`str` or support :class:`str` conversion. If not specified, the table will not have a footer row. first_col_heading: Whether to add a header column separator after the first column. Defaults to :py:obj:`False`. last_col_heading: Whether to add a header column separator before the last column. Defaults to :py:obj:`False`. - column_widths: List of widths in characters for each column. Any value of :py:obj:`None` + column_widths (:data:`Optional `\ [:class:`Sequence `\ [:data:`Optional `\ [:class:`int`]]]): + List of widths in characters for each column. Any value of :py:obj:`None` indicates that the column width should be determined automatically. If :py:obj:`None` 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, Alignment.DECIMAL]``) - or a single alignment to apply to all columns (ex. ``Alignment.LEFT``). + (ex. [:attr:`Alignment.LEFT`, :attr:`Alignment.CENTER`, :attr:`Alignment.RIGHT`, :attr:`Alignment.DECIMAL`]) + or a single alignment to apply to all columns (ex. :attr:`Alignment.LEFT`). If not specified or set to :py:obj:`None`, all columns will be center-aligned. Defaults to :py:obj:`None`. From 5a0c5070b36d993a3fae2b45ce95efb755ea592c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Jan 2023 12:54:47 -0700 Subject: [PATCH 09/27] chore(deps-dev): update pre-commit requirement from <3,>=2.0.0 to >=2.0.0,<4 (#96) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7b8a95a..092d0ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ docs = [ ] dev = [ "mypy>=0.982,<1", - "pre-commit>=2.0.0,<3", + "pre-commit>=2.0.0,<4", "pyright>=1.0.0,<2", "pytest>=6.0.0,<8", "slotscheck>=0.1.0,<1", From d462f0ff72866220a146faf316ec92b8f52f3d54 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 Jan 2023 20:49:54 -0700 Subject: [PATCH 10/27] [pre-commit.ci] pre-commit autoupdate (#97) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jonah Lawrence --- .github/workflows/lint.yml | 4 ++-- .pre-commit-config.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 01ee7f7..97c81e4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,10 +16,10 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - - name: Set up python 3.7 + - name: Set up python 3.8 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.8 cache: pip cache-dependency-path: | setup.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 293d837..edf8e31 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.4 + rev: 5.12.0 hooks: - id: isort args: ["--profile", "black", "--extend-skip", "table2ascii"] From e21ecde0d1d59bd0263b39f171efe1faa4d8a166 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Feb 2023 12:29:01 -0700 Subject: [PATCH 11/27] chore(deps-dev): update mypy requirement from <1,>=0.982 to >=0.982,<2 (#99) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 092d0ba..62c014f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ docs = [ "sphinx-book-theme==0.3.3", ] dev = [ - "mypy>=0.982,<1", + "mypy>=0.982,<2", "pre-commit>=2.0.0,<4", "pyright>=1.0.0,<2", "pytest>=6.0.0,<8", From 14eb6dc43a923085de5156890278ba6df076b778 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Feb 2023 12:29:51 -0700 Subject: [PATCH 12/27] [pre-commit.ci] pre-commit autoupdate (#98) 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 edf8e31..3828cce 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ ci: repos: - repo: https://github.com/psf/black - rev: 22.12.0 + rev: 23.1.0 hooks: - id: black name: Running black in all files. From 9adec7a24e6c0ea824c4e0b96d92ca75c33e4ae7 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Tue, 28 Feb 2023 14:05:13 +0200 Subject: [PATCH 13/27] fix: Include py.typed in package data for mypy (#100) --- pyproject.toml | 2 +- setup.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 62c014f..fd814c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "table2ascii" -version = "1.1.0" +version = "1.1.1" authors = [{name = "Jonah Lawrence", email = "jonah@freshidea.com"}] description = "Convert 2D Python lists into Unicode/ASCII tables" readme = "README.md" diff --git a/setup.py b/setup.py index d32b777..b21e3f6 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,7 @@ # /usr/bin/env python from setuptools import setup -setup() +setup( + packages=["table2ascii"], + package_data={"table2ascii": ["py.typed"]}, +) From 11f6146e24fccbc2c836390624733e29078408c5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 4 Apr 2023 09:33:41 -0600 Subject: [PATCH 14/27] [pre-commit.ci] pre-commit autoupdate (#103) 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 3828cce..0a587b2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ ci: repos: - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 23.3.0 hooks: - id: black name: Running black in all files. From 92b90b4af2a95162dc15d10dd2dd7310bc643900 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 11 Jul 2023 23:53:07 -0600 Subject: [PATCH 15/27] [pre-commit.ci] pre-commit autoupdate (#107) 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 0a587b2..9cea686 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ ci: repos: - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.7.0 hooks: - id: black name: Running black in all files. From c4ef7192f2a2e3f604c25ba1897b0551267c92b9 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Tue, 29 Aug 2023 06:47:50 -0600 Subject: [PATCH 16/27] fix: Add name and version to setup.py for older version of pip (#109) --- pyproject.toml | 2 +- setup.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fd814c1..e7343bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "table2ascii" -version = "1.1.1" +version = "1.1.2" authors = [{name = "Jonah Lawrence", email = "jonah@freshidea.com"}] description = "Convert 2D Python lists into Unicode/ASCII tables" readme = "README.md" diff --git a/setup.py b/setup.py index b21e3f6..9467adb 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,51 @@ # /usr/bin/env python +import re + from setuptools import setup -setup( - packages=["table2ascii"], - package_data={"table2ascii": ["py.typed"]}, -) + +def get_name(): + name = "" + with open("pyproject.toml") as f: + name = re.search(r'^name = ["\']([^"\']*)["\']', f.read(), re.M) + if not name: + raise RuntimeError("name is not set") + return name.group(1) + + +def get_version(): + version = "" + with open("pyproject.toml") as f: + version = re.search(r'^version = ["\']([^"\']*)["\']', f.read(), re.M) + if not version: + raise RuntimeError("version is not set") + return version.group(1) + + +def get_dependencies(): + with open("pyproject.toml") as f: + dependency_match = re.search(r"^dependencies = \[([\s\S]*?)\]", f.read(), re.M) + if not dependency_match or not dependency_match.group(1): + return [] + return [ + dependency.strip().strip(",").strip('"') + for dependency in dependency_match.group(1).split("\n") + if dependency + ] + + +try: + # check if pyproject.toml can be used to install dependencies and set the version + setup( + packages=[get_name()], + package_data={get_name(): ["py.typed"]}, + ) +except Exception: + # fallback for old versions of pip/setuptools + setup( + name=get_name(), + packages=[get_name()], + package_data={get_name(): ["py.typed"]}, + version=get_version(), + install_requires=get_dependencies(), + ) From 278785209f64ee585687d3553ffe9ba445711479 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 12 Sep 2023 13:54:37 -0600 Subject: [PATCH 17/27] [pre-commit.ci] pre-commit autoupdate (#110) 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 9cea686..b669533 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ ci: repos: - repo: https://github.com/psf/black - rev: 23.7.0 + rev: 23.9.1 hooks: - id: black name: Running black in all files. From c6735c13799cbab3edabd7915f8313221476b681 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 16:36:05 -0600 Subject: [PATCH 18/27] [pre-commit.ci] pre-commit autoupdate (#111) 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 b669533..66571a9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: name: Running isort in all files. - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-ast name: Check if python files are valid syntax for the ast parser From 32a7b6f2cf985bfcb7c774fa3c12f479b4a08016 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Fri, 20 Oct 2023 10:37:09 -0600 Subject: [PATCH 19/27] fix: Prevent text wrapping when already within width (#113) --- table2ascii/table_to_ascii.py | 19 ++++++++++++------- tests/test_convert.py | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/table2ascii/table_to_ascii.py b/table2ascii/table_to_ascii.py index dd092e2..65dfb8d 100644 --- a/table2ascii/table_to_ascii.py +++ b/table2ascii/table_to_ascii.py @@ -130,6 +130,11 @@ def __determine_alignments( return list(alignments) + def __widest_line(self, value: SupportsStr) -> int: + """Returns the width of the longest line in a multi-line string""" + text = str(value) + return max(self.__str_width(line) for line in text.splitlines()) if len(text) else 0 + 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. @@ -138,18 +143,13 @@ def __auto_column_widths(self) -> list[int]: The minimum number of characters needed for each column """ - def widest_line(value: SupportsStr) -> int: - """Returns the width of the longest line in a multi-line string""" - text = str(value) - return max(self.__str_width(line) for line in text.splitlines()) if len(text) else 0 - def get_column_width(row: Sequence[SupportsStr], column: int) -> int: """Get the width of a cell in a column""" value = row[column] next_value = row[column + 1] if column < self.__columns - 1 else None if value is Merge.LEFT or next_value is Merge.LEFT: return 0 - return widest_line(value) + return self.__widest_line(value) column_widths = [] # get the width necessary for each column @@ -306,7 +306,12 @@ def __wrap_long_lines_in_merged_cells( if row[other_col_index] is not Merge.LEFT: break merged_width += self.__column_widths[other_col_index] + len(column_separator) - cell = textwrap.fill(str(cell), merged_width - self.__cell_padding * 2) + cell = str(cell) + # if the text is too wide, wrap it + inner_cell_width = merged_width - self.__cell_padding * 2 + if self.__widest_line(cell) > inner_cell_width: + cell = textwrap.fill(cell, inner_cell_width) + # add the wrapped cell to the row wrapped_row.append(cell) return wrapped_row diff --git a/tests/test_convert.py b/tests/test_convert.py index 3030cd6..d510a1a 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -305,3 +305,22 @@ def test_east_asian_wide_characters_and_zero_width_no_wcwidth(): "╚════╩═══════════════╝" ) assert text == expected + + +def test_multiline_cells_with_wrappable_lines(): + text = t2a( + header=["Test"], + body=[["Line One...\nSecond Line...\nLineNumThree\nLineFour\nFive FinalLine"]], + ) + expected = ( + "╔════════════════╗\n" + "║ Test ║\n" + "╟────────────────╢\n" + "║ Line One... ║\n" + "║ Second Line... ║\n" + "║ LineNumThree ║\n" + "║ LineFour ║\n" + "║ Five FinalLine ║\n" + "╚════════════════╝" + ) + assert text == expected From 3fe8b5c732eb65047c9547ee2ce4361701480091 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Fri, 20 Oct 2023 10:42:54 -0600 Subject: [PATCH 20/27] chore: bump to version 1.1.3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e7343bd..267f35a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "table2ascii" -version = "1.1.2" +version = "1.1.3" authors = [{name = "Jonah Lawrence", email = "jonah@freshidea.com"}] description = "Convert 2D Python lists into Unicode/ASCII tables" readme = "README.md" From ed3f1425282b1e431a3381f5c6e87b7503276d30 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 23:43:38 -0600 Subject: [PATCH 21/27] [pre-commit.ci] pre-commit autoupdate (#114) 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 66571a9..0c735a2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ ci: repos: - repo: https://github.com/psf/black - rev: 23.9.1 + rev: 23.10.1 hooks: - id: black name: Running black in all files. From d2107a1065eee960ea1a9c5f386c3bf5def2a367 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 21:40:15 -0800 Subject: [PATCH 22/27] [pre-commit.ci] pre-commit autoupdate (#115) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.10.1 → 23.11.0](https://github.com/psf/black/compare/23.10.1...23.11.0) 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 0c735a2..9421020 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ ci: repos: - repo: https://github.com/psf/black - rev: 23.10.1 + rev: 23.11.0 hooks: - id: black name: Running black in all files. From a0d5d1fe0d56f3f4e6eaac284d434bc5b79b519d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 28 Dec 2023 12:27:34 +0200 Subject: [PATCH 23/27] [pre-commit.ci] pre-commit autoupdate (#116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.11.0 → 23.12.1](https://github.com/psf/black/compare/23.11.0...23.12.1) - [github.com/pycqa/isort: 5.12.0 → 5.13.2](https://github.com/pycqa/isort/compare/5.12.0...5.13.2) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9421020..0a4da44 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,13 +6,13 @@ ci: repos: - repo: https://github.com/psf/black - rev: 23.11.0 + rev: 23.12.1 hooks: - id: black name: Running black in all files. - repo: https://github.com/pycqa/isort - rev: 5.12.0 + rev: 5.13.2 hooks: - id: isort args: ["--profile", "black", "--extend-skip", "table2ascii"] From 66ecd7f3423179754890f3c77ff9bc8c272243d3 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Wed, 7 Aug 2024 15:13:26 +0300 Subject: [PATCH 24/27] docs: Lock <5 dependent sphinx extension versions (#119) --- pyproject.toml | 7 ++++++- table2ascii/exceptions.py | 10 +++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 267f35a..c76ef8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,12 +46,17 @@ dependencies = [ [project.optional-dependencies] docs = [ "enum-tools", - "sphinx", + "sphinx>=4.0.0,<5", "sphinx-autobuild", "sphinx-toolbox", "sphinxcontrib_trio", "sphinxext-opengraph", "sphinx-book-theme==0.3.3", + "sphinxcontrib-applehelp==1.0.4", + "sphinxcontrib-devhelp==1.0.2", + "sphinxcontrib-htmlhelp==2.0.1", + "sphinxcontrib-qthelp==1.0.3", + "sphinxcontrib-serializinghtml==1.1.5", ] dev = [ "mypy>=0.982,<2", diff --git a/table2ascii/exceptions.py b/table2ascii/exceptions.py index b4b6314..cefecf9 100644 --- a/table2ascii/exceptions.py +++ b/table2ascii/exceptions.py @@ -40,7 +40,7 @@ class FooterColumnCountMismatchError(ColumnCountMismatchError): This class is a subclass of :class:`ColumnCountMismatchError`. Attributes: - footer (:class:`Sequence `\ [:class:`SupportsStr`]): + footer (:class:`Sequence ` [:class:`SupportsStr`]): The footer that caused the error expected_columns (:class:`int`): The number of columns that were expected """ @@ -64,10 +64,10 @@ class BodyColumnCountMismatchError(ColumnCountMismatchError): This class is a subclass of :class:`ColumnCountMismatchError`. Attributes: - body (:class:`Sequence `\ [\ :class:`Sequence `\ [:class:`SupportsStr`]]): + body (:class:`Sequence ` [ :class:`Sequence ` [:class:`SupportsStr`]]): The body that caused the error expected_columns (:class:`int`): The number of columns that were expected - first_invalid_row (:class:`Sequence `\ [:class:`SupportsStr`]): + first_invalid_row (:class:`Sequence ` [:class:`SupportsStr`]): The first row with an invalid column count """ @@ -93,7 +93,7 @@ class AlignmentCountMismatchError(ColumnCountMismatchError): This class is a subclass of :class:`ColumnCountMismatchError`. Attributes: - alignments (:class:`Sequence `\ [:class:`Alignment`]): + alignments (:class:`Sequence ` [:class:`Alignment`]): The alignments that caused the error expected_columns (:class:`int`): The number of columns that were expected """ @@ -117,7 +117,7 @@ class ColumnWidthsCountMismatchError(ColumnCountMismatchError): This class is a subclass of :class:`ColumnCountMismatchError`. Attributes: - column_widths (:class:`Sequence `\ [:data:`Optional `\ [:class:`int`]]): + column_widths (:class:`Sequence ` [:data:`Optional ` [:class:`int`]]): The column widths that caused the error expected_columns (:class:`int`): The number of columns that were expected """ From eab1097778f565b81a8c35d2aac3f8835463ea39 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 7 Aug 2024 15:15:14 +0300 Subject: [PATCH 25/27] [pre-commit.ci] pre-commit autoupdate (#118) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jonah Lawrence --- .pre-commit-config.yaml | 4 ++-- table2ascii/__init__.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0a4da44..f7bfe56 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ ci: repos: - repo: https://github.com/psf/black - rev: 23.12.1 + rev: 24.8.0 hooks: - id: black name: Running black in all files. @@ -19,7 +19,7 @@ repos: name: Running isort in all files. - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: check-ast name: Check if python files are valid syntax for the ast parser diff --git a/table2ascii/__init__.py b/table2ascii/__init__.py index bc53a37..f521f0e 100644 --- a/table2ascii/__init__.py +++ b/table2ascii/__init__.py @@ -1,6 +1,7 @@ """ table2ascii - Library for converting 2D Python lists to fancy ASCII/Unicode tables """ + import sys from typing import TYPE_CHECKING From 1b016d6638e37a43270222f442da772078ecec22 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Aug 2024 15:16:22 +0300 Subject: [PATCH 26/27] chore(deps-dev): update pytest requirement from <8,>=6.0.0 to >=6.0.0,<9 (#117) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c76ef8f..f98d644 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ dev = [ "mypy>=0.982,<2", "pre-commit>=2.0.0,<4", "pyright>=1.0.0,<2", - "pytest>=6.0.0,<8", + "pytest>=6.0.0,<9", "slotscheck>=0.1.0,<1", "taskipy>=1.0.0,<2", "tox>=3.0.0,<5", From fb4f33c868974798df90d199d0495fe2ab72d428 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 20:25:23 +0300 Subject: [PATCH 27/27] chore(deps): update pre-commit requirement from <4,>=2.0.0 to >=2.0.0,<5 (#126) Updates the requirements on [pre-commit](https://github.com/pre-commit/pre-commit) to permit the latest version. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v2.0.0...v4.0.0) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f98d644..132658f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ docs = [ ] dev = [ "mypy>=0.982,<2", - "pre-commit>=2.0.0,<4", + "pre-commit>=2.0.0,<5", "pyright>=1.0.0,<2", "pytest>=6.0.0,<9", "slotscheck>=0.1.0,<1", 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