statstables 0.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- statstables/__init__.py +1 -0
- statstables/renderers.py +237 -0
- statstables/tables.py +939 -0
- statstables/tests/__init__.py +0 -0
- statstables/tests/conftest.py +36 -0
- statstables/tests/test_tables.py +41 -0
- statstables/utils.py +38 -0
- statstables-0.0a1.dist-info/LICENSE +674 -0
- statstables-0.0a1.dist-info/METADATA +707 -0
- statstables-0.0a1.dist-info/RECORD +12 -0
- statstables-0.0a1.dist-info/WHEEL +5 -0
- statstables-0.0a1.dist-info/top_level.txt +1 -0
statstables/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from statstables import tables, renderers, utils
|
statstables/renderers.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Renderer(ABC):
|
|
5
|
+
def __init__(self):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
@abstractmethod
|
|
9
|
+
def render(self):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def generate_header(self): ...
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def generate_body(self): ...
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def generate_footer(self): ...
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def _create_line(self): ...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LatexRenderer(Renderer):
|
|
26
|
+
|
|
27
|
+
# LaTeX escape characters, borrowed from pandas.io.formats.latex and Stargazer
|
|
28
|
+
_ESCAPE_CHARS = [
|
|
29
|
+
("\\", r"\textbackslash "),
|
|
30
|
+
("_", r"\_"),
|
|
31
|
+
("%", r"\%"),
|
|
32
|
+
("$", r"\$"),
|
|
33
|
+
("#", r"\#"),
|
|
34
|
+
("{", r"\{"),
|
|
35
|
+
("}", r"\}"),
|
|
36
|
+
("~", r"\textasciitilde "),
|
|
37
|
+
("^", r"\textasciicircum "),
|
|
38
|
+
("&", r"\&"),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
def __init__(self, table):
|
|
42
|
+
self.table = table
|
|
43
|
+
|
|
44
|
+
def render(self, only_tabular=False):
|
|
45
|
+
out = self.generate_header(only_tabular)
|
|
46
|
+
out += self.generate_body()
|
|
47
|
+
out += self.generate_footer(only_tabular)
|
|
48
|
+
|
|
49
|
+
return out
|
|
50
|
+
|
|
51
|
+
def generate_header(self, only_tabular=False, column_alignment=None):
|
|
52
|
+
header = ""
|
|
53
|
+
if not only_tabular:
|
|
54
|
+
header += "\\begin{table}[!htbp]\n \\centering\n"
|
|
55
|
+
|
|
56
|
+
if self.table.caption_location == "top":
|
|
57
|
+
if self.table.caption is not None:
|
|
58
|
+
header += " \\caption{" + self.table.caption + "}\n"
|
|
59
|
+
|
|
60
|
+
if self.table.label is not None:
|
|
61
|
+
header += " \\label{" + self.table.label + "}\n"
|
|
62
|
+
|
|
63
|
+
content_columns = "c" * self.table.ncolumns
|
|
64
|
+
if self.table.include_index:
|
|
65
|
+
content_columns = "l" + content_columns
|
|
66
|
+
header += "\\begin{tabular}{" + content_columns + "}\n"
|
|
67
|
+
header += " \\toprule\n"
|
|
68
|
+
for col, spans in self.table._multicolumns:
|
|
69
|
+
header += (" " + self.table.index_name + " & ") * self.table.include_index
|
|
70
|
+
header += " & ".join(
|
|
71
|
+
[
|
|
72
|
+
f"\\multicolumn{{{s}}}{{c}}{{{self._escape(c)}}}"
|
|
73
|
+
for c, s in zip(col, spans)
|
|
74
|
+
]
|
|
75
|
+
)
|
|
76
|
+
header += " \\\\\n"
|
|
77
|
+
if self.table.custom_tex_lines["after-multicolumns"]:
|
|
78
|
+
for line in self.table.custom_tex_lines["after-multicolumns"]:
|
|
79
|
+
header += " " + line + "\n"
|
|
80
|
+
if self.table.show_columns:
|
|
81
|
+
header += (" " + self.table.index_name + " & ") * self.table.include_index
|
|
82
|
+
header += " & ".join(
|
|
83
|
+
[
|
|
84
|
+
self._escape(self.table._column_labels.get(col, col))
|
|
85
|
+
for col in self.table.columns
|
|
86
|
+
]
|
|
87
|
+
)
|
|
88
|
+
header += "\\\\\n"
|
|
89
|
+
if self.table.custom_tex_lines["after-columns"]:
|
|
90
|
+
for line in self.table.custom_tex_lines["after-columns"]:
|
|
91
|
+
header += " " + line + "\n"
|
|
92
|
+
if self.table.custom_lines["after-columns"]:
|
|
93
|
+
for line in self.table.custom_lines["after-columns"]:
|
|
94
|
+
header += self._create_line(line)
|
|
95
|
+
header += " \\midrule\n"
|
|
96
|
+
|
|
97
|
+
return header
|
|
98
|
+
|
|
99
|
+
def generate_body(self):
|
|
100
|
+
rows = self.table._create_rows()
|
|
101
|
+
row_str = ""
|
|
102
|
+
for row in rows:
|
|
103
|
+
row_str += " " + " & ".join([self._escape(r) for r in row]) + " \\\\\n"
|
|
104
|
+
for line in self.table.custom_lines["after-body"]:
|
|
105
|
+
row_str += self._create_line(line)
|
|
106
|
+
for line in self.table.custom_tex_lines["after-body"]:
|
|
107
|
+
row_str += line
|
|
108
|
+
return row_str
|
|
109
|
+
|
|
110
|
+
def generate_footer(self, only_tabular=False):
|
|
111
|
+
footer = " \\bottomrule\n"
|
|
112
|
+
if self.table.custom_lines["after-footer"]:
|
|
113
|
+
for line in self.table.custom_lines["after-footer"]:
|
|
114
|
+
footer += self._create_line(line)
|
|
115
|
+
footer += " \\bottomrule\n"
|
|
116
|
+
if self.table.notes:
|
|
117
|
+
for note, alignment, escape in self.table.notes:
|
|
118
|
+
align_cols = self.table.ncolumns + self.table.include_index
|
|
119
|
+
footer += f" \\multicolumn{{{align_cols}}}{{{alignment}}}"
|
|
120
|
+
_note = self._escape(note) if escape else note
|
|
121
|
+
footer += "{{" + "\\small \\textit{" + _note + "}}}\\\\\n"
|
|
122
|
+
|
|
123
|
+
footer += "\\end{tabular}\n"
|
|
124
|
+
if not only_tabular:
|
|
125
|
+
if self.table.caption_location == "bottom":
|
|
126
|
+
if self.table.caption is not None:
|
|
127
|
+
footer += " \\caption{" + self.table.caption + "}\n"
|
|
128
|
+
|
|
129
|
+
if self.table.label is not None:
|
|
130
|
+
footer += " \\label{" + self.table.label + "}\n"
|
|
131
|
+
footer += "\\end{table}\n"
|
|
132
|
+
|
|
133
|
+
return footer
|
|
134
|
+
|
|
135
|
+
def _escape(self, text):
|
|
136
|
+
for char, escaped in self._ESCAPE_CHARS:
|
|
137
|
+
text = text.replace(char, escaped)
|
|
138
|
+
return text
|
|
139
|
+
|
|
140
|
+
def _create_line(self, line):
|
|
141
|
+
out = (" " + line["label"] + " & ") * self.table.include_index
|
|
142
|
+
out += " & ".join(line["line"])
|
|
143
|
+
out += "\\\\\n"
|
|
144
|
+
|
|
145
|
+
return out
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class HTMLRenderer(Renderer):
|
|
149
|
+
|
|
150
|
+
ALIGNMENTS = {"l": "left", "c": "center", "r": "right"}
|
|
151
|
+
|
|
152
|
+
def __init__(self, table):
|
|
153
|
+
self.table = table
|
|
154
|
+
self.ncolumns = self.table.ncolumns + int(self.table.include_index)
|
|
155
|
+
|
|
156
|
+
def render(self):
|
|
157
|
+
out = self.generate_header()
|
|
158
|
+
out += self.generate_body()
|
|
159
|
+
out += self.generate_footer()
|
|
160
|
+
return out
|
|
161
|
+
|
|
162
|
+
def generate_header(self):
|
|
163
|
+
header = "<table>\n"
|
|
164
|
+
header += " <thead>\n"
|
|
165
|
+
for col, spans in self.table._multicolumns:
|
|
166
|
+
header += " <tr>\n"
|
|
167
|
+
header += (
|
|
168
|
+
f" <th>{self.table.index_name}</th>\n"
|
|
169
|
+
) * self.table.include_index
|
|
170
|
+
header += " " + " ".join(
|
|
171
|
+
[
|
|
172
|
+
f'<th colspan="{s}" style="text-align:center;">{c}</th>'
|
|
173
|
+
for c, s in zip(col, spans)
|
|
174
|
+
]
|
|
175
|
+
)
|
|
176
|
+
header += "\n"
|
|
177
|
+
header += " </tr>\n"
|
|
178
|
+
for line in self.table.custom_html_lines["after-multicolumns"]:
|
|
179
|
+
# TODO: Implement
|
|
180
|
+
pass
|
|
181
|
+
if self.table.show_columns:
|
|
182
|
+
header += " <tr>\n"
|
|
183
|
+
header += (
|
|
184
|
+
f" <th>{self.table.index_name}</th>\n"
|
|
185
|
+
) * self.table.include_index
|
|
186
|
+
for col in self.table.columns:
|
|
187
|
+
header += f' <th style="text-align:center;">{self.table._column_labels.get(col, col)}</th>\n'
|
|
188
|
+
header += " </tr>\n"
|
|
189
|
+
if self.table.custom_lines["after-columns"]:
|
|
190
|
+
for line in self.table.custom_lines["after-columns"]:
|
|
191
|
+
header += self._create_line(line)
|
|
192
|
+
header += " </thead>\n"
|
|
193
|
+
header += " <tbody>\n"
|
|
194
|
+
return header
|
|
195
|
+
|
|
196
|
+
def generate_body(self):
|
|
197
|
+
rows = self.table._create_rows()
|
|
198
|
+
row_str = ""
|
|
199
|
+
for row in rows:
|
|
200
|
+
row_str += " <tr>\n"
|
|
201
|
+
for r in row:
|
|
202
|
+
row_str += f" <td>{r}</td>\n"
|
|
203
|
+
row_str += " </tr>\n"
|
|
204
|
+
for line in self.table.custom_lines["after-body"]:
|
|
205
|
+
row_str += self._create_line(line)
|
|
206
|
+
for line in self.table.custom_html_lines["after-body"]:
|
|
207
|
+
row_str += line
|
|
208
|
+
pass
|
|
209
|
+
return row_str
|
|
210
|
+
|
|
211
|
+
def generate_footer(self):
|
|
212
|
+
footer = ""
|
|
213
|
+
if self.table.custom_lines["after-footer"]:
|
|
214
|
+
footer += " <tr>\n"
|
|
215
|
+
for line in self.table.custom_lines["after-footer"]:
|
|
216
|
+
footer += self._create_line(line)
|
|
217
|
+
footer += " </tr>\n"
|
|
218
|
+
if self.table.notes:
|
|
219
|
+
ncols = self.table.ncolumns + self.table.include_index
|
|
220
|
+
for note, alignment, _ in self.table.notes:
|
|
221
|
+
# TODO: This doesn't actually align where expected in a notebook. Fix.
|
|
222
|
+
footer += (
|
|
223
|
+
f' <tr><td colspan="{ncols}" '
|
|
224
|
+
f'style="text-align:{self.ALIGNMENTS[alignment]};'
|
|
225
|
+
f'"><i>{note}</i></td></tr>\n'
|
|
226
|
+
)
|
|
227
|
+
footer += " </tbody>\n"
|
|
228
|
+
return footer
|
|
229
|
+
|
|
230
|
+
def _create_line(self, line):
|
|
231
|
+
out = " <tr>\n"
|
|
232
|
+
out += (f" <th>{line['label']}</th>\n") * self.table.include_index
|
|
233
|
+
for l in line["line"]:
|
|
234
|
+
out += f" <th>{l}</th>\n"
|
|
235
|
+
out += " </tr>\n"
|
|
236
|
+
|
|
237
|
+
return out
|