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/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