diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 7381f37..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
@@ -49,7 +49,7 @@ jobs:
setup.py
- name: Install dependencies
- run: python -m pip install -r requirements.txt -e ".[dev]"
+ run: python -m pip install -e ".[dev]"
- name: Set up pyright
run: echo "PYRIGHT_VERSION=$(python -c 'import pyright; print(pyright.__pyright_version__)')" >> $GITHUB_ENV
@@ -94,7 +94,7 @@ jobs:
setup.py
- name: Install dependencies
- run: python -m pip install -r requirements.txt -e ".[dev]" -e ".[docs]"
+ run: python -m pip install -e ".[dev,docs]"
- name: Run mypy
run: mypy .
diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml
index 7694a92..d550962 100644
--- a/.github/workflows/python-test.yml
+++ b/.github/workflows/python-test.yml
@@ -13,23 +13,44 @@ on:
- main
jobs:
- build:
+ test-ubuntu:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
+ python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install -e ".[dev]"
+ python -m pip install --pre tox-gh-actions
+ - name: Test with pytest
+ run: |
+ # remove '.' in python-version and prepend with 'py' to get the correct tox env
+ tox -e py$(echo ${{ matrix.python-version }} | sed 's/\.//g')
+
+ test-windows:
+ runs-on: windows-latest
+
steps:
- - uses: actions/checkout@v3
- - name: Set up Python
- uses: actions/setup-python@v4
- with:
- python-version: ${{ matrix.python-version }}
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- python -m pip install -r requirements.txt -e ".[dev]"
- - name: Test with pytest
- run: |
- # remove '.' in python-version and prepend with 'py' to get the correct tox env
- tox -e py$(echo ${{ matrix.python-version }} | sed 's/\.//g')
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: "3.7"
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install -e ".[dev]"
+ python -m pip install --pre tox-gh-actions
+ - name: Test with pytest
+ run: tox
+ env:
+ PLATFORM: windows-latest
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 5c6b8e0..f7bfe56 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -6,20 +6,20 @@ ci:
repos:
- repo: https://github.com/psf/black
- rev: 22.12.0
+ rev: 24.8.0
hooks:
- id: black
name: Running black in all files.
- repo: https://github.com/pycqa/isort
- rev: 5.11.1
+ rev: 5.13.2
hooks:
- id: isort
args: ["--profile", "black", "--extend-skip", "table2ascii"]
name: Running isort in all files.
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.4.0
+ rev: v4.6.0
hooks:
- id: check-ast
name: Check if python files are valid syntax for the ast parser
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/README.md b/README.md
index ffc0261..b9ff0a7 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 ║
+╚═══════════════════════════════════════════════════╝
"""
```
@@ -119,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)
@@ -199,18 +203,19 @@ 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 | 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]`) |
+| `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 |
+| `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/_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/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/docs/source/usage.rst b/docs/source/usage.rst
index 70f8487..d1eb820 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,
+ )
+
+ 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/pyproject.toml b/pyproject.toml
index 49c617f..132658f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,15 +1,14 @@
[build-system]
-requires = [
- "setuptools>=42",
- "wheel"
-]
+requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
[project]
name = "table2ascii"
+version = "1.1.3"
authors = [{name = "Jonah Lawrence", email = "jonah@freshidea.com"}]
-dynamic = ["version", "description", "readme", "dependencies", "optional-dependencies"]
+description = "Convert 2D Python lists into Unicode/ASCII tables"
+readme = "README.md"
requires-python = ">=3.7"
license = {file = "LICENSE"}
keywords = ["table", "ascii", "unicode", "formatter"]
@@ -38,7 +37,36 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules",
"Typing :: Typed",
]
+dependencies = [
+ "typing-extensions>=3.7.4; python_version<'3.8'",
+ "importlib-metadata<5,>=1; python_version<'3.8'",
+ "wcwidth<1",
+]
+[project.optional-dependencies]
+docs = [
+ "enum-tools",
+ "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",
+ "pre-commit>=2.0.0,<5",
+ "pyright>=1.0.0,<2",
+ "pytest>=6.0.0,<9",
+ "slotscheck>=0.1.0,<1",
+ "taskipy>=1.0.0,<2",
+ "tox>=3.0.0,<5",
+]
[project.urls]
documentation = "https://table2ascii.rtfd.io"
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index b6ab4e4..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-typing-extensions>=3.7.4; python_version<'3.8'
-wcwidth<1
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 0b3faa6..9467adb 100644
--- a/setup.py
+++ b/setup.py
@@ -1,70 +1,51 @@
# /usr/bin/env python
-import os
import re
from setuptools import setup
-def version():
+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("table2ascii/__init__.py") as f:
- version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE)
+ 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 long_description():
- # check if README.md exists
- if not os.path.exists("README.md"):
- return ""
- with open("README.md", "r") as fh:
- return fh.read()
-
-
-def requirements():
- # check if requirements.txt exists
- if not os.path.exists("requirements.txt"):
+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 []
- with open("requirements.txt") as f:
- return f.read().splitlines()
-
-
-extras_require = {
- "docs": [
- "enum-tools",
- "sphinx",
- "sphinx-autobuild",
- "sphinx-toolbox",
- "sphinxcontrib_trio",
- "sphinxext-opengraph",
- "sphinx-book-theme==0.3.3",
- ],
- "dev": [
- "mypy>=0.982,<1",
- "pre-commit>=2.0.0,<3",
- "pyright>=1.0.0,<2",
- "pytest>=6.0.0,<8",
- "slotscheck>=0.1.0,<1",
- "taskipy>=1.0.0,<2",
- "tox>=3.0.0,<5",
- ],
-}
-
-setup(
- name="table2ascii",
- version=version(),
- author="Jonah Lawrence",
- author_email="jonah@freshidea.com",
- description="Convert 2D Python lists into Unicode/Ascii tables",
- long_description=long_description(),
- long_description_content_type="text/markdown",
- url="https://github.com/DenverCoder1/table2ascii",
- packages=["table2ascii"],
- install_requires=requirements(),
- extras_require=extras_require,
- setup_requires=[],
- tests_require=[
- "pytest>=6.2,<8",
- ],
-)
+ 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(),
+ )
diff --git a/table2ascii/__init__.py b/table2ascii/__init__.py
index ebd9744..f521f0e 100644
--- a/table2ascii/__init__.py
+++ b/table2ascii/__init__.py
@@ -2,13 +2,37 @@
table2ascii - Library for converting 2D Python lists to fancy ASCII/Unicode tables
"""
+import sys
+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
from .table_to_ascii import table2ascii
-__version__ = "1.0.2"
+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",
@@ -16,4 +40,18 @@
"PresetStyle",
"TableStyle",
"table2ascii",
+ "AlignmentCountMismatchError",
+ "BodyColumnCountMismatchError",
+ "ColumnCountMismatchError",
+ "ColumnWidthsCountMismatchError",
+ "ColumnWidthTooSmallError",
+ "FooterColumnCountMismatchError",
+ "InvalidAlignmentError",
+ "InvalidCellPaddingError",
+ "InvalidColumnWidthError",
+ "Table2AsciiError",
+ "TableOptionError",
+ "TableStyleTooLongError",
+ "TableStyleTooShortWarning",
+ "SupportsStr",
]
diff --git a/table2ascii/alignment.py b/table2ascii/alignment.py
index 0a8e5f7..f11fa39 100644
--- a/table2ascii/alignment.py
+++ b/table2ascii/alignment.py
@@ -4,31 +4,70 @@
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
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],
+ # 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],
)
\"\"\"
╔════════════════════════════════════════╗
- ║ 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 ║
╚════════════════════════════════════════╝
\"\"\"
+
+ A single alignment type can be used to align all columns::
+
+ table2ascii(
+ header=["First Name", "Last Name", "Age"],
+ body=[
+ ["John", "Smith", 30],
+ ["Jane", "Doe", 28],
+ ],
+ 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 ║
+ ╚══════════════════════════════╝
+ \"\"\"
+
+ .. note::
+
+ 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
+
+ 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/annotations.py b/table2ascii/annotations.py
index 241e787..4434ced 100644
--- a/table2ascii/annotations.py
+++ b/table2ascii/annotations.py
@@ -2,19 +2,17 @@
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):
- """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..cefecf9 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/options.py b/table2ascii/options.py
index 36f6ee0..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
@@ -19,7 +23,8 @@ 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
+ number_alignments: Sequence[Alignment] | Alignment | None
cell_padding: int
style: TableStyle
use_wcwidth: bool
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 8116467..65dfb8d 100644
--- a/table2ascii/table_to_ascii.py
+++ b/table2ascii/table_to_ascii.py
@@ -67,14 +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 = self.__determine_alignments(
+ options.alignments, default=Alignment.CENTER
+ )
+ self.__number_alignments = self.__determine_alignments(
+ options.number_alignments, default=self.__alignments
+ )
- self.__alignments = options.alignments or [Alignment.CENTER] * 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
- # 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)
+ # 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:
@@ -97,6 +103,38 @@ 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 __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.
@@ -105,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
@@ -125,10 +158,46 @@ 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):
+ # 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 []
+ 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 +238,47 @@ 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)
+ # 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}"
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)
@@ -220,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
@@ -403,7 +494,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 +621,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
@@ -569,34 +673,49 @@ 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,
+ number_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`
+ 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]``). If not specified or set to
- :py:obj:`None`, all columns will be center-aligned. Defaults to :py:obj:`None`.
+ (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`.
+
+ .. 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``.
@@ -608,6 +727,8 @@ 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`.
+ .. versionadded:: 1.0.0
+
Returns:
The generated ASCII table
"""
@@ -620,6 +741,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 3e8b874..67aa5d3 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
@@ -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"],
@@ -97,3 +97,96 @@ 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
+
+
+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
+
+
+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
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
diff --git a/tox.ini b/tox.ini
index d505b52..448f7b6 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,5 +4,4 @@ envlist = py37, py38, py39, py310, py311
[testenv]
deps =
pytest
- -rrequirements.txt
commands = pytest tests -s