diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 31f7efa..33ce501 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -34,7 +34,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ["3.7", "3.8", "3.9", "3.10"]
+ python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
fail-fast: false
steps:
- name: Checkout code
@@ -48,9 +48,8 @@ jobs:
cache-dependency-path: |
setup.py
- # all extras are installed to test
- name: Install dependencies
- run: python -m pip install -e ".[dev]" -e ".[docs]"
+ run: python -m pip install -r requirements.txt -e ".[dev]" -e ".[docs]"
- name: Set up pyright
run: echo "PYRIGHT_VERSION=$(python -c 'import pyright; print(pyright.__pyright_version__)')" >> $GITHUB_ENV
@@ -77,6 +76,29 @@ jobs:
no-comments: true
warnings: true
+ mypy:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.10"]
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Set up python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+ cache: pip
+ cache-dependency-path: |
+ setup.py
+
+ - name: Install dependencies
+ run: python -m pip install -r requirements.txt -e ".[dev]" -e ".[docs]"
+
+ - name: Run mypy
+ run: mypy .
+
slotscheck:
runs-on: ubuntu-latest
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 6926c88..a85ab79 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.8.0
+ rev: 22.10.0
hooks:
- id: black
name: Running black in all files.
diff --git a/README.md b/README.md
index d64cc09..280f15f 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ Documentation and examples are available at [table2ascii.rtfd.io](https://table2
## š§āš» Usage
-### Convert lists to ASCII tables
+### š Convert lists to ASCII tables
```py
from table2ascii import table2ascii
@@ -43,7 +43,7 @@ print(output)
"""
```
-### Set first or last column headings
+### š Set first or last column headings
```py
from table2ascii import table2ascii
@@ -63,7 +63,7 @@ print(output)
"""
```
-### Set column widths and alignments
+### š° Set column widths and alignments
```py
from table2ascii import table2ascii, Alignment
@@ -72,8 +72,8 @@ 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, 5, 5]
- alignments=[Alignment.LEFT] + [Alignment.RIGHT] * 4, # First is left, remaining 4 are right
+ column_widths=[5, 5, 5, 5, 5],
+ alignments=[Alignment.LEFT] + [Alignment.RIGHT] * 4,
)
print(output)
@@ -88,15 +88,17 @@ print(output)
"""
```
-### Use a preset style
+### šØ Use a preset style
+
+See a list of 30+ preset styles [here](https://table2ascii.readthedocs.io/en/latest/styles.html).
```py
-from table2ascii import table2ascii, PresetStyle
+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] * 4,
+ column_widths=[10, 10, 10, 10],
style=PresetStyle.ascii_box
)
@@ -111,9 +113,25 @@ print(output)
| 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
+### š² Define a custom style
Check [`TableStyle`](https://github.com/DenverCoder1/table2ascii/blob/main/table2ascii/table_style.py) for more info and [`PresetStyle`](https://github.com/DenverCoder1/table2ascii/blob/main/table2ascii/preset_style.py) for examples.
@@ -141,10 +159,6 @@ print(output)
"""
```
-## šØ Preset styles
-
-See a list of all preset styles [here](https://table2ascii.readthedocs.io/en/latest/styles.html).
-
## āļø Options
All parameters are optional.
@@ -156,22 +170,25 @@ All parameters are optional.
| `footer` | `List[Any]` | `None` | Last table row seperated by header row seperator. Values should support `str()`. |
| `column_widths` | `List[Optional[int]]` | `None` (automatic) | List of column widths in characters for each column |
| `alignments` | `List[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 |
+| `style` | `TableStyle` | `double_thin_compact` | Table style to use for the table\* |
| `first_col_heading` | `bool` | `False` | Whether to add a heading column seperator after the first column |
| `last_col_heading` | `bool` | `False` | Whether to add a heading column seperator before the last column |
+| `cell_padding` | `int` | `1` | The minimum number of spaces to add between the cell content and the cell border. |
+
+\*See a list of all preset styles [here](https://table2ascii.readthedocs.io/en/latest/styles.html).
See the [API Reference](https://table2ascii.readthedocs.io/en/latest/api.html) for more info.
## šØāšØ Use cases
-### Discord messages and embeds
+### šØļø Discord messages and embeds
- Display tables nicely inside markdown code blocks on Discord
- Useful for making Discord bots with [Discord.py](https://github.com/Rapptz/discord.py)

-### Terminal outputs
+### š» Terminal outputs
- Tables display nicely whenever monospace fonts are fully supported
- Tables make terminal outputs look more professional
diff --git a/docs/source/index.rst b/docs/source/index.rst
index a172de5..808a71e 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -1,5 +1,4 @@
:og:description: Python library for converting lists to fancy ASCII tables for displaying in the terminal and on Discord. Install with \`pip install -U table2ascii\`.
-:tocdepth: 2
table2ascii
===========
@@ -21,6 +20,7 @@ Contents
--------
.. toctree::
+ :maxdepth: 2
usage
api
diff --git a/docs/source/styles.rst b/docs/source/styles.rst
index eb2df1f..85f3bf1 100644
--- a/docs/source/styles.rst
+++ b/docs/source/styles.rst
@@ -347,6 +347,21 @@ Preset styles
2 ā 30 40 35 30
āāāāāāāāāāāāāāāāāāāāāāā
+.. _PresetStyle.plain:
+
+`plain`
+~~~~~~~
+
+.. code-block:: none
+
+ # G H R S
+ 1 30 40 35 30
+ 2 30 40 35 30
+ SUM 130 140 135 130
+
+ 1 30 40 35 30
+ 2 30 40 35 30
+
.. _PresetStyle.simple:
`simple`
diff --git a/docs/source/usage.rst b/docs/source/usage.rst
index c06c544..9b43dbd 100644
--- a/docs/source/usage.rst
+++ b/docs/source/usage.rst
@@ -79,7 +79,7 @@ Use a preset style
.. code:: py
- from table2ascii import table2ascii, PresetStyle
+ from table2ascii import table2ascii, Alignment, PresetStyle
output = table2ascii(
header=["First", "Second", "Third", "Fourth"],
@@ -100,6 +100,22 @@ Use a preset style
+----------+----------+----------+----------+
"""
+ 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
~~~~~~~~~~~~~~~~~~~~~
diff --git a/pyproject.toml b/pyproject.toml
index e7e32d6..e85aafc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -14,7 +14,7 @@ dynamic = ["version", "description"]
[tool.black]
line-length = 100
-target-version = ["py37", "py38", "py39", "py310"]
+target-version = ["py37", "py38", "py39", "py310", "py311"]
[tool.isort]
@@ -67,3 +67,15 @@ reportUnsupportedDunderAll = true
reportUnusedVariable = true
reportUnnecessaryComparison = true
reportUnnecessaryTypeIgnoreComment = true
+
+
+[tool.mypy]
+python_version = "3.10"
+namespace_packages = true
+
+
+[[tool.mypy.overrides]]
+module = [
+ "setuptools.*",
+]
+ignore_missing_imports = true
diff --git a/setup.py b/setup.py
index c4f622c..6cf7af7 100644
--- a/setup.py
+++ b/setup.py
@@ -48,6 +48,7 @@ def requirements():
"pyright==1.1.244",
"tox==3.24.5",
"pytest==7.1.2",
+ "mypy==0.982",
],
}
diff --git a/table2ascii/__init__.py b/table2ascii/__init__.py
index aff121e..3304b23 100644
--- a/table2ascii/__init__.py
+++ b/table2ascii/__init__.py
@@ -7,7 +7,7 @@
from .table_style import TableStyle
from .table_to_ascii import table2ascii
-__version__ = "0.4.0"
+__version__ = "0.5.0"
__all__ = [
"table2ascii",
diff --git a/table2ascii/annotations.py b/table2ascii/annotations.py
index cb78bdd..241e787 100644
--- a/table2ascii/annotations.py
+++ b/table2ascii/annotations.py
@@ -1,11 +1,10 @@
+import sys
from abc import abstractmethod
from typing import TYPE_CHECKING
-try:
- # Python 3.8+
+if sys.version_info >= (3, 8):
from typing import Protocol, runtime_checkable
-except ImportError:
- # Python 3.7
+else:
from typing_extensions import Protocol, runtime_checkable
if TYPE_CHECKING:
@@ -16,8 +15,6 @@
class SupportsStr(Protocol):
"""An ABC with one abstract method __str__."""
- __slots__ = ()
-
@abstractmethod
def __str__(self) -> str:
pass
diff --git a/table2ascii/options.py b/table2ascii/options.py
index 10ae1b2..bb2ac6e 100644
--- a/table2ascii/options.py
+++ b/table2ascii/options.py
@@ -13,4 +13,5 @@ class Options:
last_col_heading: bool
column_widths: Optional[List[Optional[int]]]
alignments: Optional[List[Alignment]]
+ cell_padding: int
style: TableStyle
diff --git a/table2ascii/preset_style.py b/table2ascii/preset_style.py
index 929b11c..4ef96b0 100644
--- a/table2ascii/preset_style.py
+++ b/table2ascii/preset_style.py
@@ -43,6 +43,7 @@ class PresetStyle:
ascii_minimalist = TableStyle.from_string(" --- | === --- -- ")
ascii_borderless = TableStyle.from_string(" | - ")
ascii_simple = TableStyle.from_string(" = | = ")
- ascii_rounded = TableStyle.from_string("/===\|| |=|=||-|-|\|=/")
- ascii_rounded_box = TableStyle.from_string("/===\||||=||||-|||\||/")
+ ascii_rounded = TableStyle.from_string(r"/===\|| |=|=||-|-|\|=/")
+ ascii_rounded_box = TableStyle.from_string(r"/===\||||=||||-|||\||/")
markdown = TableStyle.from_string(" ||||-||| ")
+ plain = TableStyle.from_string(" ").set(left_and_right_edge="")
diff --git a/table2ascii/py.typed b/table2ascii/py.typed
new file mode 100644
index 0000000..356bf16
--- /dev/null
+++ b/table2ascii/py.typed
@@ -0,0 +1 @@
+# Marker file for PEP 561. The table2ascii package uses inline types.
\ No newline at end of file
diff --git a/table2ascii/table_style.py b/table2ascii/table_style.py
index 2239616..323c7d2 100644
--- a/table2ascii/table_style.py
+++ b/table2ascii/table_style.py
@@ -75,3 +75,21 @@ def from_string(cls, string: str) -> "TableStyle":
TableStyle.from_string("āāā¦āāāā āāā«ā⢠āā©āā")
"""
return cls(*string)
+
+ def set(self, **kwargs) -> "TableStyle":
+ """
+ Set attributes of the TableStyle
+
+ Args:
+ kwargs: The attributes to set
+
+ Returns:
+ A TableStyle object with the attributes set
+
+ Example::
+
+ TableStyle().set(top_left_corner="ā", top_and_bottom_edge="ā")
+ """
+ for key, value in kwargs.items():
+ setattr(self, key, value)
+ return self
diff --git a/table2ascii/table_to_ascii.py b/table2ascii/table_to_ascii.py
index 7b8ab81..8835e61 100644
--- a/table2ascii/table_to_ascii.py
+++ b/table2ascii/table_to_ascii.py
@@ -34,6 +34,7 @@ def __init__(
self.__style = options.style
self.__first_col_heading = options.first_col_heading
self.__last_col_heading = options.last_col_heading
+ self.__cell_padding = options.cell_padding
# calculate number of columns
self.__columns = self.__count_columns()
@@ -71,6 +72,10 @@ def __init__(
if options.alignments and len(options.alignments) != self.__columns:
raise ValueError("Length of `alignments` list must equal the number of columns")
+ # check if the cell padding is valid
+ if self.__cell_padding < 0:
+ raise ValueError("Cell padding must be greater than or equal to 0")
+
def __count_columns(self) -> int:
"""
Get the number of columns in the table based on the
@@ -96,23 +101,20 @@ def __auto_column_widths(self) -> List[int]:
The minimum number of characters needed for each column
"""
- def widest_line(text: str) -> int:
+ def widest_line(value: SupportsStr) -> int:
"""Returns the width of the longest line in a multi-line string"""
+ text = str(value)
return max(len(line) for line in text.splitlines()) if len(text) else 0
column_widths = []
# get the width necessary for each column
for i in range(self.__columns):
- # col_widest returns the width of the widest line in the ith cell of a given list
- col_widest: Callable[[List[SupportsStr], int], int] = lambda row, i=i: widest_line(
- str(row[i])
- )
# number of characters in column of i of header, each body row, and footer
- header_size = col_widest(self.__header) if self.__header else 0
- body_size = map(col_widest, self.__body) if self.__body else [0]
- footer_size = col_widest(self.__footer) if self.__footer else 0
- # get the max and add 2 for padding each side with a space
- column_widths.append(max(header_size, *body_size, footer_size) + 2)
+ header_size = widest_line(self.__header[i]) if self.__header else 0
+ body_size = max(widest_line(row[i]) for row in self.__body) if self.__body else 0
+ footer_size = widest_line(self.__footer[i]) if self.__footer else 0
+ # 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)
return column_widths
def __pad(self, cell_value: SupportsStr, width: int, alignment: Alignment) -> str:
@@ -128,17 +130,19 @@ def __pad(self, cell_value: SupportsStr, width: int, alignment: Alignment) -> st
The padded text
"""
text = str(cell_value)
+ padding = " " * self.__cell_padding
+ padded_text = f"{padding}{text}{padding}"
if alignment == Alignment.LEFT:
# pad with spaces on the end
- return f" {text} " + (" " * (width - len(text) - 2))
+ return padded_text + (" " * (width - len(padded_text)))
if alignment == Alignment.CENTER:
# pad with spaces, half on each side
- before = " " * floor((width - len(text) - 2) / 2)
- after = " " * ceil((width - len(text) - 2) / 2)
- return before + f" {text} " + after
+ before = " " * floor((width - len(padded_text)) / 2)
+ after = " " * ceil((width - len(padded_text)) / 2)
+ return before + padded_text + after
if alignment == Alignment.RIGHT:
# pad with spaces at the beginning
- return (" " * (width - len(text) - 2)) + f" {text} "
+ return (" " * (width - len(padded_text))) + padded_text
raise ValueError(f"The value '{alignment}' is not valid for alignment.")
def __row_to_ascii(
@@ -321,6 +325,7 @@ def table2ascii(
last_col_heading: bool = False,
column_widths: Optional[List[Optional[int]]] = None,
alignments: Optional[List[Alignment]] = None,
+ cell_padding: int = 1,
style: TableStyle = PresetStyle.double_thin_compact,
) -> str:
"""
@@ -344,6 +349,9 @@ def table2ascii(
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`.
+ 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``.
style: Table style to use for styling (preset styles can be imported).
Defaults to :ref:`PresetStyle.double_thin_compact `.
@@ -359,6 +367,7 @@ def table2ascii(
last_col_heading=last_col_heading,
column_widths=column_widths,
alignments=alignments,
+ cell_padding=cell_padding,
style=style,
),
).to_ascii()
diff --git a/tests/test_cell_padding.py b/tests/test_cell_padding.py
new file mode 100644
index 0000000..6190053
--- /dev/null
+++ b/tests/test_cell_padding.py
@@ -0,0 +1,82 @@
+import pytest
+
+from table2ascii import Alignment, table2ascii as t2a
+
+
+def test_without_cell_padding():
+ text = t2a(
+ header=["#", "G", "H", "R", "S"],
+ body=[[1, 2, 3, 4, 5]],
+ footer=["A", "B", 1, 2, 3],
+ first_col_heading=True,
+ cell_padding=0,
+ )
+ expected = (
+ "āāā¦āāāāāāāā\n"
+ "ā#āG H R Sā\n"
+ "āāā«āāāāāāāā¢\n"
+ "ā1ā2 3 4 5ā\n"
+ "āāā«āāāāāāāā¢\n"
+ "āAāB 1 2 3ā\n"
+ "āāā©āāāāāāāā"
+ )
+ assert text == expected
+
+
+def test_column_width_and_alignment_without_cell_padding():
+ text = t2a(
+ header=["#", "G", "H", "R", "S"],
+ body=[[1, 2, 3, 4, 5]],
+ footer=["A", "B", 1, 2, 3],
+ column_widths=[4, 8, 5, 4, 5],
+ alignments=[
+ Alignment.LEFT,
+ Alignment.CENTER,
+ Alignment.RIGHT,
+ Alignment.LEFT,
+ Alignment.RIGHT,
+ ],
+ first_col_heading=True,
+ cell_padding=0,
+ )
+ expected = (
+ "āāāāāā¦āāāāāāāāāāāāāāāāāāāāāāāāāā\n"
+ "ā# ā G H R Sā\n"
+ "āāāāāā«āāāāāāāāāāāāāāāāāāāāāāāāāā¢\n"
+ "ā1 ā 2 3 4 5ā\n"
+ "āāāāāā«āāāāāāāāāāāāāāāāāāāāāāāāāā¢\n"
+ "āA ā B 1 2 3ā\n"
+ "āāāāāā©āāāāāāāāāāāāāāāāāāāāāāāāāā"
+ )
+ assert text == expected
+
+
+def test_cell_padding_more_than_one():
+ text = t2a(
+ header=["#", "G", "H", "R", "S"],
+ body=[[1, 2, 3, 4, 5]],
+ footer=["A", "B", 1, 2, 3],
+ first_col_heading=True,
+ cell_padding=2,
+ )
+ expected = (
+ "āāāāāāā¦āāāāāāāāāāāāāāāāāāāāāāāā\n"
+ "ā # ā G H R S ā\n"
+ "āāāāāāā«āāāāāāāāāāāāāāāāāāāāāāāā¢\n"
+ "ā 1 ā 2 3 4 5 ā\n"
+ "āāāāāāā«āāāāāāāāāāāāāāāāāāāāāāāā¢\n"
+ "ā A ā B 1 2 3 ā\n"
+ "āāāāāāā©āāāāāāāāāāāāāāāāāāāāāāāā"
+ )
+ assert text == expected
+
+
+def test_negative_cell_padding():
+ with pytest.raises(ValueError):
+ t2a(
+ header=["#", "G", "H", "R", "S"],
+ body=[[1, 2, 3, 4, 5]],
+ footer=["A", "B", 1, 2, 3],
+ first_col_heading=True,
+ cell_padding=-1,
+ )
diff --git a/tests/test_styles.py b/tests/test_styles.py
index 30e1670..422f0c1 100644
--- a/tests/test_styles.py
+++ b/tests/test_styles.py
@@ -649,3 +649,21 @@ def test_markdown():
"| SUM | 130 | 140 | 135 | 130 |"
)
assert text == expected
+
+
+def test_plain():
+ text = t2a(
+ header=["#", "G", "H", "R", "S"],
+ body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]],
+ footer=["SUM", "130", "140", "135", "130"],
+ first_col_heading=True,
+ last_col_heading=False,
+ style=PresetStyle.plain,
+ )
+ expected = (
+ " # G H R S \n"
+ " 1 30 40 35 30 \n"
+ " 2 30 40 35 30 \n"
+ " SUM 130 140 135 130 "
+ )
+ assert text == expected