gnumeric 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gnumeric/.constants.json +8 -0
- gnumeric/__init__.py +41 -0
- gnumeric/cell.py +294 -0
- gnumeric/evaluation_errors.py +24 -0
- gnumeric/exceptions.py +49 -0
- gnumeric/expression.py +151 -0
- gnumeric/expression_evaluation.py +315 -0
- gnumeric/formula_functions/__init__.py +0 -0
- gnumeric/formula_functions/argument_helpers.py +21 -0
- gnumeric/formula_functions/mathematics.py +21 -0
- gnumeric/formula_functions/statistics.py +23 -0
- gnumeric/sheet.py +642 -0
- gnumeric/utils.py +131 -0
- gnumeric/workbook.py +423 -0
- gnumeric-0.1.0.dist-info/METADATA +55 -0
- gnumeric-0.1.0.dist-info/RECORD +17 -0
- gnumeric-0.1.0.dist-info/WHEEL +4 -0
gnumeric/sheet.py
ADDED
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gnumeric-py: Reading and writing gnumeric files with python
|
|
3
|
+
Copyright (C) 2017 Michael Lipschultz
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify
|
|
6
|
+
it under the terms of the GNU General Public License as published by
|
|
7
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
(at your option) any later version.
|
|
9
|
+
|
|
10
|
+
This program is distributed in the hope that it will be useful,
|
|
11
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
GNU General Public License for more details.
|
|
14
|
+
|
|
15
|
+
You should have received a copy of the GNU General Public License
|
|
16
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from itertools import product
|
|
20
|
+
from operator import attrgetter
|
|
21
|
+
from typing import (
|
|
22
|
+
Callable,
|
|
23
|
+
Dict,
|
|
24
|
+
Generator,
|
|
25
|
+
Iterable,
|
|
26
|
+
List,
|
|
27
|
+
Optional,
|
|
28
|
+
Sequence,
|
|
29
|
+
Tuple,
|
|
30
|
+
Union,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
from lxml import etree
|
|
34
|
+
|
|
35
|
+
from gnumeric import cell
|
|
36
|
+
from gnumeric.exceptions import UnsupportedOperationException
|
|
37
|
+
from gnumeric.utils import RowColReference, coordinate_from_spreadsheet
|
|
38
|
+
|
|
39
|
+
NEW_CELL = b"""<?xml version="1.0" encoding="UTF-8"?><gnm:ROOT xmlns:gnm="http://www.gnumeric.org/v10.dtd">
|
|
40
|
+
<gnm:Cell Row="%(row)a" Col="%(col)a" ValueType="%(value_type)a"/>
|
|
41
|
+
</gnm:ROOT>"""
|
|
42
|
+
|
|
43
|
+
SHEET_TYPE_REGULAR = None
|
|
44
|
+
SHEET_TYPE_OBJECT = 'object'
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
MaxMinFunction = Callable[[Iterable], int]
|
|
48
|
+
Cell = cell.Cell
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Sheet:
|
|
52
|
+
__EMPTY_CELL_XPATH_SELECTOR = f'@ValueType="{cell.VALUE_TYPE_EMPTY}" or (not(@ValueType) and not(@ExprID) and string-length(text())=0)'
|
|
53
|
+
|
|
54
|
+
def __init__(self, sheet_name_element, sheet_element, workbook):
|
|
55
|
+
self.__sheet_name = sheet_name_element
|
|
56
|
+
self.__sheet = sheet_element
|
|
57
|
+
self.__workbook = workbook
|
|
58
|
+
|
|
59
|
+
def __get_cells(self):
|
|
60
|
+
return self.__sheet.find('gnm:Cells', self.__workbook._ns)
|
|
61
|
+
|
|
62
|
+
def __get_empty_cells(self):
|
|
63
|
+
all_cells = self.__get_cells()
|
|
64
|
+
return all_cells.xpath(
|
|
65
|
+
f'./gnm:Cell[{self.__EMPTY_CELL_XPATH_SELECTOR}]',
|
|
66
|
+
namespaces=self.__workbook._ns,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def __get_non_empty_cells(self):
|
|
70
|
+
all_cells = self.__get_cells()
|
|
71
|
+
return all_cells.xpath(
|
|
72
|
+
f'./gnm:Cell[not({self.__EMPTY_CELL_XPATH_SELECTOR})]',
|
|
73
|
+
namespaces=self.__workbook._ns,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def __get_cell_element(self, row: int, col: int):
|
|
77
|
+
cells = self.__get_cells()
|
|
78
|
+
return cells.find(f'gnm:Cell[@Row="{row}"][@Col="{col}"]', self.__workbook._ns)
|
|
79
|
+
|
|
80
|
+
def __get_expression_id_cells(self):
|
|
81
|
+
all_cells = self.__get_cells()
|
|
82
|
+
return all_cells.xpath('./gnm:Cell[@ExprID]', namespaces=self.__workbook._ns)
|
|
83
|
+
|
|
84
|
+
def __get_styles(self):
|
|
85
|
+
return self.__sheet.xpath('./gnm:Styles', namespaces=self.__workbook._ns)[0]
|
|
86
|
+
|
|
87
|
+
def __get_cell_style(self, cell_element):
|
|
88
|
+
row = cell_element.get('Row')
|
|
89
|
+
col = cell_element.get('Col')
|
|
90
|
+
return self.__get_styles().xpath(
|
|
91
|
+
'./gnm:StyleRegion['
|
|
92
|
+
+ f'@startCol<="{col}" and "{col}"<=@endCol '
|
|
93
|
+
+ f'and @startRow<="{row}" and "{row}"<=@endRow]',
|
|
94
|
+
namespaces=self.__workbook._ns,
|
|
95
|
+
)[0]
|
|
96
|
+
|
|
97
|
+
def __create_and_get_new_cell(self, row_idx: int, col_idx: int) -> cell.Cell:
|
|
98
|
+
"""
|
|
99
|
+
Creates a new cell, adds it to the worksheet, and returns it.
|
|
100
|
+
"""
|
|
101
|
+
new_cell = etree.fromstring(
|
|
102
|
+
NEW_CELL
|
|
103
|
+
% {b'row': row_idx, b'col': col_idx, b'value_type': cell.VALUE_TYPE_EMPTY}
|
|
104
|
+
).getchildren()[0]
|
|
105
|
+
cells = self.__get_cells()
|
|
106
|
+
cells.append(new_cell)
|
|
107
|
+
return new_cell
|
|
108
|
+
|
|
109
|
+
def __cell_element_to_class(self, element) -> Cell:
|
|
110
|
+
return cell.Cell(
|
|
111
|
+
element, self.__get_cell_style(element), self, self.__workbook._ns
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
__ce2c = __cell_element_to_class
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def workbook(self):
|
|
118
|
+
"""
|
|
119
|
+
The workbook this sheet belongs to
|
|
120
|
+
"""
|
|
121
|
+
return self.__workbook
|
|
122
|
+
|
|
123
|
+
def get_title(self) -> str:
|
|
124
|
+
"""
|
|
125
|
+
The title, or name, of the worksheet
|
|
126
|
+
"""
|
|
127
|
+
return self.__sheet_name.text
|
|
128
|
+
|
|
129
|
+
def set_title(self, title: str) -> None:
|
|
130
|
+
sheet_name = self.__sheet.find('gnm:Name', self.__workbook._ns)
|
|
131
|
+
sheet_name.text = self.__sheet_name.text = title
|
|
132
|
+
|
|
133
|
+
title = property(get_title, set_title)
|
|
134
|
+
|
|
135
|
+
def remove_from_workbook(self) -> None:
|
|
136
|
+
"""
|
|
137
|
+
Delete this sheet from its workbook. Note that after this operation, this worksheet will be in an invalid state
|
|
138
|
+
and should not be used.
|
|
139
|
+
"""
|
|
140
|
+
self.__sheet_name.getparent().remove(self.__sheet_name)
|
|
141
|
+
self.__sheet.getparent().remove(self.__sheet)
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def type(self) -> str:
|
|
145
|
+
"""
|
|
146
|
+
The type of sheet:
|
|
147
|
+
- `SHEET_TYPE_REGULAR` if a regular worksheet
|
|
148
|
+
- `SHEET_TYPE_OBJECT` if an object (e.g. graph) worksheet
|
|
149
|
+
"""
|
|
150
|
+
return self.__sheet_name.get('{%s}SheetType' % (self.__workbook._ns['gnm']))
|
|
151
|
+
|
|
152
|
+
def __maxmin_rc(self, rc: str, mm_fn: MaxMinFunction) -> int:
|
|
153
|
+
"""
|
|
154
|
+
The abstracted method for `max_column`, `max_row`, `min_column`, and `min_row`
|
|
155
|
+
:param rc: `str` indiciating whether this is for `"column"` or `"row"`.
|
|
156
|
+
:param mm_fn: The function for whether to get the max or the min.
|
|
157
|
+
"""
|
|
158
|
+
if self.type == SHEET_TYPE_OBJECT:
|
|
159
|
+
raise UnsupportedOperationException('Chartsheet does not have ' + rc)
|
|
160
|
+
|
|
161
|
+
content_cells = self.__get_non_empty_cells()
|
|
162
|
+
return (
|
|
163
|
+
-1
|
|
164
|
+
if len(content_cells) == 0
|
|
165
|
+
else mm_fn(getattr(self.__ce2c(c), rc) for c in content_cells)
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def max_column(self) -> int:
|
|
170
|
+
"""
|
|
171
|
+
The maximum column that still holds data. Raises UnsupportedOperationException when the sheet is a chartsheet.
|
|
172
|
+
:return: `int`
|
|
173
|
+
"""
|
|
174
|
+
return self.__maxmin_rc('column', max)
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def max_row(self) -> int:
|
|
178
|
+
"""
|
|
179
|
+
The maximum row that still holds data. Raises UnsupportedOperationException when the sheet is a chartsheet.
|
|
180
|
+
:return: `int`
|
|
181
|
+
"""
|
|
182
|
+
return self.__maxmin_rc('row', max)
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def min_column(self) -> int:
|
|
186
|
+
"""
|
|
187
|
+
The minimum column that still holds data. Raises UnsupportedOperationException when the sheet is a chartsheet.
|
|
188
|
+
:return: `int`
|
|
189
|
+
"""
|
|
190
|
+
return self.__maxmin_rc('column', min)
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def min_row(self) -> int:
|
|
194
|
+
"""
|
|
195
|
+
The minimum row that still holds data. Raises UnsupportedOperationException when the sheet is a chartsheet.
|
|
196
|
+
:return: `int`
|
|
197
|
+
"""
|
|
198
|
+
return self.__maxmin_rc('row', min)
|
|
199
|
+
|
|
200
|
+
def __maxmin_rc_in_cr(self, cr: str, mm_fn: MaxMinFunction, idx: int) -> int:
|
|
201
|
+
"""
|
|
202
|
+
The abstracted method for `max_column_in_row` and `max_row_in_column`.
|
|
203
|
+
:param cr: `str` indicating whether to search the `"column"` or `"row"` for the max row/col.
|
|
204
|
+
:param mm_fn: The function for whether to get the max or the min.
|
|
205
|
+
:param idx: `int` indicating which column/row to search through
|
|
206
|
+
"""
|
|
207
|
+
rc = 'column' if cr == 'row' else 'row'
|
|
208
|
+
if self.type == SHEET_TYPE_OBJECT:
|
|
209
|
+
raise UnsupportedOperationException('Chartsheet does not have ' + rc)
|
|
210
|
+
|
|
211
|
+
content_cells = self.__get_non_empty_cells()
|
|
212
|
+
content_cells = [self.__ce2c(c) for c in content_cells]
|
|
213
|
+
content_cells = [getattr(c, rc) for c in content_cells if getattr(c, cr) == idx]
|
|
214
|
+
return -1 if len(content_cells) == 0 else mm_fn(content_cells)
|
|
215
|
+
|
|
216
|
+
def max_column_in_row(self, row: int) -> int:
|
|
217
|
+
"""
|
|
218
|
+
Get the last column in `row` that has a value. Returns -1 if the row is empty. Raises
|
|
219
|
+
UnsupportedOperationException when the sheet is a chartsheet.
|
|
220
|
+
"""
|
|
221
|
+
return self.__maxmin_rc_in_cr('row', max, row)
|
|
222
|
+
|
|
223
|
+
def max_row_in_column(self, column: int) -> int:
|
|
224
|
+
"""
|
|
225
|
+
Get the last row in `column` that has a value. Returns -1 if the column is empty. Raises
|
|
226
|
+
UnsupportedOperationException when the sheet is a chartsheet.
|
|
227
|
+
"""
|
|
228
|
+
return self.__maxmin_rc_in_cr('column', max, column)
|
|
229
|
+
|
|
230
|
+
def min_column_in_row(self, row: int) -> int:
|
|
231
|
+
"""
|
|
232
|
+
Get the first column in `row` that has a value. Returns -1 if the row is empty. Raises
|
|
233
|
+
UnsupportedOperationException when the sheet is a chartsheet.
|
|
234
|
+
"""
|
|
235
|
+
return self.__maxmin_rc_in_cr('row', min, row)
|
|
236
|
+
|
|
237
|
+
def min_row_in_column(self, column: int) -> int:
|
|
238
|
+
"""
|
|
239
|
+
Get the first row in `column` that has a value. Returns -1 if the column is empty. Raises
|
|
240
|
+
UnsupportedOperationException when the sheet is a chartsheet.
|
|
241
|
+
"""
|
|
242
|
+
return self.__maxmin_rc_in_cr('column', min, column)
|
|
243
|
+
|
|
244
|
+
def __max_allowed_rc(self, rc: str) -> int:
|
|
245
|
+
"""
|
|
246
|
+
The abstracted method for `max_allowed_column` and `max_allowed_row`.
|
|
247
|
+
:param rc: `str` indicating whether this is for `"column"` or `"row"`.
|
|
248
|
+
"""
|
|
249
|
+
rc = 'Cols' if rc == 'column' else 'Rows'
|
|
250
|
+
key = '{%s}%s' % (self.__workbook._ns['gnm'], rc)
|
|
251
|
+
return int(self.__sheet_name.get(key)) - 1
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def max_allowed_column(self) -> int:
|
|
255
|
+
"""
|
|
256
|
+
The maximum column allowed in the worksheet.
|
|
257
|
+
:return: `int`
|
|
258
|
+
"""
|
|
259
|
+
return self.__max_allowed_rc('column')
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def max_allowed_row(self) -> int:
|
|
263
|
+
"""
|
|
264
|
+
The maximum row allowed in the worksheet.
|
|
265
|
+
:return: `int`
|
|
266
|
+
"""
|
|
267
|
+
return self.__max_allowed_rc('row')
|
|
268
|
+
|
|
269
|
+
def calculate_dimension(self) -> Tuple[int, int, int, int]:
|
|
270
|
+
"""
|
|
271
|
+
The minimum bounding rectangle that contains all data in the worksheet
|
|
272
|
+
|
|
273
|
+
Raises UnsupportedOperationException when the sheet is a chartsheet.
|
|
274
|
+
|
|
275
|
+
:return: A four-tuple of ints: (min_row, min_col, max_row, max_col)
|
|
276
|
+
"""
|
|
277
|
+
if self.type == SHEET_TYPE_OBJECT:
|
|
278
|
+
raise UnsupportedOperationException(
|
|
279
|
+
'Chartsheet does not have rows or columns'
|
|
280
|
+
)
|
|
281
|
+
return self.min_row, self.min_column, self.max_row, self.max_column
|
|
282
|
+
|
|
283
|
+
def __is_valid_rc(self, rc: str, idx: int) -> bool:
|
|
284
|
+
"""
|
|
285
|
+
The abstracted method for `is_valid_column` and `is_valid_row`.
|
|
286
|
+
|
|
287
|
+
:param rc: A string indiciating whether this is for a `"column"` or `"row"`.
|
|
288
|
+
:param idx: The column or row.
|
|
289
|
+
:return: bool
|
|
290
|
+
"""
|
|
291
|
+
max_allowed = 'max_allowed_' + rc
|
|
292
|
+
return 0 <= idx <= getattr(self, max_allowed)
|
|
293
|
+
|
|
294
|
+
def is_valid_column(self, column: int) -> bool:
|
|
295
|
+
"""
|
|
296
|
+
Returns `True` if column is between `0` and `ws.max_allowed_column`, otherwise returns `False`
|
|
297
|
+
:return: bool
|
|
298
|
+
"""
|
|
299
|
+
return self.__is_valid_rc('column', column)
|
|
300
|
+
|
|
301
|
+
def is_valid_row(self, row: int) -> bool:
|
|
302
|
+
"""
|
|
303
|
+
Returns `True` if row is between `0` and `ws.max_allowed_row`, otherwise returns `False`
|
|
304
|
+
:return: bool
|
|
305
|
+
"""
|
|
306
|
+
return self.__is_valid_rc('row', row)
|
|
307
|
+
|
|
308
|
+
def cell(self, row_idx: int, col_idx: int, *, create: bool = True) -> Cell:
|
|
309
|
+
"""
|
|
310
|
+
Returns a Cell object for the cell at the specific row and column.
|
|
311
|
+
|
|
312
|
+
If the cell does not exist, then an empty cell will be created and returned, unless `create` is `False` (in
|
|
313
|
+
which case, `IndexError` is raised). Note that the cell will not be added to the worksheet until it is not
|
|
314
|
+
empty (since Gnumeric does not seem to store empty cells).
|
|
315
|
+
"""
|
|
316
|
+
if not self.is_valid_row(row_idx):
|
|
317
|
+
raise IndexError(
|
|
318
|
+
f'Row ({row_idx}) for cell is out of allowed bounds of [0, {self.max_allowed_row}]'
|
|
319
|
+
)
|
|
320
|
+
elif not self.is_valid_column(col_idx):
|
|
321
|
+
raise IndexError(
|
|
322
|
+
f'Column ({col_idx}) for cell is out of allowed bounds of [0, {self.max_allowed_column}]'
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
cell_found = self.__get_cell_element(row_idx, col_idx)
|
|
326
|
+
if cell_found is None:
|
|
327
|
+
if create:
|
|
328
|
+
cell_found = self.__create_and_get_new_cell(row_idx, col_idx)
|
|
329
|
+
else:
|
|
330
|
+
raise IndexError(f'No cell exists at position ({row_idx}, {col_idx})')
|
|
331
|
+
|
|
332
|
+
return self.__ce2c(cell_found)
|
|
333
|
+
|
|
334
|
+
def __getitem__(self, idx: Union[RowColReference, str]) -> Cell:
|
|
335
|
+
if isinstance(idx, tuple) and len(idx) == 2:
|
|
336
|
+
return self.cell(*idx)
|
|
337
|
+
elif isinstance(idx, str):
|
|
338
|
+
return self.cell(*coordinate_from_spreadsheet(idx))
|
|
339
|
+
raise IndexError(f'Unrecognized index: {idx!r}')
|
|
340
|
+
|
|
341
|
+
def cell_text(self, row_idx: int, col_idx: int) -> str:
|
|
342
|
+
"""
|
|
343
|
+
Returns the cell's text at the specific row and column.
|
|
344
|
+
|
|
345
|
+
If the cell does not exist, then it will raise an IndexError.
|
|
346
|
+
"""
|
|
347
|
+
return self.cell(row_idx, col_idx, create=False).text
|
|
348
|
+
|
|
349
|
+
def __sort_cell_elements(self, cell_elements, row_major: bool) -> List[Cell]:
|
|
350
|
+
"""
|
|
351
|
+
Sort the cells according to indices. If `row_major` is True, then sorting will occur by row first, then within
|
|
352
|
+
each row, columns will be sorted. If `row_major` is False, then the opposite will happen: first sort by column,
|
|
353
|
+
then by row within each column.
|
|
354
|
+
|
|
355
|
+
:returns: A list of Cell objects
|
|
356
|
+
"""
|
|
357
|
+
return self.__sort_cells([self.__ce2c(c) for c in cell_elements], row_major)
|
|
358
|
+
|
|
359
|
+
def __sort_cells(self, cells: Sequence[Cell], row_major: bool) -> List[Cell]:
|
|
360
|
+
"""
|
|
361
|
+
Sort the cells according to indices. If `row_major` is True, then sorting will occur by row first, then within
|
|
362
|
+
each row, columns will be sorted. If `row_major` is False, then the opposite will happen: first sort by column,
|
|
363
|
+
then by row within each column.
|
|
364
|
+
"""
|
|
365
|
+
key_fn = (
|
|
366
|
+
attrgetter('row', 'column') if row_major else attrgetter('column', 'row')
|
|
367
|
+
)
|
|
368
|
+
return sorted(cells, key=key_fn)
|
|
369
|
+
|
|
370
|
+
def get_cell_collection(
|
|
371
|
+
self,
|
|
372
|
+
start: Optional[Union[str, RowColReference, Cell]] = None,
|
|
373
|
+
end: Optional[Union[str, RowColReference, Cell]] = None,
|
|
374
|
+
*,
|
|
375
|
+
include_empty: bool = False,
|
|
376
|
+
create_cells: bool = False,
|
|
377
|
+
sort: Union[bool, str] = False,
|
|
378
|
+
) -> List[Cell]:
|
|
379
|
+
"""
|
|
380
|
+
Return cells (as a list).
|
|
381
|
+
|
|
382
|
+
If `start` and `end` are provided, then only cells within the (inclusive) range are returned. If just `start`
|
|
383
|
+
is given, then everything in a position greater than or equal to that cell's position are included. If just
|
|
384
|
+
`end` is given, then everything in a position less than or equal to that cell's position are included. `start`
|
|
385
|
+
and `end` can be 'A1'-style coordinates, a (row, col) tuple (with 0-based indexes), or Cell objects.
|
|
386
|
+
|
|
387
|
+
If `include_empty` is False (default), then only cells with content will be included. If `include_empty` is
|
|
388
|
+
True, then empty cells that have been created will be included. To get all empty cells, including those not
|
|
389
|
+
already created, set `create_cells` to True.
|
|
390
|
+
|
|
391
|
+
Use `sort` to specify whether the cells should be sorted. If `False` (default), then no sorting will take
|
|
392
|
+
place. If `sort` is `"row"`, then sorting will occur by row first, then by column within each row. If `sort`
|
|
393
|
+
is `"column"`, then the opposite will happen: first sort by column, then by row within each column.
|
|
394
|
+
"""
|
|
395
|
+
if include_empty:
|
|
396
|
+
cells = self.__get_cells()
|
|
397
|
+
else:
|
|
398
|
+
cells = self.__get_non_empty_cells()
|
|
399
|
+
|
|
400
|
+
cells = [self.__ce2c(c) for c in cells]
|
|
401
|
+
|
|
402
|
+
if start is None:
|
|
403
|
+
start = (0, 0)
|
|
404
|
+
elif isinstance(start, Cell):
|
|
405
|
+
start = (start.row, start.column)
|
|
406
|
+
elif isinstance(start, str):
|
|
407
|
+
start = coordinate_from_spreadsheet(start)
|
|
408
|
+
|
|
409
|
+
if end is None:
|
|
410
|
+
end = (self.max_allowed_row, self.max_allowed_column)
|
|
411
|
+
elif isinstance(end, Cell):
|
|
412
|
+
end = (end.row, end.column)
|
|
413
|
+
elif isinstance(end, str):
|
|
414
|
+
end = coordinate_from_spreadsheet(end)
|
|
415
|
+
|
|
416
|
+
start_row, start_column = start
|
|
417
|
+
end_row, end_column = end
|
|
418
|
+
cells = [
|
|
419
|
+
c
|
|
420
|
+
for c in cells
|
|
421
|
+
if start_column <= c.column <= end_column and start_row <= c.row <= end_row
|
|
422
|
+
]
|
|
423
|
+
|
|
424
|
+
if create_cells:
|
|
425
|
+
already_created_cells = {(c.row, c.column) for c in cells}
|
|
426
|
+
for row, col in product(
|
|
427
|
+
range(start_row, end_row + 1), range(start_column, end_column + 1)
|
|
428
|
+
):
|
|
429
|
+
if (row, col) not in already_created_cells:
|
|
430
|
+
cells.append(self.cell(row, col, create=True))
|
|
431
|
+
|
|
432
|
+
if sort:
|
|
433
|
+
return self.__sort_cells(cells, sort == 'row')
|
|
434
|
+
else:
|
|
435
|
+
return cells
|
|
436
|
+
|
|
437
|
+
def __get_rc(
|
|
438
|
+
self, rc: str, idx: int, min_cr: int, max_cr: Optional[int], create_cells: bool
|
|
439
|
+
) -> Generator[Cell, None, None]:
|
|
440
|
+
"""
|
|
441
|
+
The abstracted method for `get_column` and `get_row`.
|
|
442
|
+
|
|
443
|
+
:param rc: `str` indiciating whether this is for `"column"` or `"row"`.
|
|
444
|
+
"""
|
|
445
|
+
cr = 'row' if rc == 'column' else 'column'
|
|
446
|
+
rc_attr = rc[:3].title()
|
|
447
|
+
cr_attr = cr[:3].title()
|
|
448
|
+
if not self.__is_valid_rc(rc, idx):
|
|
449
|
+
raise IndexError(
|
|
450
|
+
rc.title()
|
|
451
|
+
+ ' ('
|
|
452
|
+
+ str(idx)
|
|
453
|
+
+ ') is out of allowed bounds of [0, '
|
|
454
|
+
+ str(self.__max_allowed_rc(rc))
|
|
455
|
+
+ ']'
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
if not self.__is_valid_rc(cr, min_cr):
|
|
459
|
+
raise IndexError(
|
|
460
|
+
'Min '
|
|
461
|
+
+ cr
|
|
462
|
+
+ ' ('
|
|
463
|
+
+ str(min_cr)
|
|
464
|
+
+ ') is out of allowed bounds of [0, '
|
|
465
|
+
+ str(self.__max_allowed_rc(cr))
|
|
466
|
+
+ ']'
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
if max_cr is not None and not self.__is_valid_rc(cr, max_cr):
|
|
470
|
+
raise IndexError(
|
|
471
|
+
'Max '
|
|
472
|
+
+ cr
|
|
473
|
+
+ ' ('
|
|
474
|
+
+ str(max_cr)
|
|
475
|
+
+ ') is out of allowed bounds of [0, '
|
|
476
|
+
+ str(self.__max_allowed_rc(cr))
|
|
477
|
+
+ ']'
|
|
478
|
+
)
|
|
479
|
+
elif max_cr is None:
|
|
480
|
+
max_cr = self.__maxmin_rc_in_cr(rc, max, idx)
|
|
481
|
+
if max_cr == -1:
|
|
482
|
+
max_cr = self.__max_allowed_rc(cr)
|
|
483
|
+
|
|
484
|
+
existing_cells = self.__get_cells().xpath(
|
|
485
|
+
'./gnm:Cell[@'
|
|
486
|
+
+ rc_attr
|
|
487
|
+
+ '="'
|
|
488
|
+
+ str(idx)
|
|
489
|
+
+ '" and "'
|
|
490
|
+
+ str(min_cr)
|
|
491
|
+
+ '"<=@'
|
|
492
|
+
+ cr_attr
|
|
493
|
+
+ ' and @'
|
|
494
|
+
+ cr_attr
|
|
495
|
+
+ '<="'
|
|
496
|
+
+ str(max_cr)
|
|
497
|
+
+ '"]',
|
|
498
|
+
namespaces=self.__workbook._ns,
|
|
499
|
+
)
|
|
500
|
+
cells = self.__sort_cell_elements(existing_cells, False)
|
|
501
|
+
|
|
502
|
+
if create_cells:
|
|
503
|
+
cell_map = dict([(getattr(c, cr), c) for c in cells])
|
|
504
|
+
return (
|
|
505
|
+
cell_map[i] if i in cell_map else self.cell(i, idx)
|
|
506
|
+
for i in range(min_cr, max_cr + 1)
|
|
507
|
+
)
|
|
508
|
+
else:
|
|
509
|
+
return (c for c in cells)
|
|
510
|
+
|
|
511
|
+
def get_column(
|
|
512
|
+
self,
|
|
513
|
+
column: int,
|
|
514
|
+
*,
|
|
515
|
+
min_row: int = 0,
|
|
516
|
+
max_row: Optional[int] = None,
|
|
517
|
+
create_cells: bool = False,
|
|
518
|
+
) -> Generator[Cell, None, None]:
|
|
519
|
+
"""
|
|
520
|
+
Get the cells in the specified column (index starting at 0).
|
|
521
|
+
|
|
522
|
+
Use `min_row` and `max_row` to specify the range of cells within the column. `min_row` defaults to 0.
|
|
523
|
+
`max_row` defaults to `None`, meaning go to the last cell with a value in the column, but if the column is
|
|
524
|
+
empty, then go to the last allowed row. These bounds are inclusive.
|
|
525
|
+
|
|
526
|
+
If `create_cells` is `True`, then any cells in the column that don't exist, will be created. If `False` (the
|
|
527
|
+
default), then only already-existing cells will be returned. Note that existing cells that are empty will be
|
|
528
|
+
returned if `create_cells` is `False`.
|
|
529
|
+
|
|
530
|
+
Raises `IndexError` if `column` is outside of the valid range for columns, or if `min_row` or `max_row` is
|
|
531
|
+
outside the valid range for rows.
|
|
532
|
+
:return: generator
|
|
533
|
+
"""
|
|
534
|
+
return self.__get_rc('column', column, min_row, max_row, create_cells)
|
|
535
|
+
|
|
536
|
+
def get_row(
|
|
537
|
+
self,
|
|
538
|
+
row: int,
|
|
539
|
+
*,
|
|
540
|
+
min_col: int = 0,
|
|
541
|
+
max_col: Optional[int] = None,
|
|
542
|
+
create_cells: bool = False,
|
|
543
|
+
) -> Generator[Cell, None, None]:
|
|
544
|
+
"""
|
|
545
|
+
Get the cells in the specified row (index starting at 0).
|
|
546
|
+
|
|
547
|
+
Use `min_col` and `max_col` to specify the range of cells within the row. `min_col` defaults to 0. `max_col`
|
|
548
|
+
defaults to `None`, meaning go to the last cell with a value in the row, but if the row is empty, then go to the
|
|
549
|
+
last allowed column. These bounds are inclusive.
|
|
550
|
+
|
|
551
|
+
If `create_cells` is `True`, then any cells in the row that don't exist, will be created. If `False` (the
|
|
552
|
+
default), then only already-existing cells will be returned. Note that existing cells that are empty will be
|
|
553
|
+
returned if `create_cells` is `False`.
|
|
554
|
+
|
|
555
|
+
Raises `IndexError` if `row` is outside of the valid range for rows, or if `min_col` or `max_col` is outside
|
|
556
|
+
the valid range for columns.
|
|
557
|
+
:return: generator
|
|
558
|
+
"""
|
|
559
|
+
return self.__get_rc('row', row, min_col, max_col, create_cells)
|
|
560
|
+
|
|
561
|
+
def get_expression_map(self) -> Dict[str, Tuple[RowColReference, str]]:
|
|
562
|
+
"""
|
|
563
|
+
In each worksheet, Gnumeric stores an expression/formula once (in the cell it's first used), then references it
|
|
564
|
+
by an id in all other cells that use the expression. This method will return a dict of
|
|
565
|
+
expression ids -> ((cell_row, cell_col), expression).
|
|
566
|
+
|
|
567
|
+
Note that this might not return all expressions in the worksheet. If an expression is only used once, then it
|
|
568
|
+
may not have an id and thus will not be returned by this method.
|
|
569
|
+
"""
|
|
570
|
+
cells = self.__get_expression_id_cells()
|
|
571
|
+
return {
|
|
572
|
+
c.get('ExprID'): (
|
|
573
|
+
RowColReference(int(c.get('Row')), int(c.get('Col'))),
|
|
574
|
+
c.text,
|
|
575
|
+
)
|
|
576
|
+
for c in cells
|
|
577
|
+
if c.text is not None
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
def get_all_cells_with_expression(
|
|
581
|
+
self, id: str, *, sort: Union[bool, str] = False
|
|
582
|
+
) -> List[Cell]:
|
|
583
|
+
"""
|
|
584
|
+
Returns a list of all cells referencing/using the expression with the provided id.
|
|
585
|
+
|
|
586
|
+
Use `sort` to specify whether the cells should be sorted. If `False` (default), then no sorting will take
|
|
587
|
+
place. If `sort` is `"row"`, then sorting will occur by row first, then by column within each row. If `sort`
|
|
588
|
+
is `"column"`, then the opposite will happen: first sort by column, then by row within each column.
|
|
589
|
+
"""
|
|
590
|
+
cells = self.__get_expression_id_cells()
|
|
591
|
+
cells = [self.__ce2c(c) for c in cells if c.get('ExprID') == id]
|
|
592
|
+
if sort:
|
|
593
|
+
return self.__sort_cells(cells, sort == 'row')
|
|
594
|
+
else:
|
|
595
|
+
return cells
|
|
596
|
+
|
|
597
|
+
def delete_cell(self, row: int, col: int) -> None:
|
|
598
|
+
"""
|
|
599
|
+
Deletes the cell at the specified row and column. If the cell doesn't exist, then nothing will happen. If the
|
|
600
|
+
cell is the originating cell for an expression, then an exception is thrown (deleting these cells is not yet
|
|
601
|
+
supported).
|
|
602
|
+
"""
|
|
603
|
+
cell = self.__get_cell_element(row, col)
|
|
604
|
+
if cell is None:
|
|
605
|
+
return
|
|
606
|
+
elif cell.get('ExprID') is not None and cell.text is not None:
|
|
607
|
+
raise UnsupportedOperationException(
|
|
608
|
+
"Can't delete originating cell for an expression"
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
all_cells = self.__get_cells()
|
|
612
|
+
all_cells.remove(cell)
|
|
613
|
+
|
|
614
|
+
def _clean_data(self) -> None:
|
|
615
|
+
"""
|
|
616
|
+
Performs housekeeping on the data. Only necessary when contents are being written to file. Should not be
|
|
617
|
+
called directly -- the workbook will call this automatically when writing to file.
|
|
618
|
+
"""
|
|
619
|
+
|
|
620
|
+
# Delete empty cells
|
|
621
|
+
all_cells = self.__get_cells()
|
|
622
|
+
empty_cells = self.__get_empty_cells()
|
|
623
|
+
for empty_cell in empty_cells:
|
|
624
|
+
all_cells.remove(empty_cell)
|
|
625
|
+
|
|
626
|
+
# Update max col and row
|
|
627
|
+
self.__sheet.find('gnm:MaxCol', self.__workbook._ns).text = str(self.max_column)
|
|
628
|
+
self.__sheet.find('gnm:MaxRow', self.__workbook._ns).text = str(self.max_row)
|
|
629
|
+
|
|
630
|
+
def __eq__(self, other) -> bool:
|
|
631
|
+
return (
|
|
632
|
+
isinstance(other, Sheet)
|
|
633
|
+
and self.__workbook == other.__workbook
|
|
634
|
+
and self.__sheet_name == other.__sheet_name
|
|
635
|
+
and self.__sheet == other.__sheet
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
def __hash__(self) -> int:
|
|
639
|
+
return hash(self.__workbook) + hash(self.__sheet)
|
|
640
|
+
|
|
641
|
+
def __str__(self) -> str:
|
|
642
|
+
return self.title
|