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