Skip to content

Commit bce515c

Browse files
committed
separated files and added preset styles
1 parent 171e881 commit bce515c

File tree

7 files changed

+906
-294
lines changed

7 files changed

+906
-294
lines changed

table2ascii/__init__.py

Lines changed: 6 additions & 294 deletions
Original file line numberDiff line numberDiff line change
@@ -1,295 +1,7 @@
1-
import enum
2-
from dataclasses import dataclass
3-
from math import ceil, floor
4-
from typing import List, Optional, Union
1+
from .table_to_ascii import table2ascii
2+
from .alignment import Alignment
53

6-
7-
class Alignment(enum.Enum):
8-
"""Enum for alignment types"""
9-
10-
LEFT = 0
11-
CENTER = 1
12-
RIGHT = 2
13-
14-
15-
@dataclass
16-
class Options:
17-
"""Class for storing options that the user sets"""
18-
19-
header: Optional[List] = None
20-
body: Optional[List[List]] = None
21-
footer: Optional[List] = None
22-
first_col_heading: bool = False
23-
last_col_heading: bool = False
24-
column_widths: Optional[List[int]] = None
25-
alignments: Optional[List[Alignment]] = None
26-
27-
28-
class TableToAscii:
29-
"""Class used to convert a 2D Python table to ASCII text"""
30-
31-
def __init__(self, options: Options):
32-
"""Validate arguments and initialize fields"""
33-
# initialize fields
34-
self.__header = options.header
35-
self.__body = options.body
36-
self.__footer = options.footer
37-
self.__first_col_heading = options.first_col_heading
38-
self.__last_col_heading = options.last_col_heading
39-
40-
# calculate number of columns
41-
self.__columns = self.__count_columns()
42-
43-
# check if footer has a different number of columns
44-
if options.footer and len(options.footer) != self.__columns:
45-
raise ValueError(
46-
"Footer must have the same number of columns as the other rows"
47-
)
48-
# check if any rows in body have a different number of columns
49-
if options.body and any(len(row) != self.__columns for row in options.body):
50-
raise ValueError(
51-
"All rows in body must have the same number of columns as the other rows"
52-
)
53-
54-
# calculate or use given column widths
55-
self.__column_widths = options.column_widths or self.__auto_column_widths()
56-
57-
# check if column widths specified have a different number of columns
58-
if options.column_widths and len(options.column_widths) != self.__columns:
59-
raise ValueError(
60-
"Length of `column_widths` list must equal the number of columns"
61-
)
62-
# check if column widths are not all at least 2
63-
if options.column_widths and min(options.column_widths) < 2:
64-
raise ValueError(
65-
"All values in `column_widths` must be greater than or equal to 2"
66-
)
67-
68-
self.__alignments = options.alignments or [Alignment.CENTER] * self.__columns
69-
70-
# check if alignments specified have a different number of columns
71-
if options.alignments and len(options.alignments) != self.__columns:
72-
raise ValueError(
73-
"Length of `alignments` list must equal the number of columns"
74-
)
75-
76-
"""
77-
╔═════╦═══════════════════════╗ ABBBBBCBBBBBDBBBBBDBBBBBDBBBBBE
78-
║ # ║ G H R S ║ F G H H H F
79-
╟─────╫───────────────────────╢ IJJJJJKJJJJJLJJJJJLJJJJJLJJJJJM
80-
║ 1 ║ 30 40 35 30 ║ F G H H H F
81-
║ 2 ║ 30 40 35 30 ║ F G H H H F
82-
╟─────╫───────────────────────╢ NOOOOOPOOOOOQOOOOOQOOOOOQOOOOOR
83-
║ SUM ║ 130 140 135 130 ║ F G H H H F
84-
╚═════╩═══════════════════════╝ SBBBBBTBBBBBUBBBBBUBBBBBUBBBBBV
85-
"""
86-
self.__parts = {
87-
"top_left_corner": "╔", # A
88-
"top_and_bottom_edge": "═", # B
89-
"heading_col_top_tee": "╦", # C
90-
"top_tee": "═", # D
91-
"top_right_corner": "╗", # E
92-
"left_and_right_edge": "║", # F
93-
"heading_col_sep": "║", # G
94-
"middle_edge": " ", # H
95-
"header_left_tee": "╟", # I
96-
"header_row_sep": "─", # J
97-
"heading_col_header_cross": "╫", # K
98-
"header_row_cross": "─", # L
99-
"header_right_tee": "╢", # M
100-
"footer_left_tee": "╟", # N
101-
"footer_row_sep": "─", # O
102-
"heading_col_footer_cross": "╫", # P
103-
"footer_row_cross": "─", # Q
104-
"footer_right_tee": "╢", # R
105-
"bottom_left_corner": "╚", # S
106-
"heading_col_bottom_tee": "╩", # T
107-
"bottom_tee": "═", # U
108-
"bottom_right_corner": "╝", # V
109-
}
110-
111-
def __count_columns(self) -> int:
112-
"""Get the number of columns in the table
113-
based on the provided header, footer, and body lists.
114-
"""
115-
if self.__header:
116-
return len(self.__header)
117-
if self.__footer:
118-
return len(self.__footer)
119-
if self.__body and len(self.__body) > 0:
120-
return len(self.__body[0])
121-
return 0
122-
123-
def __auto_column_widths(self) -> List[int]:
124-
"""Get the minimum number of characters needed for the values
125-
in each column in the table with 1 space of padding on each side.
126-
"""
127-
column_widths = []
128-
for i in range(self.__columns):
129-
# number of characters in column of i of header, each body row, and footer
130-
header_size = len(self.__header[i]) if self.__header else 0
131-
body_size = (
132-
map(lambda row, i=i: len(row[i]), self.__body) if self.__body else [0]
133-
)
134-
footer_size = len(self.__footer[i]) if self.__footer else 0
135-
# get the max and add 2 for padding each side with a space
136-
column_widths.append(max(header_size, *body_size, footer_size) + 2)
137-
return column_widths
138-
139-
def __pad(self, text: str, width: int, alignment: Alignment):
140-
"""Pad a string of text to a given width with specified alignment"""
141-
if alignment == Alignment.LEFT:
142-
# pad with spaces on the end
143-
return f" {text} " + (" " * (width - len(text) - 2))
144-
if alignment == Alignment.CENTER:
145-
# pad with spaces, half on each side
146-
before = " " * floor((width - len(text) - 2) / 2)
147-
after = " " * ceil((width - len(text) - 2) / 2)
148-
return before + f" {text} " + after
149-
if alignment == Alignment.RIGHT:
150-
# pad with spaces at the beginning
151-
return (" " * (width - len(text) - 2)) + f" {text} "
152-
raise ValueError(f"The value '{alignment}' is not valid for alignment.")
153-
154-
def __row_to_ascii(
155-
self,
156-
left_edge: str,
157-
heading_col_sep: str,
158-
column_seperator: str,
159-
right_edge: str,
160-
filler: Union[str, List],
161-
) -> str:
162-
"""Assembles a row of the ascii table"""
163-
# left edge of the row
164-
output = left_edge
165-
# add columns
166-
for i in range(self.__columns):
167-
# content between separators
168-
output += (
169-
# edge or row separator if filler is a specific character
170-
filler * self.__column_widths[i]
171-
if isinstance(filler, str)
172-
# otherwise, use the column content
173-
else self.__pad(
174-
str(filler[i]), self.__column_widths[i], self.__alignments[i]
175-
)
176-
)
177-
# column seperator
178-
sep = column_seperator
179-
if i == 0 and self.__first_col_heading:
180-
# use column heading if first column option is specified
181-
sep = heading_col_sep
182-
elif i == self.__columns - 2 and self.__last_col_heading:
183-
# use column heading if last column option is specified
184-
sep = heading_col_sep
185-
elif i == self.__columns - 1:
186-
# replace last seperator with symbol for edge of the row
187-
sep = right_edge
188-
output += sep
189-
return output + "\n"
190-
191-
def __top_edge_to_ascii(self) -> str:
192-
"""Assembles the top edge of the ascii table"""
193-
return self.__row_to_ascii(
194-
left_edge=self.__parts["top_left_corner"],
195-
heading_col_sep=self.__parts["heading_col_top_tee"],
196-
column_seperator=self.__parts["top_tee"],
197-
right_edge=self.__parts["top_right_corner"],
198-
filler=self.__parts["top_and_bottom_edge"],
199-
)
200-
201-
def __bottom_edge_to_ascii(self) -> str:
202-
"""Assembles the top edge of the ascii table"""
203-
return self.__row_to_ascii(
204-
left_edge=self.__parts["bottom_left_corner"],
205-
heading_col_sep=self.__parts["heading_col_bottom_tee"],
206-
column_seperator=self.__parts["bottom_tee"],
207-
right_edge=self.__parts["bottom_right_corner"],
208-
filler=self.__parts["top_and_bottom_edge"],
209-
)
210-
211-
def __header_row_to_ascii(self) -> str:
212-
"""Assembles the header row line of the ascii table"""
213-
return self.__row_to_ascii(
214-
left_edge=self.__parts["left_and_right_edge"],
215-
heading_col_sep=self.__parts["heading_col_sep"],
216-
column_seperator=self.__parts["middle_edge"],
217-
right_edge=self.__parts["left_and_right_edge"],
218-
filler=self.__header,
219-
)
220-
221-
def __footer_row_to_ascii(self) -> str:
222-
"""Assembles the header row line of the ascii table"""
223-
return self.__row_to_ascii(
224-
left_edge=self.__parts["left_and_right_edge"],
225-
heading_col_sep=self.__parts["heading_col_sep"],
226-
column_seperator=self.__parts["middle_edge"],
227-
right_edge=self.__parts["left_and_right_edge"],
228-
filler=self.__footer,
229-
)
230-
231-
def __header_sep_to_ascii(self) -> str:
232-
"""Assembles the seperator below the header of the ascii table"""
233-
return self.__row_to_ascii(
234-
left_edge=self.__parts["header_left_tee"],
235-
heading_col_sep=self.__parts["heading_col_header_cross"],
236-
column_seperator=self.__parts["header_row_cross"],
237-
right_edge=self.__parts["header_right_tee"],
238-
filler=self.__parts["header_row_sep"],
239-
)
240-
241-
def __footer_sep_to_ascii(self) -> str:
242-
"""Assembles the seperator below the header of the ascii table"""
243-
return self.__row_to_ascii(
244-
left_edge=self.__parts["footer_left_tee"],
245-
heading_col_sep=self.__parts["heading_col_footer_cross"],
246-
column_seperator=self.__parts["footer_row_cross"],
247-
right_edge=self.__parts["footer_right_tee"],
248-
filler=self.__parts["footer_row_sep"],
249-
)
250-
251-
def __body_to_ascii(self) -> str:
252-
return "".join(
253-
self.__row_to_ascii(
254-
left_edge=self.__parts["left_and_right_edge"],
255-
heading_col_sep=self.__parts["heading_col_sep"],
256-
column_seperator=self.__parts["middle_edge"],
257-
right_edge=self.__parts["left_and_right_edge"],
258-
filler=row,
259-
)
260-
for row in self.__body
261-
)
262-
263-
def to_ascii(self) -> str:
264-
# top row of table
265-
table = self.__top_edge_to_ascii()
266-
# add table header
267-
if self.__header:
268-
table += self.__header_row_to_ascii()
269-
table += self.__header_sep_to_ascii()
270-
# add table body
271-
if self.__body:
272-
table += self.__body_to_ascii()
273-
# add table footer
274-
if self.__footer:
275-
table += self.__footer_sep_to_ascii()
276-
table += self.__footer_row_to_ascii()
277-
# bottom row of table
278-
table += self.__bottom_edge_to_ascii()
279-
# reurn ascii table
280-
return table
281-
282-
283-
def table2ascii(**options) -> str:
284-
"""Convert a 2D Python table to ASCII text
285-
286-
### Arguments
287-
:param header: :class:`Optional[List]` List of column values in the table's header row
288-
:param body: :class:`Optional[List[List]]` 2-dimensional list of values in the table's body
289-
:param footer: :class:`Optional[List]` List of column values in the table's footer row
290-
:param column_widths: :class:`Optional[List[int]]` List of widths in characters for each column (defaults to auto-sizing)
291-
:param alignments: :class:`Optional[List[Alignment]]` List of alignments (ex. `[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT]`)
292-
:param first_col_heading: :class:`Optional[bool]` Whether to add a header column separator after the first column
293-
:param last_col_heading: :class:`Optional[bool]` Whether to add a header column separator before the last column
294-
"""
295-
return TableToAscii(Options(**options)).to_ascii()
4+
__all__ = [
5+
"table2ascii",
6+
"Alignment",
7+
]

table2ascii/alignment.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import enum
2+
3+
4+
class Alignment(enum.Enum):
5+
"""Enum for alignment types"""
6+
7+
LEFT = 0
8+
CENTER = 1
9+
RIGHT = 2

table2ascii/options.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from dataclasses import dataclass
2+
from typing import List, Optional
3+
4+
from . import styles
5+
from .alignment import Alignment
6+
from .style import Style
7+
8+
9+
@dataclass
10+
class Options:
11+
"""Class for storing options that the user sets"""
12+
13+
header: Optional[List] = None
14+
body: Optional[List[List]] = None
15+
footer: Optional[List] = None
16+
first_col_heading: bool = False
17+
last_col_heading: bool = False
18+
column_widths: Optional[List[int]] = None
19+
alignments: Optional[List[Alignment]] = None
20+
style: Style = styles.double_thin

table2ascii/style.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from dataclasses import dataclass
2+
3+
4+
@dataclass
5+
class Style:
6+
"""Class for storing information about a table style
7+
8+
**Parts of the table labeled alphabetically**
9+
10+
```text
11+
ABBBBBCBBBBBDBBBBBDBBBBBDBBBBBE
12+
F G H H H F
13+
IJJJJJKJJJJJLJJJJJLJJJJJLJJJJJM
14+
F G H H H F
15+
F G H H H F
16+
NOOOOOPOOOOOQOOOOOQOOOOOQOOOOOR
17+
F G H H H F
18+
SBBBBBTBBBBBUBBBBBUBBBBBUBBBBBV
19+
```
20+
21+
**How the default theme is displayed**
22+
23+
```text
24+
╔═════╦═══════════════════════╗
25+
║ # ║ G H R S ║
26+
╟─────╫───────────────────────╢
27+
║ 1 ║ 30 40 35 30 ║
28+
║ 2 ║ 30 40 35 30 ║
29+
╟─────╫───────────────────────╢
30+
║ SUM ║ 130 140 135 130 ║
31+
╚═════╩═══════════════════════╝
32+
```
33+
"""
34+
35+
top_left_corner: str # A
36+
top_and_bottom_edge: str # B
37+
heading_col_top_tee: str # C
38+
top_tee: str # D
39+
top_right_corner: str # E
40+
left_and_right_edge: str # F
41+
heading_col_sep: str # G
42+
middle_edge: str # H
43+
header_left_tee: str # I
44+
header_row_sep: str # J
45+
heading_col_header_cross: str # K
46+
header_row_cross: str # L
47+
header_right_tee: str # M
48+
footer_left_tee: str # N
49+
footer_row_sep: str # O
50+
heading_col_footer_cross: str # P
51+
footer_row_cross: str # Q
52+
footer_right_tee: str # R
53+
bottom_left_corner: str # S
54+
heading_col_bottom_tee: str # T
55+
bottom_tee: str # U
56+
bottom_right_corner: str # V

0 commit comments

Comments
 (0)