1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 """Table management module.
19
20
21
22
23 """
24 __docformat__ = "restructuredtext en"
25
26 from logilab.common.compat import enumerate, sum, set
27
29 """Table defines a data table with column and row names.
30 inv:
31 len(self.data) <= len(self.row_names)
32 forall(self.data, lambda x: len(x) <= len(self.col_names))
33 """
34
35 - def __init__(self, default_value=0, col_names=None, row_names=None):
36 self.col_names = []
37 self.row_names = []
38 self.data = []
39 self.default_value = default_value
40 if col_names:
41 self.create_columns(col_names)
42 if row_names:
43 self.create_rows(row_names)
44
46 return 'row%s' % (len(self.row_names)+1)
47
49 return iter(self.data)
50
52 if other is None:
53 return False
54 else:
55 return list(self) == list(other)
56
58 return not self == other
59
61 return len(self.row_names)
62
63
65 """Appends row_names to the list of existing rows
66 """
67 self.row_names.extend(row_names)
68 for row_name in row_names:
69 self.data.append([self.default_value]*len(self.col_names))
70
72 """Appends col_names to the list of existing columns
73 """
74 for col_name in col_names:
75 self.create_column(col_name)
76
78 """Creates a rowname to the row_names list
79 """
80 row_name = row_name or self._next_row_name()
81 self.row_names.append(row_name)
82 self.data.append([self.default_value]*len(self.col_names))
83
84
86 """Creates a colname to the col_names list
87 """
88 self.col_names.append(col_name)
89 for row in self.data:
90 row.append(self.default_value)
91
92
94 """Sorts the table (in-place) according to data stored in col_id
95 """
96 try:
97 col_index = self.col_names.index(col_id)
98 self.sort_by_column_index(col_index, method)
99 except ValueError:
100 raise KeyError("Col (%s) not found in table" % (col_id))
101
102
104 """Sorts the table 'in-place' according to data stored in col_index
105
106 method should be in ('asc', 'desc')
107 """
108 sort_list = [(row[col_index], row, row_name)
109 for row, row_name in zip(self.data, self.row_names)]
110
111 sort_list.sort()
112
113 if method.lower() == 'desc':
114 sort_list.reverse()
115
116
117 self.data = []
118 self.row_names = []
119 for val, row, row_name in sort_list:
120 self.data.append(row)
121 self.row_names.append(row_name)
122
123 - def groupby(self, colname, *others):
124 """builds indexes of data
125 :returns: nested dictionaries pointing to actual rows
126 """
127 groups = {}
128 colnames = (colname,) + others
129 col_indexes = [self.col_names.index(col_id) for col_id in colnames]
130 for row in self.data:
131 ptr = groups
132 for col_index in col_indexes[:-1]:
133 ptr = ptr.setdefault(row[col_index], {})
134 ptr = ptr.setdefault(row[col_indexes[-1]],
135 Table(default_value=self.default_value,
136 col_names=self.col_names))
137 ptr.append_row(tuple(row))
138 return groups
139
140 - def select(self, colname, value):
141 grouped = self.groupby(colname)
142 try:
143 return grouped[value]
144 except KeyError:
145 return []
146
147 - def remove(self, colname, value):
148 col_index = self.col_names.index(colname)
149 for row in self.data[:]:
150 if row[col_index] == value:
151 self.data.remove(row)
152
153
154
155 - def set_cell(self, row_index, col_index, data):
156 """sets value of cell 'row_indew', 'col_index' to data
157 """
158 self.data[row_index][col_index] = data
159
160
162 """sets value of cell mapped by row_id and col_id to data
163 Raises a KeyError if row_id or col_id are not found in the table
164 """
165 try:
166 row_index = self.row_names.index(row_id)
167 except ValueError:
168 raise KeyError("Row (%s) not found in table" % (row_id))
169 else:
170 try:
171 col_index = self.col_names.index(col_id)
172 self.data[row_index][col_index] = data
173 except ValueError:
174 raise KeyError("Column (%s) not found in table" % (col_id))
175
176
177 - def set_row(self, row_index, row_data):
178 """sets the 'row_index' row
179 pre:
180 type(row_data) == types.ListType
181 len(row_data) == len(self.col_names)
182 """
183 self.data[row_index] = row_data
184
185
187 """sets the 'row_id' column
188 pre:
189 type(row_data) == types.ListType
190 len(row_data) == len(self.row_names)
191 Raises a KeyError if row_id is not found
192 """
193 try:
194 row_index = self.row_names.index(row_id)
195 self.set_row(row_index, row_data)
196 except ValueError:
197 raise KeyError('Row (%s) not found in table' % (row_id))
198
199
201 """Appends a row to the table
202 pre:
203 type(row_data) == types.ListType
204 len(row_data) == len(self.col_names)
205 """
206 row_name = row_name or self._next_row_name()
207 self.row_names.append(row_name)
208 self.data.append(row_data)
209 return len(self.data) - 1
210
211 - def insert_row(self, index, row_data, row_name=None):
212 """Appends row_data before 'index' in the table. To make 'insert'
213 behave like 'list.insert', inserting in an out of range index will
214 insert row_data to the end of the list
215 pre:
216 type(row_data) == types.ListType
217 len(row_data) == len(self.col_names)
218 """
219 row_name = row_name or self._next_row_name()
220 self.row_names.insert(index, row_name)
221 self.data.insert(index, row_data)
222
223
225 """Deletes the 'index' row in the table, and returns it.
226 Raises an IndexError if index is out of range
227 """
228 self.row_names.pop(index)
229 return self.data.pop(index)
230
231
233 """Deletes the 'row_id' row in the table.
234 Raises a KeyError if row_id was not found.
235 """
236 try:
237 row_index = self.row_names.index(row_id)
238 self.delete_row(row_index)
239 except ValueError:
240 raise KeyError('Row (%s) not found in table' % (row_id))
241
242
244 """sets the 'col_index' column
245 pre:
246 type(col_data) == types.ListType
247 len(col_data) == len(self.row_names)
248 """
249
250 for row_index, cell_data in enumerate(col_data):
251 self.data[row_index][col_index] = cell_data
252
253
255 """sets the 'col_id' column
256 pre:
257 type(col_data) == types.ListType
258 len(col_data) == len(self.col_names)
259 Raises a KeyError if col_id is not found
260 """
261 try:
262 col_index = self.col_names.index(col_id)
263 self.set_column(col_index, col_data)
264 except ValueError:
265 raise KeyError('Column (%s) not found in table' % (col_id))
266
267
269 """Appends the 'col_index' column
270 pre:
271 type(col_data) == types.ListType
272 len(col_data) == len(self.row_names)
273 """
274 self.col_names.append(col_name)
275 for row_index, cell_data in enumerate(col_data):
276 self.data[row_index].append(cell_data)
277
278
280 """Appends col_data before 'index' in the table. To make 'insert'
281 behave like 'list.insert', inserting in an out of range index will
282 insert col_data to the end of the list
283 pre:
284 type(col_data) == types.ListType
285 len(col_data) == len(self.row_names)
286 """
287 self.col_names.insert(index, col_name)
288 for row_index, cell_data in enumerate(col_data):
289 self.data[row_index].insert(index, cell_data)
290
291
293 """Deletes the 'index' column in the table, and returns it.
294 Raises an IndexError if index is out of range
295 """
296 self.col_names.pop(index)
297 return [row.pop(index) for row in self.data]
298
299
301 """Deletes the 'col_id' col in the table.
302 Raises a KeyError if col_id was not found.
303 """
304 try:
305 col_index = self.col_names.index(col_id)
306 self.delete_column(col_index)
307 except ValueError:
308 raise KeyError('Column (%s) not found in table' % (col_id))
309
310
311
312
314 """Returns a tuple which represents the table's shape
315 """
316 return len(self.row_names), len(self.col_names)
317 shape = property(get_shape)
318
320 """provided for convenience"""
321 rows, multirows = None, False
322 cols, multicols = None, False
323 if isinstance(indices, tuple):
324 rows = indices[0]
325 if len(indices) > 1:
326 cols = indices[1]
327 else:
328 rows = indices
329
330 if isinstance(rows,str):
331 try:
332 rows = self.row_names.index(rows)
333 except ValueError:
334 raise KeyError("Row (%s) not found in table" % (rows))
335 if isinstance(rows,int):
336 rows = slice(rows,rows+1)
337 multirows = False
338 else:
339 rows = slice(None)
340 multirows = True
341
342 if isinstance(cols,str):
343 try:
344 cols = self.col_names.index(cols)
345 except ValueError:
346 raise KeyError("Column (%s) not found in table" % (cols))
347 if isinstance(cols,int):
348 cols = slice(cols,cols+1)
349 multicols = False
350 else:
351 cols = slice(None)
352 multicols = True
353
354 tab = Table()
355 tab.default_value = self.default_value
356 tab.create_rows(self.row_names[rows])
357 tab.create_columns(self.col_names[cols])
358 for idx,row in enumerate(self.data[rows]):
359 tab.set_row(idx, row[cols])
360 if multirows :
361 if multicols:
362 return tab
363 else:
364 return [item[0] for item in tab.data]
365 else:
366 if multicols:
367 return tab.data[0]
368 else:
369 return tab.data[0][0]
370
372 """Returns the element at [row_id][col_id]
373 """
374 try:
375 row_index = self.row_names.index(row_id)
376 except ValueError:
377 raise KeyError("Row (%s) not found in table" % (row_id))
378 else:
379 try:
380 col_index = self.col_names.index(col_id)
381 except ValueError:
382 raise KeyError("Column (%s) not found in table" % (col_id))
383 return self.data[row_index][col_index]
384
386 """Returns the 'row_id' row
387 """
388 try:
389 row_index = self.row_names.index(row_id)
390 except ValueError:
391 raise KeyError("Row (%s) not found in table" % (row_id))
392 return self.data[row_index]
393
395 """Returns the 'col_id' col
396 """
397 try:
398 col_index = self.col_names.index(col_id)
399 except ValueError:
400 raise KeyError("Column (%s) not found in table" % (col_id))
401 return self.get_column(col_index, distinct)
402
404 """Returns all the columns in the table
405 """
406 return [self[:,index] for index in range(len(self.col_names))]
407
409 """get a column by index"""
410 col = [row[col_index] for row in self.data]
411 if distinct:
412 col = list(set(col))
413 return col
414
416 """Applies the stylesheet to this table
417 """
418 for instruction in stylesheet.instructions:
419 eval(instruction)
420
421
423 """Keeps the self object intact, and returns the transposed (rotated)
424 table.
425 """
426 transposed = Table()
427 transposed.create_rows(self.col_names)
428 transposed.create_columns(self.row_names)
429 for col_index, column in enumerate(self.get_columns()):
430 transposed.set_row(col_index, column)
431 return transposed
432
433
435 """returns a string representing the table in a pretty
436 printed 'text' format.
437 """
438
439 max_row_name = 0
440 for row_name in self.row_names:
441 if len(row_name) > max_row_name:
442 max_row_name = len(row_name)
443 col_start = max_row_name + 5
444
445 lines = []
446
447
448 col_names_line = [' '*col_start]
449 for col_name in self.col_names:
450 col_names_line.append(col_name.encode('iso-8859-1') + ' '*5)
451 lines.append('|' + '|'.join(col_names_line) + '|')
452 max_line_length = len(lines[0])
453
454
455 for row_index, row in enumerate(self.data):
456 line = []
457
458 row_name = self.row_names[row_index].encode('iso-8859-1')
459 line.append(row_name + ' '*(col_start-len(row_name)))
460
461
462 for col_index, cell in enumerate(row):
463 col_name_length = len(self.col_names[col_index]) + 5
464 data = str(cell)
465 line.append(data + ' '*(col_name_length - len(data)))
466 lines.append('|' + '|'.join(line) + '|')
467 if len(lines[-1]) > max_line_length:
468 max_line_length = len(lines[-1])
469
470
471 lines.insert(0, '-'*max_line_length)
472 lines.append('-'*max_line_length)
473 return '\n'.join(lines)
474
475
477 return repr(self.data)
478
480 data = []
481
482 for row in self.data:
483 data.append([str(cell) for cell in row])
484 lines = ['\t'.join(row) for row in data]
485 return '\n'.join(lines)
486
487
488
490 """Defines a table's style
491 """
492
494
495 self._table = table
496 self.size = dict([(col_name,'1*') for col_name in table.col_names])
497
498
499 self.size['__row_column__'] = '1*'
500 self.alignment = dict([(col_name,'right')
501 for col_name in table.col_names])
502 self.alignment['__row_column__'] = 'right'
503
504
505
506 self.units = dict([(col_name,'') for col_name in table.col_names])
507 self.units['__row_column__'] = ''
508
509
511 """sets the size of the specified col_id to value
512 """
513 self.size[col_id] = value
514
516 """Allows to set the size according to the column index rather than
517 using the column's id.
518 BE CAREFUL : the '0' column is the '__row_column__' one !
519 """
520 if col_index == 0:
521 col_id = '__row_column__'
522 else:
523 col_id = self._table.col_names[col_index-1]
524
525 self.size[col_id] = value
526
527
529 """sets the alignment of the specified col_id to value
530 """
531 self.alignment[col_id] = value
532
533
535 """Allows to set the alignment according to the column index rather than
536 using the column's id.
537 BE CAREFUL : the '0' column is the '__row_column__' one !
538 """
539 if col_index == 0:
540 col_id = '__row_column__'
541 else:
542 col_id = self._table.col_names[col_index-1]
543
544 self.alignment[col_id] = value
545
546
548 """sets the unit of the specified col_id to value
549 """
550 self.units[col_id] = value
551
552
554 """Allows to set the unit according to the column index rather than
555 using the column's id.
556 BE CAREFUL : the '0' column is the '__row_column__' one !
557 (Note that in the 'unit' case, you shouldn't have to set a unit
558 for the 1st column (the __row__column__ one))
559 """
560 if col_index == 0:
561 col_id = '__row_column__'
562 else:
563 col_id = self._table.col_names[col_index-1]
564
565 self.units[col_id] = value
566
567
569 """Returns the size of the specified col_id
570 """
571 return self.size[col_id]
572
573
575 """Allows to get the size according to the column index rather than
576 using the column's id.
577 BE CAREFUL : the '0' column is the '__row_column__' one !
578 """
579 if col_index == 0:
580 col_id = '__row_column__'
581 else:
582 col_id = self._table.col_names[col_index-1]
583
584 return self.size[col_id]
585
586
588 """Returns the alignment of the specified col_id
589 """
590 return self.alignment[col_id]
591
592
594 """Allors to get the alignment according to the column index rather than
595 using the column's id.
596 BE CAREFUL : the '0' column is the '__row_column__' one !
597 """
598 if col_index == 0:
599 col_id = '__row_column__'
600 else:
601 col_id = self._table.col_names[col_index-1]
602
603 return self.alignment[col_id]
604
605
607 """Returns the unit of the specified col_id
608 """
609 return self.units[col_id]
610
611
613 """Allors to get the unit according to the column index rather than
614 using the column's id.
615 BE CAREFUL : the '0' column is the '__row_column__' one !
616 """
617 if col_index == 0:
618 col_id = '__row_column__'
619 else:
620 col_id = self._table.col_names[col_index-1]
621
622 return self.units[col_id]
623
624
625 import re
626 CELL_PROG = re.compile("([0-9]+)_([0-9]+)")
627
629 """A simple Table stylesheet
630 Rules are expressions where cells are defined by the row_index
631 and col_index separated by an underscore ('_').
632 For example, suppose you want to say that the (2,5) cell must be
633 the sum of its two preceding cells in the row, you would create
634 the following rule :
635 2_5 = 2_3 + 2_4
636 You can also use all the math.* operations you want. For example:
637 2_5 = sqrt(2_3**2 + 2_4**2)
638 """
639
641 rules = rules or []
642 self.rules = []
643 self.instructions = []
644 for rule in rules:
645 self.add_rule(rule)
646
647
649 """Adds a rule to the stylesheet rules
650 """
651 try:
652 source_code = ['from math import *']
653 source_code.append(CELL_PROG.sub(r'self.data[\1][\2]', rule))
654 self.instructions.append(compile('\n'.join(source_code),
655 'table.py', 'exec'))
656 self.rules.append(rule)
657 except SyntaxError:
658 print "Bad Stylesheet Rule : %s [skipped]"%rule
659
660
662 """Creates and adds a rule to sum over the row at row_index from
663 start_col to end_col.
664 dest_cell is a tuple of two elements (x,y) of the destination cell
665 No check is done for indexes ranges.
666 pre:
667 start_col >= 0
668 end_col > start_col
669 """
670 cell_list = ['%d_%d'%(row_index, index) for index in range(start_col,
671 end_col + 1)]
672 rule = '%d_%d=' % dest_cell + '+'.join(cell_list)
673 self.add_rule(rule)
674
675
677 """Creates and adds a rule to make the row average (from start_col
678 to end_col)
679 dest_cell is a tuple of two elements (x,y) of the destination cell
680 No check is done for indexes ranges.
681 pre:
682 start_col >= 0
683 end_col > start_col
684 """
685 cell_list = ['%d_%d'%(row_index, index) for index in range(start_col,
686 end_col + 1)]
687 num = (end_col - start_col + 1)
688 rule = '%d_%d=' % dest_cell + '('+'+'.join(cell_list)+')/%f'%num
689 self.add_rule(rule)
690
691
693 """Creates and adds a rule to sum over the col at col_index from
694 start_row to end_row.
695 dest_cell is a tuple of two elements (x,y) of the destination cell
696 No check is done for indexes ranges.
697 pre:
698 start_row >= 0
699 end_row > start_row
700 """
701 cell_list = ['%d_%d'%(index, col_index) for index in range(start_row,
702 end_row + 1)]
703 rule = '%d_%d=' % dest_cell + '+'.join(cell_list)
704 self.add_rule(rule)
705
706
708 """Creates and adds a rule to make the col average (from start_row
709 to end_row)
710 dest_cell is a tuple of two elements (x,y) of the destination cell
711 No check is done for indexes ranges.
712 pre:
713 start_row >= 0
714 end_row > start_row
715 """
716 cell_list = ['%d_%d'%(index, col_index) for index in range(start_row,
717 end_row + 1)]
718 num = (end_row - start_row + 1)
719 rule = '%d_%d=' % dest_cell + '('+'+'.join(cell_list)+')/%f'%num
720 self.add_rule(rule)
721
722
723
725 """Defines a simple text renderer
726 """
727
729 """keywords should be properties with an associated boolean as value.
730 For example :
731 renderer = TableCellRenderer(units = True, alignment = False)
732 An unspecified property will have a 'False' value by default.
733 Possible properties are :
734 alignment, unit
735 """
736 self.properties = properties
737
738
739 - def render_cell(self, cell_coord, table, table_style):
740 """Renders the cell at 'cell_coord' in the table, using table_style
741 """
742 row_index, col_index = cell_coord
743 cell_value = table.data[row_index][col_index]
744 final_content = self._make_cell_content(cell_value,
745 table_style, col_index +1)
746 return self._render_cell_content(final_content,
747 table_style, col_index + 1)
748
749
751 """Renders the cell for 'row_id' row
752 """
753 cell_value = row_name.encode('iso-8859-1')
754 return self._render_cell_content(cell_value, table_style, 0)
755
756
758 """Renders the cell for 'col_id' row
759 """
760 cell_value = col_name.encode('iso-8859-1')
761 col_index = table.col_names.index(col_name)
762 return self._render_cell_content(cell_value, table_style, col_index +1)
763
764
765
766 - def _render_cell_content(self, content, table_style, col_index):
767 """Makes the appropriate rendering for this cell content.
768 Rendering properties will be searched using the
769 *table_style.get_xxx_by_index(col_index)' methods
770
771 **This method should be overridden in the derived renderer classes.**
772 """
773 return content
774
775
776 - def _make_cell_content(self, cell_content, table_style, col_index):
777 """Makes the cell content (adds decoration data, like units for
778 example)
779 """
780 final_content = cell_content
781 if 'skip_zero' in self.properties:
782 replacement_char = self.properties['skip_zero']
783 else:
784 replacement_char = 0
785 if replacement_char and final_content == 0:
786 return replacement_char
787
788 try:
789 units_on = self.properties['units']
790 if units_on:
791 final_content = self._add_unit(
792 cell_content, table_style, col_index)
793 except KeyError:
794 pass
795
796 return final_content
797
798
799 - def _add_unit(self, cell_content, table_style, col_index):
800 """Adds unit to the cell_content if needed
801 """
802 unit = table_style.get_unit_by_index(col_index)
803 return str(cell_content) + " " + unit
804
805
806
808 """Defines how to render a cell for a docboook table
809 """
810
812 """Computes the colspec element according to the style
813 """
814 size = table_style.get_size_by_index(col_index)
815 return '<colspec colname="c%d" colwidth="%s"/>\n' % \
816 (col_index, size)
817
818
819 - def _render_cell_content(self, cell_content, table_style, col_index):
820 """Makes the appropriate rendering for this cell content.
821 Rendering properties will be searched using the
822 table_style.get_xxx_by_index(col_index)' methods.
823 """
824 try:
825 align_on = self.properties['alignment']
826 alignment = table_style.get_alignment_by_index(col_index)
827 if align_on:
828 return "<entry align='%s'>%s</entry>\n" % \
829 (alignment, cell_content)
830 except KeyError:
831
832 return "<entry>%s</entry>\n" % cell_content
833
834
836 """A class to write tables
837 """
838
839 - def __init__(self, stream, table, style, **properties):
840 self._stream = stream
841 self.style = style or TableStyle(table)
842 self._table = table
843 self.properties = properties
844 self.renderer = None
845
846
848 """sets the table's associated style
849 """
850 self.style = style
851
852
854 """sets the way to render cell
855 """
856 self.renderer = renderer
857
858
860 """Updates writer's properties (for cell rendering)
861 """
862 self.properties.update(properties)
863
864
866 """Writes the table
867 """
868 raise NotImplementedError("write_table must be implemented !")
869
870
871
873 """Defines an implementation of TableWriter to write a table in Docbook
874 """
875
877 """Writes col headers
878 """
879
880 for col_index in range(len(self._table.col_names)+1):
881 self._stream.write(self.renderer.define_col_header(col_index,
882 self.style))
883
884 self._stream.write("<thead>\n<row>\n")
885
886 self._stream.write('<entry></entry>\n')
887 for col_name in self._table.col_names:
888 self._stream.write(self.renderer.render_col_cell(
889 col_name, self._table,
890 self.style))
891
892 self._stream.write("</row>\n</thead>\n")
893
894
895 - def _write_body(self):
896 """Writes the table body
897 """
898 self._stream.write('<tbody>\n')
899
900 for row_index, row in enumerate(self._table.data):
901 self._stream.write('<row>\n')
902 row_name = self._table.row_names[row_index]
903
904 self._stream.write(self.renderer.render_row_cell(row_name,
905 self._table,
906 self.style))
907
908 for col_index, cell in enumerate(row):
909 self._stream.write(self.renderer.render_cell(
910 (row_index, col_index),
911 self._table, self.style))
912
913 self._stream.write('</row>\n')
914
915 self._stream.write('</tbody>\n')
916
917
919 """Writes the table
920 """
921 self._stream.write('<table>\n<title>%s></title>\n'%(title))
922 self._stream.write(
923 '<tgroup cols="%d" align="left" colsep="1" rowsep="1">\n'%
924 (len(self._table.col_names)+1))
925 self._write_headers()
926 self._write_body()
927
928 self._stream.write('</tgroup>\n</table>\n')
929