def __init__(self, shape): self.dict_grid = DictGrid(shape) # Undo and redo management self.unredo = UnRedo() self.dict_grid.cell_attributes.unredo = self.unredo # Safe mode self.safe_mode = False
class DataArray(object): """DataArray provides enhanced grid read/write access. Enhancements comprise: * Slicing * Multi-dimensional operations such as insertion and deletion along 1 axis * Undo/redo operations This class represents layer 2 of the model. Parameters ---------- shape: n-tuple of integer \tShape of the grid """ def __init__(self, shape): self.dict_grid = DictGrid(shape) # Undo and redo management self.unredo = UnRedo() self.dict_grid.cell_attributes.unredo = self.unredo # Safe mode self.safe_mode = False def __eq__(self, other): if not hasattr(other, "dict_grid") or \ not hasattr(other, "cell_attributes"): return NotImplemented return self.dict_grid == other.dict_grid and \ self.cell_attributes == other.cell_attributes def __ne__(self, other): return not self.__eq__(other) # Data is the central content interface for loading / saving data. # It shall be used for loading and saving from and to pys and other files. # It shall be used for loading and saving macros. # It is not used for importinf and exporting data because these operations # are partial to the grid. def _get_data(self): """Returns dict of data content. Keys ---- shape: 3-tuple of Integer \tGrid shape grid: Dict of 3-tuples to strings \tCell content attributes: List of 3-tuples \tCell attributes row_heights: Dict of 2-tuples to float \t(row, tab): row_height col_widths: Dict of 2-tuples to float \t(col, tab): col_width macros: String \tMacros from macro list """ data = {} data["shape"] = self.shape data["grid"] = {}.update(self.dict_grid) data["attributes"] = [ca for ca in self.cell_attributes] data["row_heights"] = self.row_heights data["col_widths"] = self.col_widths data["macros"] = self.macros return data def _set_data(self, **kwargs): """Sets data from given parameters Old values are deleted. If a paremeter is not given, nothing is changed. Parameters ---------- shape: 3-tuple of Integer \tGrid shape grid: Dict of 3-tuples to strings \tCell content attributes: List of 3-tuples \tCell attributes row_heights: Dict of 2-tuples to float \t(row, tab): row_height col_widths: Dict of 2-tuples to float \t(col, tab): col_width macros: String \tMacros from macro list """ if "shape" in kwargs: self.shape = kwargs["shape"] if "grid" in kwargs: self.dict_grid.clear() self.dict_grid.update(kwargs["grid"]) if "attributes" in kwargs: self.attributes[:] = kwargs["attributes"] if "row_heights" in kwargs: self.row_heights = kwargs["row_heights"] if "col_widths" in kwargs: self.col_widths = kwargs["col_widths"] if "macros" in kwargs: self.macros = kwargs["macros"] data = property(_get_data, _set_data) def get_row_height(self, row, tab): """Returns row height""" try: return self.row_heights[(row, tab)] except KeyError: return config["default_row_height"] def get_col_width(self, col, tab): """Returns column width""" try: return self.col_widths[(col, tab)] except KeyError: return config["default_col_width"] # Row and column attributes mask # Keys have the format (row, table) def _get_row_heights(self): """Returns row_heights dict""" return self.dict_grid.row_heights def _set_row_heights(self, row_heights): """Sets macros string""" self.dict_grid.row_heights = row_heights row_heights = property(_get_row_heights, _set_row_heights) def _get_col_widths(self): """Returns col_widths dict""" return self.dict_grid.col_widths def _set_col_widths(self, col_widths): """Sets macros string""" self.dict_grid.col_widths = col_widths col_widths = property(_get_col_widths, _set_col_widths) # Cell attributes mask def _get_cell_attributes(self): """Returns cell_attributes list""" return self.dict_grid.cell_attributes def _set_cell_attributes(self, value): """Setter for cell_atributes""" # Empty cell_attributes first self.cell_attributes[:] = [] self.cell_attributes.extend(value) cell_attributes = attributes = \ property(_get_cell_attributes, _set_cell_attributes) def __iter__(self): """Returns iterator over self.dict_grid""" return iter(self.dict_grid) def _get_macros(self): """Returns macros string""" return self.dict_grid.macros def _set_macros(self, macros): """Sets macros string""" self.dict_grid.macros = macros macros = property(_get_macros, _set_macros) def keys(self): """Returns keys in self.dict_grid""" return self.dict_grid.keys() def pop(self, key, mark_unredo=True): """Pops dict_grid with undo and redo support Parameters ---------- key: 3-tuple of Integer \tCell key that shall be popped mark_unredo: Boolean, defaults to True \tIf True then an unredo marker is set after the operation """ result = self.dict_grid.pop(key) # UnRedo support if mark_unredo: self.unredo.mark() undo_operation = (self.__setitem__, [key, result, mark_unredo]) redo_operation = (self.pop, [key, mark_unredo]) self.unredo.append(undo_operation, redo_operation) if mark_unredo: self.unredo.mark() # End UnRedo support return result # Shape mask def _get_shape(self): """Returns dict_grid shape""" return self.dict_grid.shape def _set_shape(self, shape, mark_unredo=True): """Deletes all cells beyond new shape and sets dict_grid shape Parameters ---------- shape: 3-tuple of Integer \tTarget shape for grid mark_unredo: Boolean, defaults to True \tIf True then an unredo marker is set after the operation """ # Delete each cell that is beyond new borders old_shape = self.shape if any(new_axis < old_axis for new_axis, old_axis in zip(shape, old_shape)): for key in self.dict_grid.keys(): if any(key_ele >= new_axis for key_ele, new_axis in zip(key, shape)): self.pop(key) # Set dict_grid shape attribute self.dict_grid.shape = shape # UnRedo support undo_operation = (self._set_shape, [old_shape, mark_unredo]) redo_operation = (self._set_shape, [shape, mark_unredo]) self.unredo.append(undo_operation, redo_operation) if mark_unredo: self.unredo.mark() # End UnRedo support shape = property(_get_shape, _set_shape) def get_last_filled_cell(self, table=None): """Returns key for the bottommost rightmost cell with content Parameters ---------- table: Integer, defaults to None \tLimit search to this table """ maxrow = 0 maxcol = 0 for row, col, tab in self.dict_grid: if table is None or tab == table: maxrow = max(row, maxrow) maxcol = max(col, maxcol) return maxrow, maxcol, table # Pickle support def __getstate__(self): """Returns dict_grid for pickling Note that all persistent data is contained in the DictGrid class """ return {"dict_grid": self.dict_grid} # Slice support def __getitem__(self, key): """Adds slicing access to cell code retrieval The cells are returned as a generator of generators, of ... of unicode. Parameters ---------- key: n-tuple of integer or slice \tKeys of the cell code that is returned Note ---- Classical Excel type addressing (A$1, ...) may be added here """ for key_ele in key: if is_slice_like(key_ele): # We have something slice-like here return self.cell_array_generator(key) elif is_string_like(key_ele): # We have something string-like here msg = "Cell string based access not implemented" raise NotImplementedError(msg) # key_ele should be a single cell return self.dict_grid[key] def __setitem__(self, key, value, mark_unredo=True): """Accepts index and slice keys Parameters ---------- key: 3-tuple of Integer or Slice object \tCell key(s) that shall be set value: Object (should be Unicode or similar) \tCode for cell(s) to be set mark_unredo: Boolean, defaults to True \tIf True then an unredo marker is set after the operation """ single_keys_per_dim = [] for axis, key_ele in enumerate(key): if is_slice_like(key_ele): # We have something slice-like here length = key[axis] slice_range = xrange(*key_ele.indices(length)) single_keys_per_dim.append(slice_range) elif is_string_like(key_ele): # We have something string-like here raise NotImplementedError else: # key_ele is a single cell single_keys_per_dim.append((key_ele, )) single_keys = product(*single_keys_per_dim) unredo_mark = False for single_key in single_keys: if value: # UnRedo support old_value = self(key) try: old_value = unicode(old_value, encoding="utf-8") except TypeError: pass # We seem to have double calls on __setitem__ # This hack catches them if old_value != value: unredo_mark = True undo_operation = (self.__setitem__, [key, old_value, mark_unredo]) redo_operation = (self.__setitem__, [key, value, mark_unredo]) self.unredo.append(undo_operation, redo_operation) # End UnRedo support # Never change merged cells merging_cell = \ self.cell_attributes.get_merging_cell(single_key) if merging_cell is None or merging_cell == single_key: self.dict_grid[single_key] = value else: # Value is empty --> delete cell try: self.pop(key) except (KeyError, TypeError): pass if mark_unredo and unredo_mark: self.unredo.mark() def cell_array_generator(self, key): """Generator traversing cells specified in key Parameters ---------- key: Iterable of Integer or slice \tThe key specifies the cell keys of the generator """ for i, key_ele in enumerate(key): # Get first element of key that is a slice if type(key_ele) is SliceType: slc_keys = xrange(*key_ele.indices(self.dict_grid.shape[i])) key_list = list(key) key_list[i] = None has_subslice = any(type(ele) is SliceType for ele in key_list) for slc_key in slc_keys: key_list[i] = slc_key if has_subslice: # If there is a slice left yield generator yield self.cell_array_generator(key_list) else: # No slices? Yield value yield self[tuple(key_list)] break def _shift_rowcol(self, insertion_point, no_to_insert, mark_unredo): """Shifts row and column sizes when a table is inserted or deleted""" if mark_unredo: self.unredo.mark() # Shift row heights new_row_heights = {} del_row_heights = [] for row, tab in self.row_heights: if tab > insertion_point: new_row_heights[(row, tab + no_to_insert)] = \ self.row_heights[(row, tab)] del_row_heights.append((row, tab)) for row, tab in new_row_heights: self.set_row_height(row, tab, new_row_heights[(row, tab)], mark_unredo=False) for row, tab in del_row_heights: if (row, tab) not in new_row_heights: self.set_row_height(row, tab, None, mark_unredo=False) # Shift column widths new_col_widths = {} del_col_widths = [] for col, tab in self.col_widths: if tab > insertion_point: new_col_widths[(col, tab + no_to_insert)] = \ self.col_widths[(col, tab)] del_col_widths.append((col, tab)) for col, tab in new_col_widths: self.set_col_width(col, tab, new_col_widths[(col, tab)], mark_unredo=False) for col, tab in del_col_widths: if (col, tab) not in new_col_widths: self.set_col_width(col, tab, None, mark_unredo=False) if mark_unredo: self.unredo.mark() def _adjust_rowcol(self, insertion_point, no_to_insert, axis, tab=None, mark_unredo=True): """Adjusts row and column sizes on insertion/deletion""" if axis == 2: self._shift_rowcol(insertion_point, no_to_insert, mark_unredo) return assert axis in (0, 1) if mark_unredo: self.unredo.mark() cell_sizes = self.col_widths if axis else self.row_heights set_cell_size = self.set_col_width if axis else self.set_row_height new_sizes = {} del_sizes = [] for pos, table in cell_sizes: if pos > insertion_point and (tab is None or tab == table): if 0 <= pos + no_to_insert < self.shape[axis]: new_sizes[(pos + no_to_insert, table)] = \ cell_sizes[(pos, table)] del_sizes.append((pos, table)) for pos, table in new_sizes: set_cell_size(pos, table, new_sizes[(pos, table)], mark_unredo=False) for pos, table in del_sizes: if (pos, table) not in new_sizes: set_cell_size(pos, table, None, mark_unredo=False) if mark_unredo: self.unredo.mark() def _adjust_merge_area(self, attrs, insertion_point, no_to_insert, axis): """Returns an updated merge area Parameters ---------- attrs: Dict \tCell attribute dictionary that shall be adjusted insertion_point: Integer \tPont on axis, before which insertion takes place no_to_insert: Integer >= 0 \tNumber of rows/cols/tabs that shall be inserted axis: Integer in range(2) \tSpecifies number of dimension, i.e. 0 == row, 1 == col """ assert axis in range(2) if "merge_area" not in attrs or attrs["merge_area"] is None: return top, left, bottom, right = attrs["merge_area"] selection = Selection([(top, left)], [(bottom, right)], [], [], []) selection.insert(insertion_point, no_to_insert, axis) __top, __left = selection.block_tl[0] __bottom, __right = selection.block_br[0] # Adjust merge area if it is beyond the grid shape rows, cols, tabs = self.shape if __top < 0 or __bottom >= rows or __left < 0 or __right >= cols: attrs["merge_area"] = None else: attrs["merge_area"] = __top, __left, __bottom, __right def _adjust_cell_attributes(self, insertion_point, no_to_insert, axis, tab=None, cell_attrs=None, mark_unredo=True): """Adjusts cell attributes on insertion/deletion Parameters ---------- insertion_point: Integer \tPont on axis, before which insertion takes place no_to_insert: Integer >= 0 \tNumber of rows/cols/tabs that shall be inserted axis: Integer in range(3) \tSpecifies number of dimension, i.e. 0 == row, 1 == col, ... tab: Integer, defaults to None \tIf given then insertion is limited to this tab for axis < 2 cell_attrs: List, defaults to [] \tIf not empty then the given cell attributes replace the existing ones mark_unredo: Boolean, defaults to True \tIf True then an unredo marker is set after the operation """ def replace_cell_attributes_table(index, new_table): ca = list(self.cell_attributes.get_item(index)) ca[1] = new_table self.cell_attributes.set_item(index, tuple(ca)) if axis not in range(3): raise ValueError("Axis must be in [0, 1, 2]") assert tab is None or tab >= 0 if cell_attrs is None: cell_attrs = [] # Store existing cell attributes for creating undo operation old_cell_attrs = self.cell_attributes[:] if cell_attrs: self.cell_attributes[:] = cell_attrs elif axis < 2: # Adjust selections on given table for selection, table, attrs in self.cell_attributes: if tab is None or tab == table: selection.insert(insertion_point, no_to_insert, axis) # Update merge area if present self._adjust_merge_area(attrs, insertion_point, no_to_insert, axis) elif axis == 2: # Adjust tabs pop_indices = [] for i, cell_attribute in enumerate(self.cell_attributes): selection, table, value = cell_attribute if no_to_insert < 0 and insertion_point <= table: if insertion_point > table + no_to_insert: # Delete later pop_indices.append(i) else: replace_cell_attributes_table(i, table + no_to_insert) elif insertion_point < table: # Insert replace_cell_attributes_table(i, table + no_to_insert) for i in pop_indices[::-1]: self.cell_attributes.pop(i) self.cell_attributes._attr_cache.clear() self.cell_attributes._update_table_cache() undo_operation = (self._adjust_cell_attributes, [insertion_point, -no_to_insert, axis, tab, old_cell_attrs, mark_unredo]) redo_operation = (self._adjust_cell_attributes, [insertion_point, no_to_insert, axis, tab, cell_attrs, mark_unredo]) self.unredo.append(undo_operation, redo_operation) if mark_unredo: self.unredo.mark() def insert(self, insertion_point, no_to_insert, axis, tab=None): """Inserts no_to_insert rows/cols/tabs/... before insertion_point Parameters ---------- insertion_point: Integer \tPont on axis, before which insertion takes place no_to_insert: Integer >= 0 \tNumber of rows/cols/tabs that shall be inserted axis: Integer \tSpecifies number of dimension, i.e. 0 == row, 1 == col, ... tab: Integer, defaults to None \tIf given then insertion is limited to this tab for axis < 2 """ self.unredo.mark() if not 0 <= axis <= len(self.shape): raise ValueError("Axis not in grid dimensions") if insertion_point > self.shape[axis] or \ insertion_point < -self.shape[axis]: raise IndexError("Insertion point not in grid") new_keys = {} del_keys = [] for key in self.dict_grid.keys(): if key[axis] > insertion_point and (tab is None or tab == key[2]): new_key = list(key) new_key[axis] += no_to_insert if 0 <= new_key[axis] < self.shape[axis]: new_keys[tuple(new_key)] = self(key) del_keys.append(key) # Now re-insert moved keys for key in del_keys: if key not in new_keys and self(key) is not None: self.pop(key, mark_unredo=False) self._adjust_rowcol(insertion_point, no_to_insert, axis, tab=tab, mark_unredo=False) self._adjust_cell_attributes(insertion_point, no_to_insert, axis, tab, mark_unredo=False) for key in new_keys: self.__setitem__(key, new_keys[key], mark_unredo=False) self.unredo.mark() def delete(self, deletion_point, no_to_delete, axis, tab=None): """Deletes no_to_delete rows/cols/... starting with deletion_point Axis specifies number of dimension, i.e. 0 == row, 1 == col, 2 == tab """ self.unredo.mark() if not 0 <= axis < len(self.shape): raise ValueError("Axis not in grid dimensions") if no_to_delete < 0: raise ValueError("Cannot delete negative number of rows/cols/...") elif no_to_delete >= self.shape[axis]: raise ValueError("Last row/column/table must not be deleted") if deletion_point > self.shape[axis] or \ deletion_point <= -self.shape[axis]: raise IndexError("Deletion point not in grid") new_keys = {} del_keys = [] # Note that the loop goes over a list that copies all dict keys for key in self.dict_grid.keys(): if tab is None or tab == key[2]: if deletion_point <= key[axis] < deletion_point + no_to_delete: del_keys.append(key) elif key[axis] >= deletion_point + no_to_delete: new_key = list(key) new_key[axis] -= no_to_delete new_keys[tuple(new_key)] = self(key) del_keys.append(key) # Now re-insert moved keys for key in new_keys: self.__setitem__(key, new_keys[key], mark_unredo=False) for key in del_keys: if key not in new_keys and self(key) is not None: self.pop(key, mark_unredo=False) self._adjust_rowcol(deletion_point, -no_to_delete, axis, tab=tab, mark_unredo=False) self._adjust_cell_attributes(deletion_point, -no_to_delete, axis, tab, mark_unredo=False) self.unredo.mark() def set_row_height(self, row, tab, height, mark_unredo=True): """Sets row height""" if mark_unredo: self.unredo.mark() try: old_height = self.row_heights.pop((row, tab)) except KeyError: old_height = None if height is not None: self.row_heights[(row, tab)] = float(height) # Make undoable undo_operation = (self.set_row_height, [row, tab, old_height, mark_unredo]) redo_operation = (self.set_row_height, [row, tab, height, mark_unredo]) self.unredo.append(undo_operation, redo_operation) if mark_unredo: self.unredo.mark() def set_col_width(self, col, tab, width, mark_unredo=True): """Sets column width""" if mark_unredo: self.unredo.mark() try: old_width = self.col_widths.pop((col, tab)) except KeyError: old_width = None if width is not None: self.col_widths[(col, tab)] = float(width) # Make undoable undo_operation = (self.set_col_width, [col, tab, old_width, mark_unredo]) redo_operation = (self.set_col_width, [col, tab, width, mark_unredo]) self.unredo.append(undo_operation, redo_operation) if mark_unredo: self.unredo.mark() # Element access via call __call__ = __getitem__
class DataArray(object): """DataArray provides enhanced grid read/write access. Enhancements comprise: * Slicing * Multi-dimensional operations such as insertion and deletion along 1 axis * Undo/redo operations This class represents layer 2 of the model. Parameters ---------- shape: n-tuple of integer \tShape of the grid """ def __init__(self, shape): self.dict_grid = DictGrid(shape) # Undo and redo management self.unredo = UnRedo() self.dict_grid.cell_attributes.unredo = self.unredo # Safe mode self.safe_mode = False # Row and column attributes mask # Keys have the format (row, table) @property def row_heights(self): return self.dict_grid.row_heights @property def col_widths(self): return self.dict_grid.col_widths # Cell attributes mask @property def cell_attributes(self): return self.dict_grid.cell_attributes def __iter__(self): """returns iterator over self.dict_grid""" return iter(self.dict_grid) def _get_macros(self): return self.dict_grid.macros def _set_macros(self, macros): self.dict_grid.macros = macros macros = property(_get_macros, _set_macros) def keys(self): """Returns keys in self.dict_grid""" return self.dict_grid.keys() def pop(self, key): """Pops dict_grid with undo and redo support""" # UnRedo support try: undo_operation = (self.__setitem__, [key, self.dict_grid[key]]) redo_operation = (self.pop, [key]) self.unredo.append(undo_operation, redo_operation) self.unredo.mark() except KeyError: # If key not present then unredo is not necessary pass # End UnRedo support return self.dict_grid.pop(key) # Shape mask def _get_shape(self): """Returns dict_grid shape""" return self.dict_grid.shape def _set_shape(self, shape): """Deletes all cells beyond new shape and sets dict_grid shape""" # Delete each cell that is beyond new borders old_shape = self.shape if any(new_axis < old_axis for new_axis, old_axis in zip(shape, old_shape)): for key in self.dict_grid.keys(): if any(key_ele >= new_axis for key_ele, new_axis in zip(key, shape)): self.pop(key) # Set dict_grid shape attribute self.dict_grid.shape = shape # UnRedo support undo_operation = (setattr, [self.dict_grid, "shape", old_shape]) redo_operation = (setattr, [self.dict_grid, "shape", shape]) self.unredo.append(undo_operation, redo_operation) self.unredo.mark() # End UnRedo support shape = property(_get_shape, _set_shape) # Pickle support def __getstate__(self): """Returns dict_grid for pickling Note that all persistent data is contained in the DictGrid class """ return {"dict_grid": self.dict_grid} # Slice support def __getitem__(self, key): """Adds slicing access to cell code retrieval The cells are returned as a generator of generators, of ... of unicode. Parameters ---------- key: n-tuple of integer or slice \tKeys of the cell code that is returned Note ---- Classical Excel type addressing (A$1, ...) may be added here """ for key_ele in key: if is_slice_like(key_ele): # We have something slice-like here return self.cell_array_generator(key) elif is_string_like(key_ele): # We have something string-like here raise NotImplementedError, \ "Cell string based access not implemented" # key_ele should be a single cell return self.dict_grid[key] def __str__(self): return self.dict_grid.__str__() def __setitem__(self, key, value): """Accepts index and slice keys""" single_keys_per_dim = [] for axis, key_ele in enumerate(key): if is_slice_like(key_ele): # We have something slice-like here single_keys_per_dim.append(slice_range(key_ele, length = key[axis])) elif is_string_like(key_ele): # We have something string-like here raise NotImplementedError else: # key_ele is a single cell single_keys_per_dim.append((key_ele, )) single_keys = product(*single_keys_per_dim) unredo_mark = False for single_key in single_keys: if value: # UnRedo support old_value = self(key) # We seem to have double calls on __setitem__ # This hack catches them if old_value != value: unredo_mark = True undo_operation = (self.__setitem__, [key, old_value]) redo_operation = (self.__setitem__, [key, value]) self.unredo.append(undo_operation, redo_operation) # End UnRedo support self.dict_grid[single_key] = value else: # Value is empty --> delete cell try: self.dict_grid.pop(key) except (KeyError, TypeError): pass if unredo_mark: self.unredo.mark() def cell_array_generator(self, key): """Generator traversing cells specified in key Parameters ---------- key: Iterable of Integer or slice \tThe key specifies the cell keys of the generator """ for i, key_ele in enumerate(key): # Get first element of key that is a slice if type(key_ele) is SliceType: slc_keys = slice_range(key_ele, self.dict_grid.shape[i]) key_list = list(key) key_list[i] = None has_subslice = any(type(ele) is SliceType for ele in key_list) for slc_key in slc_keys: key_list[i] = slc_key if has_subslice: # If there is a slice left yield generator yield self.cell_array_generator(key_list) else: # No slices? Yield value yield self[tuple(key_list)] break def _adjust_shape(self, amount, axis): """Changes shape along axis by amount""" new_shape = list(self.shape) new_shape[axis] += amount self.shape = tuple(new_shape) def _set_cell_attributes(self, value): """Setter for cell_atributes""" while len(self.cell_attributes): self.cell_attributes.pop() self.cell_attributes.extend(value) def _adjust_cell_attributes(self, insertion_point, no_to_insert, axis): """Adjusts cell attributes on insertion/deletion""" assert axis in [0, 1, 2] # Save cell_attributes for undo old_cell_attributes = copy(self.cell_attributes) if axis < 2: # Adjust selections for selection, _, _ in self.cell_attributes: selection.insert(insertion_point, no_to_insert, axis) self.cell_attributes._attr_cache.clear() # Adjust row heights and col widths cell_sizes = self.col_widths if axis else self.row_heights new_sizes = {} for pos, tab in cell_sizes: if pos > insertion_point: new_sizes[(pos+no_to_insert, tab)] = cell_sizes[(pos, tab)] cell_sizes[(pos, tab)] = None else: new_sizes[(pos, tab)] = cell_sizes[(pos, tab)] cell_sizes.update(new_sizes) elif axis == 2: # Adjust tabs new_tabs = [] for _, old_tab, _ in self.cell_attributes: new_tabs.append(old_tab + no_to_insert \ if old_tab > insertion_point else old_tab) for i, new_tab in new_tabs: self.cell_attributes[i][1] = new_tab self.cell_attributes._attr_cache.clear() else: raise ValueError, "axis must be in [0, 1, 2]" # Make undoable undo_operation = (self._adjust_cell_attributes, [insertion_point, -no_to_insert, axis]) redo_operation = (self._adjust_cell_attributes, [insertion_point, no_to_insert, axis]) self.unredo.append(undo_operation, redo_operation) def insert(self, insertion_point, no_to_insert, axis): """Inserts no_to_insert rows/cols/tabs/... before insertion_point Parameters ---------- insertion_point: Integer \tPont on axis, before which insertion takes place no_to_insert: Integer >= 0 \tNumber of rows/cols/tabs that shall be inserted axis: Integer \tSpecifies number of dimension, i.e. 0 == row, 1 == col, ... """ if not 0 <= axis <= len(self.shape): raise ValueError, "Axis not in grid dimensions" if insertion_point > self.shape[axis] or \ insertion_point <= -self.shape[axis]: raise IndexError, "Insertion point not in grid" new_keys = {} for key in copy(self.dict_grid): if key[axis] >= insertion_point: new_key = list(key) new_key[axis] += no_to_insert new_keys[tuple(new_key)] = self.pop(key) self._adjust_shape(no_to_insert, axis) for key in new_keys: self[key] = new_keys[key] self._adjust_cell_attributes(insertion_point, no_to_insert, axis) def delete(self, deletion_point, no_to_delete, axis): """Deletes no_to_delete rows/cols/tabs/... starting with deletion_point Axis specifies number of dimension, i.e. 0 == row, 1 == col, ... """ if no_to_delete < 0: raise ValueError, "Cannot delete negative number of rows/cols/..." if not 0 <= axis <= len(self.shape): raise ValueError, "Axis not in grid dimensions" if deletion_point > self.shape[axis] or \ deletion_point <= -self.shape[axis]: raise IndexError, "Deletion point not in grid" for key in copy(self.dict_grid): if deletion_point <= key[axis] < deletion_point + no_to_delete: self[key] = self.pop(key) elif key[axis] >= deletion_point + no_to_delete: new_key = list(key) new_key[axis] -= no_to_delete self[tuple(new_key)] = self.pop(key) self._adjust_cell_attributes(deletion_point, -no_to_delete, axis) self._adjust_shape(-no_to_delete, axis) def set_row_height(self, row, tab, height): """Sets row height""" try: old_height = self.row_heights[(row, tab)] except KeyError: old_height = None if height is None: self.row_heights.pop((row, tab)) else: self.row_heights[(row, tab)] = height # Make undoable undo_operation = (self.set_row_height, [row, tab, old_height]) redo_operation = (self.set_row_height, [row, tab, height]) self.unredo.append(undo_operation, redo_operation) def set_col_width(self, col, tab, width): """Sets column width""" try: old_width = self.col_widths[(col, tab)] except KeyError: old_width = None if width is None: self.col_widths.pop((col, tab)) else: self.col_widths[(col, tab)] = width # Make undoable undo_operation = (self.set_col_width, [col, tab, old_width]) redo_operation = (self.set_col_width, [col, tab, width]) self.unredo.append(undo_operation, redo_operation) # Element access via call __call__ = __getitem__
class DataArray(object): """DataArray provides enhanced grid read/write access. Enhancements comprise: * Slicing * Multi-dimensional operations such as insertion and deletion along 1 axis * Undo/redo operations This class represents layer 2 of the model. Parameters ---------- shape: n-tuple of integer \tShape of the grid """ def __init__(self, shape): self.dict_grid = DictGrid(shape) # Undo and redo management self.unredo = UnRedo() self.dict_grid.cell_attributes.unredo = self.unredo # Safe mode self.safe_mode = False # Data is the central content interface for loading / saving data. # It shall be used for loading and saving from and to pys and other files. # It shall be used for loading and saving macros. # It is not used for importinf and exporting data because these operations # are partial to the grid. def _get_data(self): """Returns dict of data content. Keys ---- shape: 3-tuple of Integer \tGrid shape grid: Dict of 3-tuples to strings \tCell content attributes: List of 3-tuples \tCell attributes row_heights: Dict of 2-tuples to float \t(row, tab): row_height col_widths: Dict of 2-tuples to float \t(col, tab): col_width macros: String \tMacros from macro list """ data = {} data["shape"] = self.shape data["grid"] = {}.update(self.dict_grid) data["attributes"] = [ca for ca in self.cell_attributes] data["row_heights"] = self.row_heights data["col_widths"] = self.col_widths data["macros"] = self.macros return data def _set_data(self, **kwargs): """Sets data from given parameters Old values are deleted. If a paremeter is not given, nothing is changed. Parameters ---------- shape: 3-tuple of Integer \tGrid shape grid: Dict of 3-tuples to strings \tCell content attributes: List of 3-tuples \tCell attributes row_heights: Dict of 2-tuples to float \t(row, tab): row_height col_widths: Dict of 2-tuples to float \t(col, tab): col_width macros: String \tMacros from macro list """ if "shape" in kwargs: self.shape = kwargs["shape"] if "grid" in kwargs: self.dict_grid.clear() self.dict_grid.update(kwargs["grid"]) if "attributes" in kwargs: self.attributes[:] = kwargs["attributes"] if "row_heights" in kwargs: self.row_heights = kwargs["row_heights"] if "col_widths" in kwargs: self.col_widths = kwargs["col_widths"] if "macros" in kwargs: self.macros = kwargs["macros"] data = property(_get_data, _set_data) # Row and column attributes mask # Keys have the format (row, table) def _get_row_heights(self): """Returns row_heights dict""" return self.dict_grid.row_heights def _set_row_heights(self, row_heights): """Sets macros string""" self.dict_grid.row_heights = row_heights row_heights = property(_get_row_heights, _set_row_heights) def _get_col_widths(self): """Returns col_widths dict""" return self.dict_grid.col_widths def _set_col_widths(self, col_widths): """Sets macros string""" self.dict_grid.col_widths = col_widths col_widths = property(_get_col_widths, _set_col_widths) # Cell attributes mask def _get_cell_attributes(self): """Returns cell_attributes list""" return self.dict_grid.cell_attributes def _set_cell_attributes(self, value): """Setter for cell_atributes""" # Empty cell_attributes first self.cell_attributes[:] = [] self.cell_attributes.extend(value) cell_attributes = attributes = \ property(_get_cell_attributes, _set_cell_attributes) def __iter__(self): """Returns iterator over self.dict_grid""" return iter(self.dict_grid) def _get_macros(self): """Returns macros string""" return self.dict_grid.macros def _set_macros(self, macros): """Sets macros string""" self.dict_grid.macros = macros macros = property(_get_macros, _set_macros) def keys(self): """Returns keys in self.dict_grid""" return self.dict_grid.keys() def pop(self, key, mark_unredo=True): """Pops dict_grid with undo and redo support Parameters ---------- key: 3-tuple of Integer \tCell key that shall be popped mark_unredo: Boolean, defaults to True \tIf True then an unredo marker is set after the operation """ result = self.dict_grid.pop(key) # UnRedo support if mark_unredo: self.unredo.mark() undo_operation = (self.__setitem__, [key, result, mark_unredo]) redo_operation = (self.pop, [key, mark_unredo]) self.unredo.append(undo_operation, redo_operation) if mark_unredo: self.unredo.mark() # End UnRedo support return result # Shape mask def _get_shape(self): """Returns dict_grid shape""" return self.dict_grid.shape def _set_shape(self, shape, mark_unredo=True): """Deletes all cells beyond new shape and sets dict_grid shape Parameters ---------- shape: 3-tuple of Integer \tTarget shape for grid mark_unredo: Boolean, defaults to True \tIf True then an unredo marker is set after the operation """ # Delete each cell that is beyond new borders old_shape = self.shape if any(new_axis < old_axis for new_axis, old_axis in zip(shape, old_shape)): for key in self.dict_grid.keys(): if any(key_ele >= new_axis for key_ele, new_axis in zip(key, shape)): self.pop(key) # Set dict_grid shape attribute self.dict_grid.shape = shape # UnRedo support undo_operation = (self._set_shape, [old_shape, mark_unredo]) redo_operation = (self._set_shape, [shape, mark_unredo]) self.unredo.append(undo_operation, redo_operation) if mark_unredo: self.unredo.mark() # End UnRedo support shape = property(_get_shape, _set_shape) def get_last_filled_cell(self, table=None): """Returns key for the bottommost rightmost cell with content Parameters ---------- table: Integer, defaults to None \tLimit search to this table """ maxrow = 0 maxcol = 0 for row, col, tab in self.dict_grid: if table is None or tab == table: maxrow = max(row, maxrow) maxcol = max(col, maxcol) return maxrow, maxcol, table # Pickle support def __getstate__(self): """Returns dict_grid for pickling Note that all persistent data is contained in the DictGrid class """ return {"dict_grid": self.dict_grid} # Slice support def __getitem__(self, key): """Adds slicing access to cell code retrieval The cells are returned as a generator of generators, of ... of unicode. Parameters ---------- key: n-tuple of integer or slice \tKeys of the cell code that is returned Note ---- Classical Excel type addressing (A$1, ...) may be added here """ for key_ele in key: if is_slice_like(key_ele): # We have something slice-like here return self.cell_array_generator(key) elif is_string_like(key_ele): # We have something string-like here msg = "Cell string based access not implemented" raise NotImplementedError(msg) # key_ele should be a single cell return self.dict_grid[key] def __setitem__(self, key, value, mark_unredo=True): """Accepts index and slice keys Parameters ---------- key: 3-tuple of Integer or Slice object \tCell key(s) that shall be set value: Object (should be Unicode or similar) \tCode for cell(s) to be set mark_unredo: Boolean, defaults to True \tIf True then an unredo marker is set after the operation """ single_keys_per_dim = [] for axis, key_ele in enumerate(key): if is_slice_like(key_ele): # We have something slice-like here length = key[axis] slice_range = xrange(*key_ele.indices(length)) single_keys_per_dim.append(slice_range) elif is_string_like(key_ele): # We have something string-like here raise NotImplementedError else: # key_ele is a single cell single_keys_per_dim.append((key_ele, )) single_keys = product(*single_keys_per_dim) unredo_mark = False for single_key in single_keys: if value: # UnRedo support old_value = self(key) try: old_value = unicode(old_value, encoding="utf-8") except TypeError: pass # We seem to have double calls on __setitem__ # This hack catches them if old_value != value: unredo_mark = True undo_operation = (self.__setitem__, [key, old_value, mark_unredo]) redo_operation = (self.__setitem__, [key, value, mark_unredo]) self.unredo.append(undo_operation, redo_operation) # End UnRedo support self.dict_grid[single_key] = value else: # Value is empty --> delete cell try: self.dict_grid.pop(key) except (KeyError, TypeError): pass if mark_unredo and unredo_mark: self.unredo.mark() def cell_array_generator(self, key): """Generator traversing cells specified in key Parameters ---------- key: Iterable of Integer or slice \tThe key specifies the cell keys of the generator """ for i, key_ele in enumerate(key): # Get first element of key that is a slice if type(key_ele) is SliceType: slc_keys = xrange(*key_ele.indices(self.dict_grid.shape[i])) key_list = list(key) key_list[i] = None has_subslice = any(type(ele) is SliceType for ele in key_list) for slc_key in slc_keys: key_list[i] = slc_key if has_subslice: # If there is a slice left yield generator yield self.cell_array_generator(key_list) else: # No slices? Yield value yield self[tuple(key_list)] break def _shift_rowcol(self, insertion_point, no_to_insert, mark_unredo): """Shifts row and column sizes when a table is inserted or deleted""" if mark_unredo: self.unredo.mark() # Shift row heights new_row_heights = {} del_row_heights = [] for row, tab in self.row_heights: if tab > insertion_point: new_row_heights[(row, tab + no_to_insert)] = \ self.row_heights[(row, tab)] del_row_heights.append((row, tab)) for row, tab in new_row_heights: self.set_row_height(row, tab, new_row_heights[(row, tab)], mark_unredo=False) for row, tab in del_row_heights: if (row, tab) not in new_row_heights: self.set_row_height(row, tab, None, mark_unredo=False) # Shift column widths new_col_widths = {} del_col_widths = [] for col, tab in self.col_widths: if tab > insertion_point: new_col_widths[(col, tab + no_to_insert)] = \ self.col_widths[(col, tab)] del_col_widths.append((col, tab)) for col, tab in new_col_widths: self.set_col_width(col, tab, new_col_widths[(col, tab)], mark_unredo=False) for col, tab in del_col_widths: if (col, tab) not in new_col_widths: self.set_col_width(col, tab, None, mark_unredo=False) if mark_unredo: self.unredo.mark() def _adjust_rowcol(self, insertion_point, no_to_insert, axis, tab=None, mark_unredo=True): """Adjusts row and column sizes on insertion/deletion""" if axis == 2: self._shift_rowcol(insertion_point, no_to_insert, mark_unredo) return assert axis in (0, 1) if mark_unredo: self.unredo.mark() cell_sizes = self.col_widths if axis else self.row_heights set_cell_size = self.set_col_width if axis else self.set_row_height new_sizes = {} del_sizes = [] for pos, table in cell_sizes: if pos > insertion_point and (tab is None or tab == table): if 0 <= pos + no_to_insert < self.shape[axis]: new_sizes[(pos + no_to_insert, table)] = \ cell_sizes[(pos, table)] del_sizes.append((pos, table)) for pos, table in new_sizes: set_cell_size(pos, table, new_sizes[(pos, table)], mark_unredo=False) for pos, table in del_sizes: if (pos, table) not in new_sizes: set_cell_size(pos, table, None, mark_unredo=False) if mark_unredo: self.unredo.mark() def _adjust_cell_attributes(self, insertion_point, no_to_insert, axis, tab=None, cell_attrs=None, mark_unredo=True): """Adjusts cell attributes on insertion/deletion""" if mark_unredo: self.unredo.mark() old_cell_attrs = self.cell_attributes[:] if axis < 2: # Adjust selections if cell_attrs is None: cell_attrs = [] for key in self.cell_attributes: selection, table, value = key if tab is None or tab == table: new_sel = copy(selection) new_val = copy(value) new_sel.insert(insertion_point, no_to_insert, axis) # Update merge area if present if "merge_area" in value: top, left, bottom, right = value["merge_area"] ma_sel = Selection([(top, left)], [(bottom, right)], [], [], []) ma_sel.insert(insertion_point, no_to_insert, axis) __top, __left = ma_sel.block_tl[0] __bottom, __right = ma_sel.block_br[0] new_val["merge_area"] = \ __top, __left, __bottom, __right cell_attrs.append((new_sel, table, new_val)) self.cell_attributes[:] = cell_attrs self.cell_attributes._attr_cache.clear() elif axis == 2: # Adjust tabs new_tabs = [] for selection, old_tab, value in self.cell_attributes: if old_tab > insertion_point and \ (tab is None or tab == old_tab): new_tabs.append((selection, old_tab + no_to_insert, value)) else: new_tabs.append(None) for i, sel_tab_val in enumerate(new_tabs): if sel_tab_val is not None: self.dict_grid.cell_attributes.set_item(i, sel_tab_val) self.cell_attributes._attr_cache.clear() else: raise ValueError("Axis must be in [0, 1, 2]") undo_operation = (self._adjust_cell_attributes, [ insertion_point, -no_to_insert, axis, tab, old_cell_attrs, mark_unredo ]) redo_operation = (self._adjust_cell_attributes, [ insertion_point, no_to_insert, axis, tab, cell_attrs, mark_unredo ]) self.unredo.append(undo_operation, redo_operation) if mark_unredo: self.unredo.mark() def insert(self, insertion_point, no_to_insert, axis, tab=None): """Inserts no_to_insert rows/cols/tabs/... before insertion_point Parameters ---------- insertion_point: Integer \tPont on axis, before which insertion takes place no_to_insert: Integer >= 0 \tNumber of rows/cols/tabs that shall be inserted axis: Integer \tSpecifies number of dimension, i.e. 0 == row, 1 == col, ... tab: Integer, defaults to None \tIf given then insertion is limited to this tab for axis < 2 """ self.unredo.mark() if not 0 <= axis <= len(self.shape): raise ValueError("Axis not in grid dimensions") if insertion_point > self.shape[axis] or \ insertion_point < -self.shape[axis]: raise IndexError("Insertion point not in grid") new_keys = {} del_keys = [] for key in self.dict_grid.keys(): if key[axis] > insertion_point and (tab is None or tab == key[2]): new_key = list(key) new_key[axis] += no_to_insert if 0 <= new_key[axis] < self.shape[axis]: new_keys[tuple(new_key)] = self(key) del_keys.append(key) # Now re-insert moved keys for key in new_keys: self.__setitem__(key, new_keys[key], mark_unredo=False) for key in del_keys: if key not in new_keys and self(key) is not None: self.pop(key, mark_unredo=False) self._adjust_rowcol(insertion_point, no_to_insert, axis, tab=tab, mark_unredo=False) self._adjust_cell_attributes(insertion_point, no_to_insert, axis, tab=tab, mark_unredo=False) self.unredo.mark() def delete(self, deletion_point, no_to_delete, axis, tab=None): """Deletes no_to_delete rows/cols/... starting with deletion_point Axis specifies number of dimension, i.e. 0 == row, 1 == col, ... """ self.unredo.mark() if not 0 <= axis < len(self.shape): raise ValueError("Axis not in grid dimensions") if no_to_delete < 0: raise ValueError("Cannot delete negative number of rows/cols/...") elif no_to_delete >= self.shape[axis]: raise ValueError("Last row/column/table must not be deleted") if deletion_point > self.shape[axis] or \ deletion_point <= -self.shape[axis]: raise IndexError("Deletion point not in grid") new_keys = {} del_keys = [] # Note that the loop goes over a list that copies all dict keys for key in self.dict_grid.keys(): if tab is None or tab == key[2]: if deletion_point <= key[axis] < deletion_point + no_to_delete: del_keys.append(key) elif key[axis] >= deletion_point + no_to_delete: new_key = list(key) new_key[axis] -= no_to_delete new_keys[tuple(new_key)] = self(key) del_keys.append(key) # Now re-insert moved keys for key in new_keys: self.__setitem__(key, new_keys[key], mark_unredo=False) for key in del_keys: if key not in new_keys and self(key) is not None: self.pop(key, mark_unredo=False) if axis in (0, 1): self._adjust_rowcol(deletion_point, -no_to_delete, axis, tab=tab, mark_unredo=False) self._adjust_cell_attributes(deletion_point, -no_to_delete, axis, tab=tab, mark_unredo=False) self.unredo.mark() def set_row_height(self, row, tab, height, mark_unredo=True): """Sets row height""" if mark_unredo: self.unredo.mark() try: old_height = self.row_heights.pop((row, tab)) except KeyError: old_height = None if height is not None: self.row_heights[(row, tab)] = float(height) # Make undoable undo_operation = (self.set_row_height, [row, tab, old_height, mark_unredo]) redo_operation = (self.set_row_height, [row, tab, height, mark_unredo]) self.unredo.append(undo_operation, redo_operation) if mark_unredo: self.unredo.mark() def set_col_width(self, col, tab, width, mark_unredo=True): """Sets column width""" if mark_unredo: self.unredo.mark() try: old_width = self.col_widths.pop((col, tab)) except KeyError: old_width = None if width is not None: self.col_widths[(col, tab)] = float(width) # Make undoable undo_operation = (self.set_col_width, [col, tab, old_width, mark_unredo]) redo_operation = (self.set_col_width, [col, tab, width, mark_unredo]) self.unredo.append(undo_operation, redo_operation) if mark_unredo: self.unredo.mark() # Element access via call __call__ = __getitem__