diff options
Diffstat (limited to 'logilab/common/table.py')
-rw-r--r-- | logilab/common/table.py | 246 |
1 files changed, 146 insertions, 100 deletions
diff --git a/logilab/common/table.py b/logilab/common/table.py index 1f1101c..e7b9195 100644 --- a/logilab/common/table.py +++ b/logilab/common/table.py @@ -18,6 +18,10 @@ """Table management module.""" from __future__ import print_function +from types import CodeType +from typing import Any, List, Optional, Tuple, Union, Dict, Iterator +from _io import StringIO +from mypy_extensions import NoReturn __docformat__ = "restructuredtext en" @@ -30,51 +34,64 @@ class Table(object): forall(self.data, lambda x: len(x) <= len(self.col_names)) """ - def __init__(self, default_value=0, col_names=None, row_names=None): - self.col_names = [] - self.row_names = [] - self.data = [] - self.default_value = default_value + def __init__(self, default_value: int = 0, col_names: Optional[List[str]] = None, row_names: Optional[Any] = None) -> None: + self.col_names: List = [] + self.row_names: List = [] + self.data: List = [] + self.default_value: int = default_value if col_names: self.create_columns(col_names) if row_names: self.create_rows(row_names) - def _next_row_name(self): + def _next_row_name(self) -> str: return 'row%s' % (len(self.row_names)+1) - def __iter__(self): + def __iter__(self) -> Iterator: return iter(self.data) - def __eq__(self, other): + # def __eq__(self, other: Union[List[List[int]], List[Tuple[str, str, str, float]]]) -> bool: + def __eq__(self, other: object) -> bool: + def is_iterable(variable: Any) -> bool: + try: + iter(variable) + except TypeError: + return False + else: + return True + if other is None: return False + elif is_iterable(other): + # mypy: No overload variant of "list" matches argument type "object" + # checked before + return list(self) == list(other) # type: ignore else: - return list(self) == list(other) + return False __hash__ = object.__hash__ def __ne__(self, other): return not self == other - def __len__(self): + def __len__(self) -> int: return len(self.row_names) ## Rows / Columns creation ################################################# - def create_rows(self, row_names): + def create_rows(self, row_names: List[str]) -> None: """Appends row_names to the list of existing rows """ self.row_names.extend(row_names) for row_name in row_names: self.data.append([self.default_value]*len(self.col_names)) - def create_columns(self, col_names): + def create_columns(self, col_names: List[str]) -> None: """Appends col_names to the list of existing columns """ for col_name in col_names: self.create_column(col_name) - def create_row(self, row_name=None): + def create_row(self, row_name: str = None) -> None: """Creates a rowname to the row_names list """ row_name = row_name or self._next_row_name() @@ -82,7 +99,7 @@ class Table(object): self.data.append([self.default_value]*len(self.col_names)) - def create_column(self, col_name): + def create_column(self, col_name: str) -> None: """Creates a colname to the col_names list """ self.col_names.append(col_name) @@ -90,7 +107,7 @@ class Table(object): row.append(self.default_value) ## Sort by column ########################################################## - def sort_by_column_id(self, col_id, method = 'asc'): + def sort_by_column_id(self, col_id: str, method: str = 'asc') -> None: """Sorts the table (in-place) according to data stored in col_id """ try: @@ -100,7 +117,7 @@ class Table(object): raise KeyError("Col (%s) not found in table" % (col_id)) - def sort_by_column_index(self, col_index, method = 'asc'): + def sort_by_column_index(self, col_index: int, method: str = 'asc') -> None: """Sorts the table 'in-place' according to data stored in col_index method should be in ('asc', 'desc') @@ -119,29 +136,33 @@ class Table(object): self.data.append(row) self.row_names.append(row_name) - def groupby(self, colname, *others): + def groupby(self, colname: str, *others: str) -> Union[Dict[str, Dict[str, 'Table']], + Dict[str, 'Table']]: """builds indexes of data :returns: nested dictionaries pointing to actual rows """ - groups = {} + groups: Dict = {} colnames = (colname,) + others col_indexes = [self.col_names.index(col_id) for col_id in colnames] for row in self.data: ptr = groups for col_index in col_indexes[:-1]: ptr = ptr.setdefault(row[col_index], {}) - ptr = ptr.setdefault(row[col_indexes[-1]], - Table(default_value=self.default_value, - col_names=self.col_names)) - ptr.append_row(tuple(row)) + table = ptr.setdefault(row[col_indexes[-1]], + Table(default_value=self.default_value, + col_names=self.col_names)) + table.append_row(tuple(row)) return groups - def select(self, colname, value): + def select(self, colname: str, value: str) -> 'Table': grouped = self.groupby(colname) try: - return grouped[value] + # mypy: Incompatible return value type (got "Union[Dict[str, Table], Table]", + # mypy: expected "Table") + # I guess we are sure we'll get a Table here? + return grouped[value] # type: ignore except KeyError: - return [] + return Table() def remove(self, colname, value): col_index = self.col_names.index(colname) @@ -151,13 +172,13 @@ class Table(object): ## The 'setter' part ####################################################### - def set_cell(self, row_index, col_index, data): + def set_cell(self, row_index: int, col_index: int, data: int) -> None: """sets value of cell 'row_indew', 'col_index' to data """ self.data[row_index][col_index] = data - def set_cell_by_ids(self, row_id, col_id, data): + def set_cell_by_ids(self, row_id: str, col_id: str, data: Union[int, str]) -> None: """sets value of cell mapped by row_id and col_id to data Raises a KeyError if row_id or col_id are not found in the table """ @@ -173,7 +194,7 @@ class Table(object): raise KeyError("Column (%s) not found in table" % (col_id)) - def set_row(self, row_index, row_data): + def set_row(self, row_index: int, row_data: Union[List[float], List[int], List[str]]) -> None: """sets the 'row_index' row pre:: @@ -183,7 +204,7 @@ class Table(object): self.data[row_index] = row_data - def set_row_by_id(self, row_id, row_data): + def set_row_by_id(self, row_id: str, row_data: List[str]) -> None: """sets the 'row_id' column pre:: @@ -199,7 +220,7 @@ class Table(object): raise KeyError('Row (%s) not found in table' % (row_id)) - def append_row(self, row_data, row_name=None): + def append_row(self, row_data: Union[List[Union[float, str]], List[int]], row_name: Optional[str] = None) -> int: """Appends a row to the table pre:: @@ -211,7 +232,7 @@ class Table(object): self.data.append(row_data) return len(self.data) - 1 - def insert_row(self, index, row_data, row_name=None): + def insert_row(self, index: int, row_data: List[str], row_name: str = None) -> None: """Appends row_data before 'index' in the table. To make 'insert' behave like 'list.insert', inserting in an out of range index will insert row_data to the end of the list @@ -225,7 +246,7 @@ class Table(object): self.data.insert(index, row_data) - def delete_row(self, index): + def delete_row(self, index: int) -> List[str]: """Deletes the 'index' row in the table, and returns it. Raises an IndexError if index is out of range """ @@ -233,7 +254,7 @@ class Table(object): return self.data.pop(index) - def delete_row_by_id(self, row_id): + def delete_row_by_id(self, row_id: str) -> None: """Deletes the 'row_id' row in the table. Raises a KeyError if row_id was not found. """ @@ -244,7 +265,7 @@ class Table(object): raise KeyError('Row (%s) not found in table' % (row_id)) - def set_column(self, col_index, col_data): + def set_column(self, col_index: int, col_data: Union[List[int], range]) -> None: """sets the 'col_index' column pre:: @@ -256,7 +277,7 @@ class Table(object): self.data[row_index][col_index] = cell_data - def set_column_by_id(self, col_id, col_data): + def set_column_by_id(self, col_id: str, col_data: Union[List[int], range]) -> None: """sets the 'col_id' column pre:: @@ -272,7 +293,7 @@ class Table(object): raise KeyError('Column (%s) not found in table' % (col_id)) - def append_column(self, col_data, col_name): + def append_column(self, col_data: range, col_name: str) -> None: """Appends the 'col_index' column pre:: @@ -284,7 +305,7 @@ class Table(object): self.data[row_index].append(cell_data) - def insert_column(self, index, col_data, col_name): + def insert_column(self, index: int, col_data: range, col_name: str) -> None: """Appends col_data before 'index' in the table. To make 'insert' behave like 'list.insert', inserting in an out of range index will insert col_data to the end of the list @@ -298,7 +319,7 @@ class Table(object): self.data[row_index].insert(index, cell_data) - def delete_column(self, index): + def delete_column(self, index: int) -> List[int]: """Deletes the 'index' column in the table, and returns it. Raises an IndexError if index is out of range """ @@ -306,7 +327,7 @@ class Table(object): return [row.pop(index) for row in self.data] - def delete_column_by_id(self, col_id): + def delete_column_by_id(self, col_id: str) -> None: """Deletes the 'col_id' col in the table. Raises a KeyError if col_id was not found. """ @@ -319,53 +340,68 @@ class Table(object): ## The 'getter' part ####################################################### - def get_shape(self): + def get_shape(self) -> Tuple[int, int]: """Returns a tuple which represents the table's shape """ return len(self.row_names), len(self.col_names) shape = property(get_shape) - def __getitem__(self, indices): + def __getitem__(self, indices: Union[Tuple[Union[int, slice, str], Union[int, str]], int, slice]) -> Any: """provided for convenience""" - rows, multirows = None, False - cols, multicols = None, False + multirows: bool = False + multicols: bool = False + + rows: slice + cols: slice + + rows_indice: Union[int, slice, str] + cols_indice: Union[int, str, None] = None + if isinstance(indices, tuple): - rows = indices[0] + rows_indice = indices[0] if len(indices) > 1: - cols = indices[1] + cols_indice = indices[1] else: - rows = indices + rows_indice = indices + # define row slice - if isinstance(rows, str): + if isinstance(rows_indice, str): try: - rows = self.row_names.index(rows) + rows_indice = self.row_names.index(rows_indice) except ValueError: - raise KeyError("Row (%s) not found in table" % (rows)) - if isinstance(rows, int): - rows = slice(rows, rows+1) + raise KeyError("Row (%s) not found in table" % (rows_indice)) + + if isinstance(rows_indice, int): + rows = slice(rows_indice, rows_indice + 1) multirows = False else: rows = slice(None) multirows = True + # define col slice - if isinstance(cols, str): + if isinstance(cols_indice, str): try: - cols = self.col_names.index(cols) + cols_indice = self.col_names.index(cols_indice) except ValueError: - raise KeyError("Column (%s) not found in table" % (cols)) - if isinstance(cols, int): - cols = slice(cols, cols+1) + raise KeyError("Column (%s) not found in table" % (cols_indice)) + + if isinstance(cols_indice, int): + cols = slice(cols_indice, cols_indice + 1) multicols = False else: cols = slice(None) multicols = True + # get sub-table tab = Table() tab.default_value = self.default_value + tab.create_rows(self.row_names[rows]) tab.create_columns(self.col_names[cols]) + for idx, row in enumerate(self.data[rows]): tab.set_row(idx, row[cols]) + if multirows : if multicols: return tab @@ -409,7 +445,7 @@ class Table(object): raise KeyError("Column (%s) not found in table" % (col_id)) return self.get_column(col_index, distinct) - def get_columns(self): + def get_columns(self) -> List[List[int]]: """Returns all the columns in the table """ return [self[:, index] for index in range(len(self.col_names))] @@ -421,14 +457,14 @@ class Table(object): col = list(set(col)) return col - def apply_stylesheet(self, stylesheet): + def apply_stylesheet(self, stylesheet: 'TableStyleSheet') -> None: """Applies the stylesheet to this table """ for instruction in stylesheet.instructions: eval(instruction) - def transpose(self): + def transpose(self) -> 'Table': """Keeps the self object intact, and returns the transposed (rotated) table. """ @@ -440,7 +476,7 @@ class Table(object): return transposed - def pprint(self): + def pprint(self) -> str: """returns a string representing the table in a pretty printed 'text' format. """ @@ -482,7 +518,7 @@ class Table(object): return '\n'.join(lines) - def __repr__(self): + def __repr__(self) -> str: return repr(self.data) def as_text(self): @@ -499,7 +535,7 @@ class TableStyle: """Defines a table's style """ - def __init__(self, table): + def __init__(self, table: Table) -> None: self._table = table self.size = dict([(col_name, '1*') for col_name in table.col_names]) @@ -516,12 +552,12 @@ class TableStyle: self.units['__row_column__'] = '' # XXX FIXME : params order should be reversed for all set() methods - def set_size(self, value, col_id): + def set_size(self, value: str, col_id: str) -> None: """sets the size of the specified col_id to value """ self.size[col_id] = value - def set_size_by_index(self, value, col_index): + def set_size_by_index(self, value: str, col_index: int) -> None: """Allows to set the size according to the column index rather than using the column's id. BE CAREFUL : the '0' column is the '__row_column__' one ! @@ -534,13 +570,13 @@ class TableStyle: self.size[col_id] = value - def set_alignment(self, value, col_id): + def set_alignment(self, value: str, col_id: str) -> None: """sets the alignment of the specified col_id to value """ self.alignment[col_id] = value - def set_alignment_by_index(self, value, col_index): + def set_alignment_by_index(self, value: str, col_index: int) -> None: """Allows to set the alignment according to the column index rather than using the column's id. BE CAREFUL : the '0' column is the '__row_column__' one ! @@ -553,13 +589,13 @@ class TableStyle: self.alignment[col_id] = value - def set_unit(self, value, col_id): + def set_unit(self, value: str, col_id: str) -> None: """sets the unit of the specified col_id to value """ self.units[col_id] = value - def set_unit_by_index(self, value, col_index): + def set_unit_by_index(self, value: str, col_index: int) -> None: """Allows to set the unit according to the column index rather than using the column's id. BE CAREFUL : the '0' column is the '__row_column__' one ! @@ -574,13 +610,13 @@ class TableStyle: self.units[col_id] = value - def get_size(self, col_id): + def get_size(self, col_id: str) -> str: """Returns the size of the specified col_id """ return self.size[col_id] - def get_size_by_index(self, col_index): + def get_size_by_index(self, col_index: int) -> str: """Allows to get the size according to the column index rather than using the column's id. BE CAREFUL : the '0' column is the '__row_column__' one ! @@ -593,13 +629,13 @@ class TableStyle: return self.size[col_id] - def get_alignment(self, col_id): + def get_alignment(self, col_id: str) -> str: """Returns the alignment of the specified col_id """ return self.alignment[col_id] - def get_alignment_by_index(self, col_index): + def get_alignment_by_index(self, col_index: int) -> str: """Allors to get the alignment according to the column index rather than using the column's id. BE CAREFUL : the '0' column is the '__row_column__' one ! @@ -612,13 +648,13 @@ class TableStyle: return self.alignment[col_id] - def get_unit(self, col_id): + def get_unit(self, col_id: str) -> str: """Returns the unit of the specified col_id """ return self.units[col_id] - def get_unit_by_index(self, col_index): + def get_unit_by_index(self, col_index: int) -> str: """Allors to get the unit according to the column index rather than using the column's id. BE CAREFUL : the '0' column is the '__row_column__' one ! @@ -649,28 +685,30 @@ class TableStyleSheet: 2_5 = sqrt(2_3**2 + 2_4**2) """ - def __init__(self, rules = None): + def __init__(self, rules: Optional[List[str]] = None) -> None: rules = rules or [] - self.rules = [] - self.instructions = [] + + self.rules: List[str] = [] + self.instructions: List[CodeType] = [] + for rule in rules: self.add_rule(rule) - def add_rule(self, rule): + def add_rule(self, rule: str) -> None: """Adds a rule to the stylesheet rules """ try: source_code = ['from math import *'] source_code.append(CELL_PROG.sub(r'self.data[\1][\2]', rule)) self.instructions.append(compile('\n'.join(source_code), - 'table.py', 'exec')) + 'table.py', 'exec')) self.rules.append(rule) except SyntaxError: print("Bad Stylesheet Rule : %s [skipped]" % rule) - def add_rowsum_rule(self, dest_cell, row_index, start_col, end_col): + def add_rowsum_rule(self, dest_cell: Tuple[int, int], row_index: int, start_col: int, end_col: int) -> None: """Creates and adds a rule to sum over the row at row_index from start_col to end_col. dest_cell is a tuple of two elements (x,y) of the destination cell @@ -686,7 +724,7 @@ class TableStyleSheet: self.add_rule(rule) - def add_rowavg_rule(self, dest_cell, row_index, start_col, end_col): + def add_rowavg_rule(self, dest_cell: Tuple[int, int], row_index: int, start_col: int, end_col: int) -> None: """Creates and adds a rule to make the row average (from start_col to end_col) dest_cell is a tuple of two elements (x,y) of the destination cell @@ -703,7 +741,7 @@ class TableStyleSheet: self.add_rule(rule) - def add_colsum_rule(self, dest_cell, col_index, start_row, end_row): + def add_colsum_rule(self, dest_cell: Tuple[int, int], col_index: int, start_row: int, end_row: int) -> None: """Creates and adds a rule to sum over the col at col_index from start_row to end_row. dest_cell is a tuple of two elements (x,y) of the destination cell @@ -719,7 +757,7 @@ class TableStyleSheet: self.add_rule(rule) - def add_colavg_rule(self, dest_cell, col_index, start_row, end_row): + def add_colavg_rule(self, dest_cell: Tuple[int, int], col_index: int, start_row: int, end_row: int) -> None: """Creates and adds a rule to make the col average (from start_row to end_row) dest_cell is a tuple of two elements (x,y) of the destination cell @@ -741,7 +779,7 @@ class TableCellRenderer: """Defines a simple text renderer """ - def __init__(self, **properties): + def __init__(self, **properties: Any) -> None: """keywords should be properties with an associated boolean as value. For example : renderer = TableCellRenderer(units = True, alignment = False) @@ -752,7 +790,7 @@ class TableCellRenderer: self.properties = properties - def render_cell(self, cell_coord, table, table_style): + def render_cell(self, cell_coord: Tuple[int, int], table: Table, table_style: TableStyle) -> Union[str, int]: """Renders the cell at 'cell_coord' in the table, using table_style """ row_index, col_index = cell_coord @@ -763,14 +801,14 @@ class TableCellRenderer: table_style, col_index + 1) - def render_row_cell(self, row_name, table, table_style): + def render_row_cell(self, row_name: str, table: Table, table_style: TableStyle) -> Union[str, int]: """Renders the cell for 'row_id' row """ cell_value = row_name return self._render_cell_content(cell_value, table_style, 0) - def render_col_cell(self, col_name, table, table_style): + def render_col_cell(self, col_name: str, table: Table, table_style: TableStyle) -> Union[str, int]: """Renders the cell for 'col_id' row """ cell_value = col_name @@ -779,7 +817,7 @@ class TableCellRenderer: - def _render_cell_content(self, content, table_style, col_index): + def _render_cell_content(self, content: Union[str, int], table_style: TableStyle, col_index: int) -> Union[str, int]: """Makes the appropriate rendering for this cell content. Rendering properties will be searched using the *table_style.get_xxx_by_index(col_index)' methods @@ -789,11 +827,12 @@ class TableCellRenderer: return content - def _make_cell_content(self, cell_content, table_style, col_index): + def _make_cell_content(self, cell_content: int, table_style: TableStyle, col_index: int) -> Union[int, str]: """Makes the cell content (adds decoration data, like units for example) """ - final_content = cell_content + final_content: Union[int, str] = cell_content + if 'skip_zero' in self.properties: replacement_char = self.properties['skip_zero'] else: @@ -812,7 +851,7 @@ class TableCellRenderer: return final_content - def _add_unit(self, cell_content, table_style, col_index): + def _add_unit(self, cell_content: int, table_style: TableStyle, col_index: int) -> str: """Adds unit to the cell_content if needed """ unit = table_style.get_unit_by_index(col_index) @@ -824,7 +863,7 @@ class DocbookRenderer(TableCellRenderer): """Defines how to render a cell for a docboook table """ - def define_col_header(self, col_index, table_style): + def define_col_header(self, col_index: int, table_style: TableStyle) -> str: """Computes the colspec element according to the style """ size = table_style.get_size_by_index(col_index) @@ -832,7 +871,7 @@ class DocbookRenderer(TableCellRenderer): (col_index, size) - def _render_cell_content(self, cell_content, table_style, col_index): + def _render_cell_content(self, cell_content: Union[int, str], table_style: TableStyle, col_index: int) -> str: """Makes the appropriate rendering for this cell content. Rendering properties will be searched using the table_style.get_xxx_by_index(col_index)' methods. @@ -847,17 +886,20 @@ class DocbookRenderer(TableCellRenderer): # KeyError <=> Default alignment return "<entry>%s</entry>\n" % cell_content + # XXX really? + return "" + class TableWriter: """A class to write tables """ - def __init__(self, stream, table, style, **properties): + def __init__(self, stream: StringIO, table: Table, style: Optional[Any], **properties: Any) -> None: self._stream = stream self.style = style or TableStyle(table) self._table = table self.properties = properties - self.renderer = None + self.renderer: Optional[DocbookRenderer] = None def set_style(self, style): @@ -866,7 +908,7 @@ class TableWriter: self.style = style - def set_renderer(self, renderer): + def set_renderer(self, renderer: DocbookRenderer) -> None: """sets the way to render cell """ self.renderer = renderer @@ -878,7 +920,7 @@ class TableWriter: self.properties.update(properties) - def write_table(self, title = ""): + def write_table(self, title: str = "") -> None: """Writes the table """ raise NotImplementedError("write_table must be implemented !") @@ -889,9 +931,11 @@ class DocbookTableWriter(TableWriter): """Defines an implementation of TableWriter to write a table in Docbook """ - def _write_headers(self): + def _write_headers(self) -> None: """Writes col headers """ + assert self.renderer is not None + # Define col_headers (colstpec elements) for col_index in range(len(self._table.col_names)+1): self._stream.write(self.renderer.define_col_header(col_index, @@ -908,9 +952,11 @@ class DocbookTableWriter(TableWriter): self._stream.write("</row>\n</thead>\n") - def _write_body(self): + def _write_body(self) -> None: """Writes the table body """ + assert self.renderer is not None + self._stream.write('<tbody>\n') for row_index, row in enumerate(self._table.data): @@ -931,7 +977,7 @@ class DocbookTableWriter(TableWriter): self._stream.write('</tbody>\n') - def write_table(self, title = ""): + def write_table(self, title: str = "") -> None: """Writes the table """ self._stream.write('<table>\n<title>%s></title>\n'%(title)) |