diff --git a/.gitignore b/.gitignore index 2f295e5f..fde77957 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,12 @@ eval* .pytest_cache .tox docs_api + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ diff --git a/koala/Cell.py b/koala/Cell.py index df893ccd..a7e8cc43 100644 --- a/koala/Cell.py +++ b/koala/Cell.py @@ -6,8 +6,8 @@ from koala.Range import RangeCore from koala.utils import * -from openpyxl.compat import unicode - +#from openpyxl.compat import unicode +unicode = str class Cell(CellBase): ctr = 0 diff --git a/koala/Range.py b/koala/Range.py index f3c2c0cd..7f33dfa1 100644 --- a/koala/Range.py +++ b/koala/Range.py @@ -4,8 +4,8 @@ from koala.ExcelError import ErrorCodes, ExcelError from koala.utils import * -from openpyxl.compat import unicode - +#from openpyxl.compat import unicode +unicode = str # WARNING: Range should never be imported directly. Import Range from excelutils instead. diff --git a/koala/Spreadsheet.py b/koala/Spreadsheet.py index f4ac5db5..78b91501 100644 --- a/koala/Spreadsheet.py +++ b/koala/Spreadsheet.py @@ -7,7 +7,10 @@ from koala.reader import read_archive, read_named_ranges, read_cells # This import equivalent functions defined in Excel. from koala.excellib import * + from openpyxl.formula.translate import Translator +from openpyxl import Workbook + from koala.serializer import * from koala.tokenizer import reverse_rpn from koala.utils import * @@ -17,8 +20,10 @@ import networkx from networkx.readwrite import json_graph -from openpyxl.compat import unicode +import koala.ToExcel +#from openpyxl.compat import unicode +unicode = str class Spreadsheet(object): def __init__(self, file=None, ignore_sheets=[], ignore_hidden=False, debug=False): @@ -181,7 +186,7 @@ def gen_graph(self, outputs=[], inputs=[]): sp = Spreadsheet() sp.build_spreadsheet(G, cellmap, self.named_ranges, pointers = self.pointers, outputs = outputs, inputs = inputs, debug = self.debug) return sp - + def build_spreadsheet(self, G, cellmap, named_ranges, pointers = set(), outputs = set(), inputs = set(), debug = False): """ Writes the elements created by gen_graph to the object @@ -274,8 +279,16 @@ def cell_add(self, address=None, cell=None, value=None, formula=None): cellmap, G = graph_from_seeds([cell], self) - self.cellmap = cellmap - self.G = G + ''' + update this spreadsheet object + ''' + self.build_spreadsheet(G, cellmap, self.named_range, debug=self.debug) + + ''' + superceded by above + ''' + # self.cellmap = cellmap + # self.G = G print("Graph construction updated, %s nodes, %s edges, %s cellmap entries" % (len(G.nodes()),len(G.edges()),len(cellmap))) @@ -663,6 +676,10 @@ def dump_json(self, fname): def dump(self, fname): dump(self, fname) + + def to_excel(self, fname = None): + koala.ToExcel.to_excel(self, fname) + @staticmethod def load(fname): spreadsheet = Spreadsheet() diff --git a/koala/ToExcel.py b/koala/ToExcel.py new file mode 100644 index 00000000..4d269ece --- /dev/null +++ b/koala/ToExcel.py @@ -0,0 +1,141 @@ +import openpyxl + +def to_excel(spreadsheet, fname=None): + '''chaz's thing''' + + if fname is None: + raise Exception('No filename specified. Please provide one.') + else: + fname=fname.split('.')[0] + + # TODO: + # sort sheets before creating + # test ad-hoc worksheets + # pivot tables? eek. + + spreadsheet.prune_graph() + #spreadsheet.clean_pointer() + + ''' + isolate sheets, cells, and formulae from graph into a dict: + {thisSheet: [(thisCell, thisFormula), (thisCell_1, thisFormula_1),...]} + do not include range names as keys + ''' + + theDict={} + + #print(spreadsheet.addr_to_name) + #print(spreadsheet.addr_to_range) + #print(spreadsheet.named_ranges) + + print('reading tree contents...') + print('address, formula, value') + + for c in list(spreadsheet.cellmap.values()): + print(c.address(), c.formula, c.value) + ''' + actual name ranges (as opposed to named cells (single-cell ranges)) should be excluded + from theDict + ''' + if not any(c.address() in val for val in spreadsheet.addr_to_range.values()): + #print(c.address(), c.formula, c. value) + + thisCell = None + + thisAddress = c.address().split('!') + thisSheet = thisAddress[0] + if len(thisAddress) > 1: + thisCell = thisAddress[1] + + #thisCell=None + # thisSheet = c.address().split('!')[0] + # if len(c.address().) + + thisFormula = c.formula + if thisFormula is not None and thisFormula.find('=') != 0: thisFormula = '=' + thisFormula + + thisValue = c.value + + if thisFormula is not None: + thisValue = thisFormula + + print('collecting ' + thisSheet, thisCell, thisValue ) + if thisSheet not in theDict: + theDict[thisSheet] = [(thisCell, thisValue)] + else: + theDict[thisSheet].append((thisCell, thisValue)) + + ''' + clean up dict by removing range names from keys (keys are spreadsheet names) + ''' + for i in spreadsheet.named_ranges: + if i in theDict: + print(' removing name range for special handling') + theDict.pop(i) + + # print('------the dict------') + # print(theDict) + # print('--------------------') + + ''' + create the workbook with openpyxl + ''' + wb = openpyxl.Workbook() + + ''' + create sheets from theDict + ''' + for sheetName in theDict.keys(): + print(' creating sheet... ', sheetName) + wb.create_sheet(sheetName) + + ''' + get rid of the sheet autocreated by openpyxl + and don't consider range names (if any) as sheet named_ranges + ''' + for sheet in wb.sheetnames: + if sheet not in theDict.keys() or sheet in spreadsheet.named_ranges: + rm_sheet = wb[sheet] + wb.remove(rm_sheet) + + ''' + add formuale to cells by sheets + ''' + for sheetName, values in theDict.items(): + print('adding cell contents...') + for cells in values: + thisCell = cells[0] + thisFormula = cells[1] + print(' adding ' + sheetName + '!' + thisCell, thisFormula) + wb[sheetName][thisCell] = thisFormula + + ''' + add named ranges + ''' + print('adding ranges') + for thisName, thisAddress in spreadsheet.named_ranges.items(): + thisAddress = absolute_addr(thisAddress) + + print(' adding range ', thisName, thisAddress) + theRange = openpyxl.defined_name.DefinedName(thisName, attr_text=thisAddress) + wb.defined_names.append(theRange) + + print('saving wb') + try: + wb.close() + wb.save(fname + '.xlsx') + except Exception as e: + print('error saving wb: "' + str(e)+'"') + +def absolute_addr(theAddress): + #make the address absolute + theSheet = theAddress.split('!')[0] + theCells = theAddress.split('!')[1:] + for cell in theCells: + absCell = '' + for i in cell.split(':'): + absCell = absCell + openpyxl.cell.absolute_coordinate(i.strip()) + if len(cell.split(':')) > 1: absCell = absCell + ':' + if absCell[-1] == ':': absCell = absCell[:-1] + absAddress = theSheet + '!' + absCell + return absAddress diff --git a/koala/ast/__init__.py b/koala/ast/__init__.py index 6d1f8d21..bd817e33 100644 --- a/koala/ast/__init__.py +++ b/koala/ast/__init__.py @@ -6,7 +6,9 @@ import networkx from networkx.classes.digraph import DiGraph -from openpyxl.compat import unicode + +#from openpyxl.compat import unicode +unicode = str from koala.utils import uniqueify, flatten, max_dimension, col2num, resolve_range from koala.Cell import Cell diff --git a/koala/ast/astnodes.py b/koala/ast/astnodes.py index 03bceb44..3c766f91 100644 --- a/koala/ast/astnodes.py +++ b/koala/ast/astnodes.py @@ -3,7 +3,8 @@ from networkx import NetworkXError -from openpyxl.compat import unicode +#from openpyxl.compat import unicode +unicode = str from koala.excellib import FUNCTION_MAP, IND_FUN from koala.utils import is_range, split_range, split_address, resolve_range @@ -45,7 +46,7 @@ def __getattr__(self, name): def children(self, ast): try: args = ast.predecessors(self) - args = sorted(args, key=lambda x: ast.node[x]['pos']) + args = sorted(args, key=lambda x: ast.nodes[x]['pos']) except NetworkXError: args = '' return args diff --git a/koala/excellib.py b/koala/excellib.py index 46b85008..aef11d16 100644 --- a/koala/excellib.py +++ b/koala/excellib.py @@ -18,7 +18,8 @@ from calendar import monthrange from dateutil.relativedelta import relativedelta -from openpyxl.compat import unicode +#from openpyxl.compat import unicode +unicode = str from koala.utils import * from koala.Range import RangeCore as Range diff --git a/koala/serializer.py b/koala/serializer.py index 27dd2d79..7a20ab91 100644 --- a/koala/serializer.py +++ b/koala/serializer.py @@ -7,8 +7,8 @@ from networkx.classes.digraph import DiGraph from networkx.readwrite import json_graph from networkx.drawing.nx_pydot import write_dot -from openpyxl.compat import unicode - +#from openpyxl.compat import unicode +unicode = str from koala.Cell import Cell from koala.Range import RangeCore, RangeFactory @@ -120,12 +120,12 @@ def to_float(string): inputs = None named_ranges = {} infile = gzip.GzipFile(fname, 'rb') - + for line in infile.read().splitlines(): line= line.decode("utf-8") - if line == "====": + if line == "====": mode = "node0" continue if line == "-----": @@ -133,7 +133,7 @@ def to_float(string): Range = RangeFactory(cellmap_temp) mode = "node0" continue - elif line == "edges": + elif line == "edges": cellmap = {n.address(): n for n in nodes} mode = "edges" continue @@ -148,15 +148,15 @@ def to_float(string): continue if mode == "node0": - [address, formula, python_expression, is_range, is_named_range, is_pointer, should_eval] = line.split(SEP) + [address, formula, python_expression, is_range, is_named_range, is_pointer, should_eval] = line.split(SEP) formula = clean_bool(formula) python_expression = clean_bool(python_expression) is_range = to_bool(is_range) is_named_range = to_bool(is_named_range) is_pointer = to_bool(is_pointer) - should_eval = should_eval + should_eval = should_eval mode = "node1" - elif mode == "node1": + elif mode == "node1": if is_range: reference = json.loads(line) if is_pointer else line # in order to be able to parse dicts vv = Range(reference) diff --git a/koala/utils.py b/koala/utils.py index 626f1980..4d92687a 100644 --- a/koala/utils.py +++ b/koala/utils.py @@ -13,7 +13,8 @@ from six import string_types from copy import deepcopy -from openpyxl.compat import unicode +#from openpyxl.compat import unicode +unicode = str from .ExcelError import ExcelError diff --git a/requirements.txt b/requirements.txt index c676f783..3f0a0f55 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,8 +2,8 @@ networkx==2.1 openpyxl==2.5.3 numpy>=1.14.2 Cython==0.28.2 -lxml==4.1.1 six==1.11.0 scipy>=1.0.0 python-dateutil==2.8.0 backports.functools_lru_cache==1.5 +lxml==4.1.1