Skip to content

Commit 20fa680

Browse files
author
Sam Kleinman
committed
build: refactor table builder
1 parent ec64350 commit 20fa680

File tree

1 file changed

+150
-159
lines changed

1 file changed

+150
-159
lines changed

bin/table_builder.py

Lines changed: 150 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -19,76 +19,138 @@
1919
except ImportError:
2020
exit('[table-builder]: You must install PyYAML to build tables.')
2121

22-
# global variables
23-
columnwidths=[] # the column widths to use in get_rowline() and get_row() methods
24-
tempcolumnwidths=[] # working list to determine columnwidths global variable
25-
# tempcolumnwidths keeps the running max value for each cell
26-
27-
###################################
28-
#
29-
# Generating parts of the table.
30-
31-
def get_row_line(delim='-'):
32-
"""
33-
Prints lines to seperate rows in the column for restrucutred text
34-
format.
35-
"""
36-
return '+' + delim + str(delim + '+' + delim).join([ delim * width for width in columnwidths ]) + delim + '+'
37-
38-
def get_row(rowdata):
39-
"""
40-
Prints the contents of the row, seperated as needed for
41-
restructured text format.
42-
"""
43-
rowlines = []
44-
for line in zip(*rowdata):
45-
if len(rowlines) > 0:
46-
rowlines.append('\n')
47-
rowlines.append( '| ' + ' | '.join([ line[idx].ljust(columnwidths[idx]) for idx in range(len(line)) ]) + ' |' )
48-
49-
return ''.join(rowlines)
50-
51-
###################################
52-
#
53-
# Flexible table building processing.
54-
5522
def normalize_cell_height(rowdata):
5623
"""
5724
Normalize cells in the rowdata so that each cell has the same height;
5825
more specifically, ensure that each cell data list spans the same number of
59-
lines when printed as others in the row.
26+
lines when printed as others in the row. Mutates 'rowdata' in place by only
27+
appeneding rather than resetting the reference.
6028
"""
61-
62-
# This function mutates the rowdata object, which is passed by
63-
# reference.. By appending to rowdata content as opposed to
64-
# resetting the reference (i.e. setting it equal to something
65-
# else) the method modifies whatever was referenced by rowdata and
66-
# does not need to explicitly return something
67-
6829
maxlines = max([ len(cell) for cell in rowdata])
6930

7031
for cell in rowdata:
71-
for x in range(maxlines-len(cell)):
32+
for x in range(maxlines - len(cell)):
7233
cell.append( ' ' )
7334

74-
def check_column_width(rowdata):
75-
"""
76-
Compares the cell widths of the row with the curren max cell width
77-
in the global variables. Then updates the global values in the
78-
variable ``tempcolumnwidths``. ``tempcolumnwidths`` variable maintains
79-
the running max width of each column.
35+
def get_default_outputfile(inputfile):
36+
return inputfile.rsplit('.')[0] + '.rst'
8037

81-
By appending to the variable, the global
82-
scope of tempcolumnwidths is maintained.
83-
"""
8438

85-
thisrowwidths = [ max([len(line) for line in cell ]) for cell in rowdata]
39+
###################################
40+
#
41+
# Generating parts of the table.
8642

87-
if len( tempcolumnwidths ) == 0:
88-
tempcolumnwidths.append(thisrowwidths)
89-
else:
90-
currentmaxwidths = tempcolumnwidths.pop()
91-
tempcolumnwidths.append([ max(currentmaxwidths[i], thisrowwidths[i]) for i in range(len(currentmaxwidths))])
43+
class RstTable(object):
44+
def __init__(self, inputfile):
45+
self.columnwidths = []
46+
self.tempcolumnwidths = []
47+
self.read_data(inputfile)
48+
self.process_table_content()
49+
50+
def read_data(self, datafile):
51+
with open(datafile, "rt") as f:
52+
parsed = yaml.load_all(f)
53+
layout = dict2obj(parsed.next())
54+
meta = dict2obj(parsed.next())
55+
content = dict2obj(parsed.next())
56+
57+
if layout.section != 'layout':
58+
exit('layout document in "' + datafile + '" is malformed.')
59+
elif meta.section != 'meta':
60+
exit('meta document in "' + datafile + '" is malformed.')
61+
elif content.section != 'content':
62+
exit('content document in "' + datafile + '" is malformed.')
63+
64+
rows = { 'rows': [] }
65+
66+
if layout.header:
67+
header = []
68+
for cell in layout.header:
69+
header.append([eval(cell)])
70+
else:
71+
header = None
72+
73+
for rownum in layout.rows:
74+
parsed_cell = []
75+
for cell in rownum.items()[0][1]:
76+
parsed_cell.append(eval(cell))
77+
rows['rows'].append( dict(zip(rownum.keys(), [parsed_cell])) )
78+
79+
# return header, rows
80+
self.header = header
81+
self.rows = rows
82+
83+
###################################
84+
#
85+
# Flexibility for tables of different sizes.
86+
87+
def check_column_width(self, rowdata):
88+
"""
89+
Compares the cell widths of the row with the curren max cell
90+
width in the global variables. Then updates
91+
``tempcolumnwidths``. ``tempcolumnwidths`` variable maintains
92+
the running max width of each column.
93+
"""
94+
95+
thisrowwidths = [ max([len(line) for line in cell ]) for cell in rowdata]
96+
97+
if len(self.tempcolumnwidths) == 0:
98+
self.tempcolumnwidths.append(thisrowwidths)
99+
else:
100+
currentmaxwidths = self.tempcolumnwidths.pop()
101+
self.tempcolumnwidths.append([ max(currentmaxwidths[i], thisrowwidths[i]) for i in range(len(currentmaxwidths))])
102+
103+
104+
###################################
105+
#
106+
# Building the table representation
107+
108+
def get_row_line(self, delim='-'):
109+
"""
110+
Produces and returns row deliminiters for restructured text tables.
111+
"""
112+
return '+' + delim + str(delim + '+' + delim).join([ delim * width for width in self.columnwidths ]) + delim + '+'
113+
114+
def get_row(self, rowdata):
115+
"""
116+
Returns rows given ``rowdata`` properly formated for restructured text tables.
117+
"""
118+
rowlines = []
119+
for line in zip(*rowdata):
120+
if len(rowlines) > 0:
121+
rowlines.append('\n')
122+
rowlines.append( '| ' + ' | '.join([ line[idx].ljust(self.columnwidths[idx]) for idx in range(len(line)) ]) + ' |' )
123+
124+
return ''.join(rowlines)
125+
126+
def process_table_content(self):
127+
self.tabledata = []
128+
129+
# Compare cell widths of the header with the
130+
# max cell widths stored in the global var tempcolumnwidths
131+
# and swap out value(s) if necessary.
132+
if self.header is not None:
133+
self.check_column_width(self.header)
134+
135+
for index in range(len(self.rows['rows'])):
136+
parsed_row = []
137+
138+
# Append each cell to the parsed_row list, breaking multi-line
139+
# cell data as needed.
140+
for cell in self.rows['rows'][index][index + 1]:
141+
parsed_row.append(cell.split('\n'))
142+
143+
# process the data to ensure the table is big enough.
144+
self.check_column_width(parsed_row)
145+
normalize_cell_height(parsed_row)
146+
147+
# add the processed data to the table
148+
self.tabledata.append(parsed_row)
149+
150+
# Set the global variable columnwidths to the flattened out
151+
# tempcolumnwidths
152+
for cellwidth in self.tempcolumnwidths.pop():
153+
self.columnwidths.append(cellwidth)
92154

93155
###################################
94156
#
@@ -109,114 +171,31 @@ def __getattr__(self, key):
109171

110172
return value
111173

112-
def check_input_data(datafile, layout, meta, content):
113-
if layout != 'layout':
114-
exit('layout document in "' + datafile + '" is malformed.')
115-
elif meta != 'meta':
116-
exit('meta document in "' + datafile + '" is malformed.')
117-
elif content != 'content':
118-
exit('content document in "' + datafile + '" is malformed.')
119-
120-
def read_data(datafile):
121-
with open(datafile, "rt") as f:
122-
parsed = yaml.load_all(f)
123-
layout = dict2obj(parsed.next())
124-
meta = dict2obj(parsed.next())
125-
content = dict2obj(parsed.next())
126-
127-
check_input_data(datafile, layout.section, meta.section, content.section)
128-
129-
rows = { 'rows': [] }
130-
131-
if layout.header:
132-
header = []
133-
for cell in layout.header:
134-
header.append([eval(cell)])
135-
else:
136-
header = None
137-
138-
for rownum in layout.rows:
139-
parsed_cell = []
140-
for cell in rownum.items()[0][1]:
141-
parsed_cell.append(eval(cell))
142-
rows['rows'].append( dict(zip(rownum.keys(), [parsed_cell])) )
143-
144-
return header, rows
145-
146-
def render_table(header, rows):
147-
tabledata = []
148-
149-
# Compare cell widths of the header with the
150-
# max cell widths stored in the global var tempcolumnwidths
151-
# and swap out value(s) if necessary.
152-
if header is not None:
153-
check_column_width(header)
154-
155-
for index in range(len(rows['rows'])):
156-
parsed_row = []
157-
158-
# Append each cell to the parsed_row list, breaking multi-line
159-
# cell data as needed.
160-
for cell in rows['rows'][index][index + 1]:
161-
parsed_row.append(cell.split('\n'))
162-
163-
# process the data to ensure the table is big enough.
164-
check_column_width(parsed_row)
165-
normalize_cell_height(parsed_row)
166-
167-
# add the processed data to the table
168-
tabledata.append(parsed_row)
169-
170-
# Set the global variable columnwidths to the flattened out
171-
# tempcolumnwidths
172-
for cellwidth in tempcolumnwidths.pop():
173-
columnwidths.append(cellwidth)
174-
175-
output = []
176-
output.append(get_row_line())
177-
178-
if header is not None:
179-
output.append(get_row(header))
180-
output.append(get_row_line('='))
181-
182-
for row in tabledata:
183-
output.append(get_row(row))
184-
output.append(get_row_line())
185-
186-
return output
187-
188174
###################################
189175
#
190176
# Interaction
191177

192-
def get_default_outputfile(inputfile):
193-
return inputfile.rsplit('.')[0] + '.rst'
178+
class YamlTableBuilder(RstTable):
179+
def __init__(self, inputfile):
180+
self.inputfile = inputfile
181+
super(YamlTableBuilder, self).__init__(inputfile)
182+
self.output = self.render_table()
194183

195-
def cli():
196-
# this is a total hack to avoid argparse. first argument is input,
197-
# second is output. we'll have to break down and use argparse if
198-
# we want any other options, just for sanity.
184+
def render_table(self):
185+
o = []
186+
o.append(self.get_row_line())
199187

200-
inputfile = sys.argv[1]
188+
if self.header is not None:
189+
o.append(self.get_row(self.header))
190+
o.append(self.get_row_line('='))
201191

202-
try:
203-
outputfile = sys.argv[2]
204-
except IndexError:
205-
outputfile = get_default_outputfile(inputfile)
192+
for self.row in self.tabledata:
193+
o.append(self.get_row(self.row))
194+
o.append(self.get_row_line())
206195

207-
return inputfile, outputfile
196+
return o
208197

209-
###################################
210-
#
211-
# Interfaces.
212-
213-
class YamlTableBuilder(object):
214-
def __init__(self, inputfile):
215-
self.inputfile = inputfile
216-
self.table_header, self.table_rows = read_data(inputfile)
217-
self.output = render_table(self.table_header, self.table_rows)
218-
219-
def write_file(self, outputfile=None):
198+
def write(self, outputfile=None):
220199
if outputfile is None:
221200
outputfile = get_default_outputfile(self.inputfile)
222201

@@ -228,12 +207,24 @@ def print_table(self):
228207
for line in self.output:
229208
print(line)
230209

210+
###################################
211+
#
212+
# Interface.
213+
231214
def main():
232-
inputfile, outputfile = cli()
215+
# the following is a total hack to avoid argparse. first argument
216+
# is input, second is output. we'll have to break down and use
217+
# argparse if we want any other options, just for sanity.
233218

234-
table = YamlTableBuilder(inputfile)
219+
inputfile = sys.argv[1]
220+
221+
try:
222+
outputfile = sys.argv[2]
223+
except IndexError:
224+
outputfile = get_default_outputfile(inputfile)
235225

236-
table.write_file(outputfile)
226+
table = YamlTableBuilder(inputfile)
227+
table.write(outputfile)
237228

238229
if __name__ == '__main__':
239230
main()

0 commit comments

Comments
 (0)