numbers-parser 4.4.6__py3-none-any.whl → 4.4.8__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.
- numbers_parser/__init__.py +7 -5
- numbers_parser/_cat_numbers.py +1 -2
- numbers_parser/_unpack_numbers.py +14 -16
- numbers_parser/bullets.py +1 -1
- numbers_parser/cell.py +33 -29
- numbers_parser/cell_storage.py +36 -29
- numbers_parser/constants.py +2 -2
- numbers_parser/containers.py +12 -14
- numbers_parser/document.py +54 -45
- numbers_parser/exceptions.py +7 -7
- numbers_parser/file.py +18 -8
- numbers_parser/formula.py +7 -3
- numbers_parser/iwafile.py +23 -27
- numbers_parser/mapping.py +24 -24
- numbers_parser/model.py +112 -131
- numbers_parser/numbers_cache.py +3 -2
- {numbers_parser-4.4.6.dist-info → numbers_parser-4.4.8.dist-info}/METADATA +8 -5
- {numbers_parser-4.4.6.dist-info → numbers_parser-4.4.8.dist-info}/RECORD +21 -21
- {numbers_parser-4.4.6.dist-info → numbers_parser-4.4.8.dist-info}/LICENSE.rst +0 -0
- {numbers_parser-4.4.6.dist-info → numbers_parser-4.4.8.dist-info}/WHEEL +0 -0
- {numbers_parser-4.4.6.dist-info → numbers_parser-4.4.8.dist-info}/entry_points.txt +0 -0
numbers_parser/document.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from typing import Generator, Tuple, Union
|
|
2
2
|
from warnings import warn
|
|
3
|
+
|
|
3
4
|
from numbers_parser.cell import Border, Cell, MergedCell, Style, xl_cell_to_rowcol
|
|
4
5
|
from numbers_parser.cell_storage import CellStorage
|
|
5
6
|
from numbers_parser.constants import (
|
|
@@ -13,11 +14,11 @@ from numbers_parser.constants import (
|
|
|
13
14
|
from numbers_parser.containers import ItemsList
|
|
14
15
|
from numbers_parser.file import write_numbers_file
|
|
15
16
|
from numbers_parser.model import _NumbersModel
|
|
16
|
-
from numbers_parser.numbers_cache import
|
|
17
|
+
from numbers_parser.numbers_cache import Cacheable, cache
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class Document:
|
|
20
|
-
def __init__(
|
|
21
|
+
def __init__( # noqa: PLR0913
|
|
21
22
|
self,
|
|
22
23
|
filename: str = None,
|
|
23
24
|
sheet_name: str = None,
|
|
@@ -35,7 +36,11 @@ class Document:
|
|
|
35
36
|
or (num_rows is not None)
|
|
36
37
|
or (num_cols is not None)
|
|
37
38
|
):
|
|
38
|
-
warn(
|
|
39
|
+
warn(
|
|
40
|
+
"can't set table/sheet attributes on load of existing document",
|
|
41
|
+
RuntimeWarning,
|
|
42
|
+
stacklevel=2,
|
|
43
|
+
)
|
|
39
44
|
|
|
40
45
|
self._model = _NumbersModel(filename)
|
|
41
46
|
refs = self._model.sheet_ids()
|
|
@@ -65,12 +70,12 @@ class Document:
|
|
|
65
70
|
|
|
66
71
|
@property
|
|
67
72
|
def sheets(self) -> list:
|
|
68
|
-
"""Return a list of all sheets in the document"""
|
|
73
|
+
"""Return a list of all sheets in the document."""
|
|
69
74
|
return self._sheets
|
|
70
75
|
|
|
71
76
|
@property
|
|
72
77
|
def styles(self) -> list:
|
|
73
|
-
"""Return a list of styles available in the document"""
|
|
78
|
+
"""Return a list of styles available in the document."""
|
|
74
79
|
return self._model.styles
|
|
75
80
|
|
|
76
81
|
def save(self, filename):
|
|
@@ -87,7 +92,8 @@ class Document:
|
|
|
87
92
|
num_cols=DEFAULT_COLUMN_COUNT,
|
|
88
93
|
) -> object:
|
|
89
94
|
"""Add a new sheet to the current document. If no sheet name is provided,
|
|
90
|
-
the next available numbered sheet will be generated
|
|
95
|
+
the next available numbered sheet will be generated.
|
|
96
|
+
"""
|
|
91
97
|
if sheet_name is not None:
|
|
92
98
|
if sheet_name in self._sheets:
|
|
93
99
|
raise IndexError(f"sheet '{sheet_name}' already exists")
|
|
@@ -117,10 +123,10 @@ class Document:
|
|
|
117
123
|
|
|
118
124
|
def add_style(self, **kwargs) -> Style:
|
|
119
125
|
"""Add a new style to the current document. If no style name is
|
|
120
|
-
provided, the next available numbered style will be generated
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
126
|
+
provided, the next available numbered style will be generated.
|
|
127
|
+
"""
|
|
128
|
+
if "name" in kwargs and kwargs["name"] is not None and kwargs["name"] in self._model.styles:
|
|
129
|
+
raise IndexError(f"style '{kwargs['name']}' already exists")
|
|
124
130
|
style = Style(**kwargs)
|
|
125
131
|
if style.name is None:
|
|
126
132
|
style.name = self._model.custom_style_name()
|
|
@@ -142,15 +148,15 @@ class Sheet:
|
|
|
142
148
|
|
|
143
149
|
@property
|
|
144
150
|
def name(self):
|
|
145
|
-
"""Return the sheets name"""
|
|
151
|
+
"""Return the sheets name."""
|
|
146
152
|
return self._model.sheet_name(self._sheet_id)
|
|
147
153
|
|
|
148
154
|
@name.setter
|
|
149
155
|
def name(self, value):
|
|
150
|
-
"""Set the sheet's name"""
|
|
156
|
+
"""Set the sheet's name."""
|
|
151
157
|
self._model.sheet_name(self._sheet_id, value)
|
|
152
158
|
|
|
153
|
-
def add_table(
|
|
159
|
+
def add_table( # noqa: PLR0913
|
|
154
160
|
self,
|
|
155
161
|
table_name=None,
|
|
156
162
|
x=None,
|
|
@@ -159,12 +165,14 @@ class Sheet:
|
|
|
159
165
|
num_cols=DEFAULT_COLUMN_COUNT,
|
|
160
166
|
) -> object:
|
|
161
167
|
"""Add a new table to the current sheet. If no table name is provided,
|
|
162
|
-
the next available numbered table will be generated
|
|
163
|
-
|
|
168
|
+
the next available numbered table will be generated.
|
|
169
|
+
"""
|
|
164
170
|
from_table_id = self._tables[-1]._table_id
|
|
165
171
|
return self._add_table(table_name, from_table_id, x, y, num_rows, num_cols)
|
|
166
172
|
|
|
167
|
-
def _add_table(
|
|
173
|
+
def _add_table(
|
|
174
|
+
self, table_name, from_table_id, x, y, num_rows, num_cols
|
|
175
|
+
) -> object: # noqa: PLR0913
|
|
168
176
|
if table_name is not None:
|
|
169
177
|
if table_name in self._tables:
|
|
170
178
|
raise IndexError(f"table '{table_name}' already exists")
|
|
@@ -183,7 +191,7 @@ class Sheet:
|
|
|
183
191
|
|
|
184
192
|
class Table(Cacheable):
|
|
185
193
|
def __init__(self, model, table_id):
|
|
186
|
-
super(
|
|
194
|
+
super().__init__()
|
|
187
195
|
self._model = model
|
|
188
196
|
self._table_id = table_id
|
|
189
197
|
self.num_rows = self._model.number_of_rows(self._table_id)
|
|
@@ -209,12 +217,12 @@ class Table(Cacheable):
|
|
|
209
217
|
|
|
210
218
|
@property
|
|
211
219
|
def name(self) -> str:
|
|
212
|
-
"""Return the table's name"""
|
|
220
|
+
"""Return the table's name."""
|
|
213
221
|
return self._model.table_name(self._table_id)
|
|
214
222
|
|
|
215
223
|
@name.setter
|
|
216
224
|
def name(self, value: str):
|
|
217
|
-
"""Set the table's name"""
|
|
225
|
+
"""Set the table's name."""
|
|
218
226
|
self._model.table_name(self._table_id, value)
|
|
219
227
|
|
|
220
228
|
@property
|
|
@@ -227,12 +235,12 @@ class Table(Cacheable):
|
|
|
227
235
|
|
|
228
236
|
@property
|
|
229
237
|
def num_header_rows(self) -> int:
|
|
230
|
-
"""Return the number of header rows"""
|
|
238
|
+
"""Return the number of header rows."""
|
|
231
239
|
return self._model.num_header_rows(self._table_id)
|
|
232
240
|
|
|
233
241
|
@num_header_rows.setter
|
|
234
242
|
def num_header_rows(self, num_headers: int):
|
|
235
|
-
"""Return the number of header rows"""
|
|
243
|
+
"""Return the number of header rows."""
|
|
236
244
|
if num_headers < 0:
|
|
237
245
|
raise ValueError("Number of headers cannot be negative")
|
|
238
246
|
elif num_headers > self.num_rows:
|
|
@@ -243,12 +251,12 @@ class Table(Cacheable):
|
|
|
243
251
|
|
|
244
252
|
@property
|
|
245
253
|
def num_header_cols(self) -> int:
|
|
246
|
-
"""Return the number of header columns"""
|
|
254
|
+
"""Return the number of header columns."""
|
|
247
255
|
return self._model.num_header_cols(self._table_id)
|
|
248
256
|
|
|
249
257
|
@num_header_cols.setter
|
|
250
258
|
def num_header_cols(self, num_headers: int):
|
|
251
|
-
"""Return the number of header columns"""
|
|
259
|
+
"""Return the number of header columns."""
|
|
252
260
|
if num_headers < 0:
|
|
253
261
|
raise ValueError("Number of headers cannot be negative")
|
|
254
262
|
elif num_headers > self.num_cols:
|
|
@@ -259,30 +267,29 @@ class Table(Cacheable):
|
|
|
259
267
|
|
|
260
268
|
@property
|
|
261
269
|
def height(self) -> int:
|
|
262
|
-
"""Return the table's height in points"""
|
|
270
|
+
"""Return the table's height in points."""
|
|
263
271
|
return self._model.table_height(self._table_id)
|
|
264
272
|
|
|
265
273
|
@property
|
|
266
274
|
def width(self) -> int:
|
|
267
|
-
"""Return the table's width in points"""
|
|
275
|
+
"""Return the table's width in points."""
|
|
268
276
|
return self._model.table_width(self._table_id)
|
|
269
277
|
|
|
270
278
|
def row_height(self, row_num: int, height: int = None) -> int:
|
|
271
|
-
"""Return the height of a table row. Set the height if not None"""
|
|
279
|
+
"""Return the height of a table row. Set the height if not None."""
|
|
272
280
|
return self._model.row_height(self._table_id, row_num, height)
|
|
273
281
|
|
|
274
282
|
def col_width(self, col_num: int, width: int = None) -> int:
|
|
275
|
-
"""Return the width of a table column. Set the width if not None"""
|
|
283
|
+
"""Return the width of a table column. Set the width if not None."""
|
|
276
284
|
return self._model.col_width(self._table_id, col_num, width)
|
|
277
285
|
|
|
278
286
|
@property
|
|
279
287
|
def coordinates(self) -> Tuple[float]:
|
|
280
|
-
"""Return the table's x,y offsets in points"""
|
|
288
|
+
"""Return the table's x,y offsets in points."""
|
|
281
289
|
return self._model.table_coordinates(self._table_id)
|
|
282
290
|
|
|
283
291
|
def rows(self, values_only: bool = False) -> list:
|
|
284
|
-
"""
|
|
285
|
-
Return all rows of cells for the Table.
|
|
292
|
+
"""Return all rows of cells for the Table.
|
|
286
293
|
|
|
287
294
|
Args:
|
|
288
295
|
values_only: if True, return cell values instead of Cell objects
|
|
@@ -291,8 +298,7 @@ class Table(Cacheable):
|
|
|
291
298
|
rows: list of rows; each row is a list of Cell objects
|
|
292
299
|
"""
|
|
293
300
|
if values_only:
|
|
294
|
-
|
|
295
|
-
return rows
|
|
301
|
+
return [[cell.value for cell in row] for row in self._data]
|
|
296
302
|
else:
|
|
297
303
|
return self._data
|
|
298
304
|
|
|
@@ -317,7 +323,7 @@ class Table(Cacheable):
|
|
|
317
323
|
|
|
318
324
|
return self._data[row_num][col_num]
|
|
319
325
|
|
|
320
|
-
def iter_rows(
|
|
326
|
+
def iter_rows( # noqa: PLR0913
|
|
321
327
|
self,
|
|
322
328
|
min_row: int = None,
|
|
323
329
|
max_row: int = None,
|
|
@@ -325,8 +331,7 @@ class Table(Cacheable):
|
|
|
325
331
|
max_col: int = None,
|
|
326
332
|
values_only: bool = False,
|
|
327
333
|
) -> Generator[tuple, None, None]:
|
|
328
|
-
"""
|
|
329
|
-
Produces cells from a table, by row. Specify the iteration range using
|
|
334
|
+
"""Produces cells from a table, by row. Specify the iteration range using
|
|
330
335
|
the indexes of the rows and columns.
|
|
331
336
|
|
|
332
337
|
Args:
|
|
@@ -363,7 +368,7 @@ class Table(Cacheable):
|
|
|
363
368
|
else:
|
|
364
369
|
yield tuple(rows[row_num][min_col : max_col + 1])
|
|
365
370
|
|
|
366
|
-
def iter_cols(
|
|
371
|
+
def iter_cols( # noqa: PLR0913
|
|
367
372
|
self,
|
|
368
373
|
min_col: int = None,
|
|
369
374
|
max_col: int = None,
|
|
@@ -371,8 +376,7 @@ class Table(Cacheable):
|
|
|
371
376
|
max_row: int = None,
|
|
372
377
|
values_only: bool = False,
|
|
373
378
|
) -> Generator[tuple, None, None]:
|
|
374
|
-
"""
|
|
375
|
-
Produces cells from a table, by column. Specify the iteration range using
|
|
379
|
+
"""Produces cells from a table, by column. Specify the iteration range using
|
|
376
380
|
the indexes of the rows and columns.
|
|
377
381
|
|
|
378
382
|
Args:
|
|
@@ -411,7 +415,8 @@ class Table(Cacheable):
|
|
|
411
415
|
|
|
412
416
|
def _validate_cell_coords(self, *args):
|
|
413
417
|
"""Check first arguments are value cell references and pad
|
|
414
|
-
the table with empty cells if outside current bounds
|
|
418
|
+
the table with empty cells if outside current bounds.
|
|
419
|
+
"""
|
|
415
420
|
if isinstance(args[0], str):
|
|
416
421
|
(row_num, col_num) = xl_cell_to_rowcol(args[0])
|
|
417
422
|
values = args[1:]
|
|
@@ -426,10 +431,10 @@ class Table(Cacheable):
|
|
|
426
431
|
if col_num >= MAX_COL_COUNT:
|
|
427
432
|
raise IndexError(f"{col_num} exceeds maximum column {MAX_COL_COUNT-1}")
|
|
428
433
|
|
|
429
|
-
for
|
|
434
|
+
for _ in range(self.num_rows, row_num + 1):
|
|
430
435
|
self.add_row()
|
|
431
436
|
|
|
432
|
-
for
|
|
437
|
+
for _ in range(self.num_cols, col_num + 1):
|
|
433
438
|
self.add_column()
|
|
434
439
|
|
|
435
440
|
return (row_num, col_num) + tuple(values)
|
|
@@ -480,7 +485,7 @@ class Table(Cacheable):
|
|
|
480
485
|
self._model.number_of_columns(self._table_id, self.num_cols)
|
|
481
486
|
|
|
482
487
|
def merge_cells(self, cell_range):
|
|
483
|
-
"""Convert a cell range or list of cell ranges into merged cells"""
|
|
488
|
+
"""Convert a cell range or list of cell ranges into merged cells."""
|
|
484
489
|
if isinstance(cell_range, list):
|
|
485
490
|
for x in cell_range:
|
|
486
491
|
self.merge_cells(x)
|
|
@@ -526,17 +531,21 @@ class Table(Cacheable):
|
|
|
526
531
|
return
|
|
527
532
|
|
|
528
533
|
if self._data[row_num][col_num].is_merged and side in ["bottom", "right"]:
|
|
529
|
-
warn(
|
|
534
|
+
warn(
|
|
535
|
+
f"cell [{row_num},{col_num}] is merged; {side} border not set",
|
|
536
|
+
RuntimeWarning,
|
|
537
|
+
stacklevel=2,
|
|
538
|
+
)
|
|
530
539
|
return
|
|
531
540
|
|
|
532
541
|
self._model.extract_strokes(self._table_id)
|
|
533
542
|
|
|
534
|
-
if side
|
|
543
|
+
if side in ["top", "bottom"]:
|
|
535
544
|
for border_col_num in range(col_num, col_num + length):
|
|
536
545
|
self._model.set_cell_border(
|
|
537
546
|
self._table_id, row_num, border_col_num, side, border_value
|
|
538
547
|
)
|
|
539
|
-
elif side
|
|
548
|
+
elif side in ["left", "right"]:
|
|
540
549
|
for border_row_num in range(row_num, row_num + length):
|
|
541
550
|
self._model.set_cell_border(
|
|
542
551
|
self._table_id, border_row_num, col_num, side, border_value
|
numbers_parser/exceptions.py
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
1
|
class NumbersError(Exception):
|
|
2
|
-
"""Base class for other exceptions"""
|
|
2
|
+
"""Base class for other exceptions."""
|
|
3
3
|
|
|
4
4
|
pass
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class UnsupportedError(NumbersError):
|
|
8
|
-
"""Raised for unsupported file format features"""
|
|
8
|
+
"""Raised for unsupported file format features."""
|
|
9
9
|
|
|
10
10
|
pass
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class NotImplementedError(NumbersError):
|
|
14
|
-
"""Raised for missing Protobufs"""
|
|
14
|
+
"""Raised for missing Protobufs."""
|
|
15
15
|
|
|
16
16
|
pass
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class FileError(NumbersError):
|
|
20
|
-
"""Raised for IO and other OS errors"""
|
|
20
|
+
"""Raised for IO and other OS errors."""
|
|
21
21
|
|
|
22
22
|
pass
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
class FileFormatError(NumbersError):
|
|
26
|
-
"""Raised for parsing errors during file load"""
|
|
26
|
+
"""Raised for parsing errors during file load."""
|
|
27
27
|
|
|
28
28
|
pass
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
class FormulaError(NumbersError):
|
|
32
|
-
""" "Raise for formula evaluation errors"""
|
|
32
|
+
""" "Raise for formula evaluation errors."""
|
|
33
33
|
|
|
34
34
|
pass
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
class UnsupportedWarning(Warning):
|
|
38
|
-
"""Raised for unsupported file format features"""
|
|
38
|
+
"""Raised for unsupported file format features."""
|
|
39
39
|
|
|
40
40
|
pass
|
numbers_parser/file.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
3
|
from io import BytesIO
|
|
4
|
-
from
|
|
4
|
+
from sys import version_info
|
|
5
|
+
from zipfile import BadZipFile, ZipFile
|
|
5
6
|
|
|
6
7
|
from numbers_parser.exceptions import FileError, FileFormatError
|
|
7
8
|
from numbers_parser.iwafile import IWAFile, is_iwa_file
|
|
@@ -10,6 +11,15 @@ logger = logging.getLogger(__name__)
|
|
|
10
11
|
debug = logger.debug
|
|
11
12
|
|
|
12
13
|
|
|
14
|
+
def open_zipfile(file):
|
|
15
|
+
"""Open Zip file with the correct filename encoding supported by current python"""
|
|
16
|
+
# Coverage is python version dependent, so one path with always fail coverage
|
|
17
|
+
if version_info.minor >= 11: # pragma: no cover
|
|
18
|
+
return ZipFile(file, metadata_encoding="utf-8")
|
|
19
|
+
else: # pragma: no cover
|
|
20
|
+
return ZipFile(file)
|
|
21
|
+
|
|
22
|
+
|
|
13
23
|
def read_numbers_file(path, file_handler, object_handler=None):
|
|
14
24
|
debug("read_numbers_file: path=%s", path)
|
|
15
25
|
if os.path.isdir(path):
|
|
@@ -29,21 +39,21 @@ def read_numbers_file(path, file_handler, object_handler=None):
|
|
|
29
39
|
file_handler(os.path.join(path, filename), blob)
|
|
30
40
|
else:
|
|
31
41
|
try:
|
|
32
|
-
zipf =
|
|
42
|
+
zipf = open_zipfile(path)
|
|
33
43
|
except BadZipFile:
|
|
34
|
-
raise FileFormatError("Invalid Numbers file")
|
|
44
|
+
raise FileFormatError("Invalid Numbers file") from None
|
|
35
45
|
except FileNotFoundError:
|
|
36
|
-
raise FileError("No such file or directory")
|
|
46
|
+
raise FileError("No such file or directory") from None
|
|
37
47
|
|
|
38
48
|
try:
|
|
39
49
|
index_zip = [f for f in zipf.namelist() if f.lower().endswith("index.zip")]
|
|
40
50
|
if len(index_zip) > 0:
|
|
41
51
|
index_data = BytesIO(zipf.read(index_zip[0]))
|
|
42
|
-
get_objects_from_zip_stream(
|
|
52
|
+
get_objects_from_zip_stream(open_zipfile(index_data), file_handler, object_handler)
|
|
43
53
|
else:
|
|
44
54
|
get_objects_from_zip_stream(zipf, file_handler, object_handler)
|
|
45
55
|
except BadZipFile:
|
|
46
|
-
raise FileFormatError("Invalid Numbers file")
|
|
56
|
+
raise FileFormatError("Invalid Numbers file") from None
|
|
47
57
|
|
|
48
58
|
|
|
49
59
|
def write_numbers_file(filename, file_store):
|
|
@@ -58,9 +68,9 @@ def write_numbers_file(filename, file_store):
|
|
|
58
68
|
|
|
59
69
|
def get_objects_from_zip_file(path, file_handler, object_handler):
|
|
60
70
|
try:
|
|
61
|
-
zipf =
|
|
71
|
+
zipf = open_zipfile(path)
|
|
62
72
|
except BadZipFile:
|
|
63
|
-
raise FileFormatError("Invalid Numbers file")
|
|
73
|
+
raise FileFormatError("Invalid Numbers file") from None
|
|
64
74
|
|
|
65
75
|
get_objects_from_zip_stream(zipf, file_handler, object_handler)
|
|
66
76
|
|
numbers_parser/formula.py
CHANGED
|
@@ -24,7 +24,7 @@ class Formula(list):
|
|
|
24
24
|
|
|
25
25
|
def popn(self, num_args: int) -> tuple:
|
|
26
26
|
values = ()
|
|
27
|
-
for
|
|
27
|
+
for _i in range(num_args):
|
|
28
28
|
values += (self._stack.pop(),)
|
|
29
29
|
return values
|
|
30
30
|
|
|
@@ -47,7 +47,7 @@ class Formula(list):
|
|
|
47
47
|
else:
|
|
48
48
|
# 2-dimentional array: {a,b;c,d}
|
|
49
49
|
rows = []
|
|
50
|
-
for
|
|
50
|
+
for _row_num in range(num_rows):
|
|
51
51
|
args = self.popn(num_cols)
|
|
52
52
|
args = ",".join(reversed(args))
|
|
53
53
|
rows.append(f"{args}")
|
|
@@ -92,6 +92,7 @@ class Formula(list):
|
|
|
92
92
|
warnings.warn(
|
|
93
93
|
f"{table_name}@[{self.row},{self.col}]: function ID {node_index} is unsupported",
|
|
94
94
|
UnsupportedWarning,
|
|
95
|
+
stacklevel=2,
|
|
95
96
|
)
|
|
96
97
|
func_name = "UNDEFINED!"
|
|
97
98
|
else:
|
|
@@ -102,6 +103,7 @@ class Formula(list):
|
|
|
102
103
|
warnings.warn(
|
|
103
104
|
f"{table_name}@[{self.row},{self.col}]: stack too small for {func_name}",
|
|
104
105
|
UnsupportedWarning,
|
|
106
|
+
stacklevel=2,
|
|
105
107
|
)
|
|
106
108
|
num_args = len(self._stack)
|
|
107
109
|
|
|
@@ -284,6 +286,7 @@ class TableFormulas:
|
|
|
284
286
|
warnings.warn(
|
|
285
287
|
f"{table_name}@[{row_num},{col_num}]: key #{formula_key} not found",
|
|
286
288
|
UnsupportedWarning,
|
|
289
|
+
stacklevel=2,
|
|
287
290
|
)
|
|
288
291
|
return "INVALID_KEY!(" + str(formula_key) + ")"
|
|
289
292
|
|
|
@@ -297,6 +300,7 @@ class TableFormulas:
|
|
|
297
300
|
warnings.warn(
|
|
298
301
|
f"{table_name}@[{row_num},{col_num}]: node type {node_type} is unsupported",
|
|
299
302
|
UnsupportedWarning,
|
|
303
|
+
stacklevel=2,
|
|
300
304
|
)
|
|
301
305
|
pass
|
|
302
306
|
elif NODE_FUNCTION_MAP[node_type] is not None:
|
|
@@ -307,7 +311,7 @@ class TableFormulas:
|
|
|
307
311
|
|
|
308
312
|
|
|
309
313
|
def number_to_str(v: int) -> str:
|
|
310
|
-
"""Format a float as a string"""
|
|
314
|
+
"""Format a float as a string."""
|
|
311
315
|
# Number is never negative; formula will use NEGATION_NODE
|
|
312
316
|
v_str = repr(v)
|
|
313
317
|
if "e" in v_str:
|
numbers_parser/iwafile.py
CHANGED
|
@@ -2,26 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import struct
|
|
5
|
-
import snappy
|
|
6
|
-
from typing import List
|
|
7
|
-
|
|
8
5
|
from functools import partial
|
|
9
6
|
from struct import unpack
|
|
7
|
+
from typing import List
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
from numbers_parser.exceptions import NotImplementedError
|
|
13
|
-
from numbers_parser.generated.TSPArchiveMessages_pb2 import ArchiveInfo
|
|
14
|
-
|
|
15
|
-
from google.protobuf.internal.encoder import _VarintBytes
|
|
9
|
+
import snappy
|
|
16
10
|
from google.protobuf.internal.decoder import _DecodeVarint32
|
|
11
|
+
from google.protobuf.internal.encoder import _VarintBytes
|
|
17
12
|
from google.protobuf.json_format import MessageToDict, ParseDict
|
|
18
13
|
from google.protobuf.message import EncodeError
|
|
19
14
|
|
|
15
|
+
from numbers_parser.exceptions import NotImplementedError
|
|
16
|
+
from numbers_parser.generated.TSPArchiveMessages_pb2 import ArchiveInfo
|
|
17
|
+
from numbers_parser.mapping import ID_NAME_MAP, NAME_CLASS_MAP, NAME_ID_MAP
|
|
18
|
+
|
|
20
19
|
logger = logging.getLogger(__name__)
|
|
21
20
|
debug = logger.debug
|
|
22
21
|
|
|
23
22
|
|
|
24
|
-
class IWAFile
|
|
23
|
+
class IWAFile:
|
|
25
24
|
def __init__(self, chunks, filename=None):
|
|
26
25
|
self.chunks = chunks
|
|
27
26
|
self.filename = filename
|
|
@@ -59,7 +58,7 @@ class IWAFile(object):
|
|
|
59
58
|
return b"".join([chunk.to_buffer() for chunk in self.chunks])
|
|
60
59
|
|
|
61
60
|
|
|
62
|
-
class IWACompressedChunk
|
|
61
|
+
class IWACompressedChunk:
|
|
63
62
|
def __init__(self, archives):
|
|
64
63
|
self.archives = archives
|
|
65
64
|
|
|
@@ -117,7 +116,7 @@ class IWACompressedChunk(object):
|
|
|
117
116
|
)
|
|
118
117
|
|
|
119
118
|
|
|
120
|
-
class ProtobufPatch
|
|
119
|
+
class ProtobufPatch:
|
|
121
120
|
def __init__(self, data):
|
|
122
121
|
self.data = data
|
|
123
122
|
|
|
@@ -125,7 +124,7 @@ class ProtobufPatch(object):
|
|
|
125
124
|
return self.data == other.data # pragma: no cover
|
|
126
125
|
|
|
127
126
|
def __repr__(self):
|
|
128
|
-
return "
|
|
127
|
+
return f"<{self.__class__.__name__} {self.data}>" # pragma: no cover
|
|
129
128
|
|
|
130
129
|
def to_dict(self):
|
|
131
130
|
return message_to_dict(self.data)
|
|
@@ -141,7 +140,7 @@ class ProtobufPatch(object):
|
|
|
141
140
|
return self.data.SerializePartialToString()
|
|
142
141
|
|
|
143
142
|
|
|
144
|
-
class IWAArchiveSegment
|
|
143
|
+
class IWAArchiveSegment:
|
|
145
144
|
def __init__(self, header, objects):
|
|
146
145
|
self.header = header
|
|
147
146
|
self.objects = objects
|
|
@@ -149,12 +148,9 @@ class IWAArchiveSegment(object):
|
|
|
149
148
|
def __eq__(self, other):
|
|
150
149
|
return self.header == other.header and self.objects == other.objects # pragma: no cover
|
|
151
150
|
|
|
152
|
-
def __repr__(self):
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
self.header.identifier,
|
|
156
|
-
repr(self.objects).replace("\n", " ").replace(" ", " "),
|
|
157
|
-
)
|
|
151
|
+
def __repr__(self): # pragma: no cover
|
|
152
|
+
self_str = repr(self.objects).replace("\n", " ").replace(" ", " ")
|
|
153
|
+
return f"<{self.__class__.__name__} identifier={self.header.identifier} objects={self_str}>"
|
|
158
154
|
|
|
159
155
|
@classmethod
|
|
160
156
|
def from_buffer(cls, buf, filename=None):
|
|
@@ -181,7 +177,7 @@ class IWAArchiveSegment(object):
|
|
|
181
177
|
except KeyError: # pragma: no cover
|
|
182
178
|
raise NotImplementedError(
|
|
183
179
|
"Don't know how to parse Protobuf message type " + str(message_info.type)
|
|
184
|
-
)
|
|
180
|
+
) from None
|
|
185
181
|
try:
|
|
186
182
|
message_payload = payload[n : n + message_info.length]
|
|
187
183
|
if hasattr(klass, "FromString"):
|
|
@@ -192,7 +188,7 @@ class IWAArchiveSegment(object):
|
|
|
192
188
|
raise ValueError(
|
|
193
189
|
"Failed to deserialize %s payload of length %d: %s"
|
|
194
190
|
% (klass, message_info.length, e)
|
|
195
|
-
)
|
|
191
|
+
) from None
|
|
196
192
|
payloads.append(output)
|
|
197
193
|
n += message_info.length
|
|
198
194
|
|
|
@@ -202,7 +198,7 @@ class IWAArchiveSegment(object):
|
|
|
202
198
|
def from_dict(cls, _dict):
|
|
203
199
|
header = dict_to_header(_dict["header"])
|
|
204
200
|
objects = []
|
|
205
|
-
for
|
|
201
|
+
for _message_info, o in zip(header.message_infos, _dict["objects"]):
|
|
206
202
|
objects.append(dict_to_message(o))
|
|
207
203
|
return cls(header, objects)
|
|
208
204
|
|
|
@@ -223,9 +219,10 @@ class IWAArchiveSegment(object):
|
|
|
223
219
|
message_info.length = object_length
|
|
224
220
|
except EncodeError as e: # pragma: no cover
|
|
225
221
|
raise ValueError(
|
|
226
|
-
"Failed to encode object:
|
|
227
|
-
|
|
228
|
-
|
|
222
|
+
"Failed to encode object: {}\nObject: '{}'\nMessage info: {}".format(
|
|
223
|
+
e, repr(obj), message_info
|
|
224
|
+
)
|
|
225
|
+
) from None
|
|
229
226
|
return b"".join(
|
|
230
227
|
[_VarintBytes(self.header.ByteSize()), self.header.SerializeToString()]
|
|
231
228
|
+ [obj.SerializeToString() for obj in self.objects]
|
|
@@ -284,8 +281,7 @@ def create_iwa_segment(id: int, cls: object, object_dict: dict) -> object:
|
|
|
284
281
|
object_dict["_pbtype"] = full_name
|
|
285
282
|
archive_dict = {"header": header, "objects": [object_dict]}
|
|
286
283
|
|
|
287
|
-
|
|
288
|
-
return iwa_segment
|
|
284
|
+
return IWAArchiveSegment.from_dict(archive_dict)
|
|
289
285
|
|
|
290
286
|
|
|
291
287
|
def find_references(obj, references=list):
|
numbers_parser/mapping.py
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
from numbers_parser.generated import TNArchives_pb2 as TNArchives
|
|
2
|
-
from numbers_parser.generated import
|
|
3
|
-
from numbers_parser.generated import
|
|
4
|
-
from numbers_parser.generated import
|
|
2
|
+
from numbers_parser.generated import TNArchives_sos_pb2 as TNArchives_sos
|
|
3
|
+
from numbers_parser.generated import TNCommandArchives_pb2 as TNCommandArchives
|
|
4
|
+
from numbers_parser.generated import TNCommandArchives_sos_pb2 as TNCommandArchives_sos
|
|
5
|
+
from numbers_parser.generated import TSAArchives_pb2 as TSAArchives
|
|
5
6
|
from numbers_parser.generated import TSAArchives_sos_pb2 as TSAArchives_sos
|
|
7
|
+
from numbers_parser.generated import TSACommandArchives_sos_pb2 as TSACommandArchives_sos
|
|
8
|
+
from numbers_parser.generated import TSCEArchives_pb2 as TSCEArchives
|
|
9
|
+
from numbers_parser.generated import TSCH3DArchives_pb2 as TSCH3DArchives
|
|
10
|
+
from numbers_parser.generated import TSCHArchives_Common_pb2 as TSCHArchives_Common
|
|
6
11
|
from numbers_parser.generated import TSCHArchives_GEN_pb2 as TSCHArchives_GEN
|
|
7
|
-
from numbers_parser.generated import
|
|
8
|
-
from numbers_parser.generated import
|
|
9
|
-
from numbers_parser.generated import
|
|
12
|
+
from numbers_parser.generated import TSCHArchives_pb2 as TSCHArchives
|
|
13
|
+
from numbers_parser.generated import TSCHArchives_sos_pb2 as TSCHArchives_sos
|
|
14
|
+
from numbers_parser.generated import TSCHCommandArchives_pb2 as TSCHCommandArchives
|
|
10
15
|
from numbers_parser.generated import TSCHPreUFFArchives_pb2 as TSCHPreUFFArchives
|
|
11
|
-
from numbers_parser.generated import
|
|
12
|
-
from numbers_parser.generated import TSPDatabaseMessages_pb2 as TSPDatabaseMessages
|
|
13
|
-
from numbers_parser.generated import TSSArchives_sos_pb2 as TSSArchives_sos
|
|
16
|
+
from numbers_parser.generated import TSDArchives_pb2 as TSDArchives
|
|
14
17
|
from numbers_parser.generated import TSDArchives_sos_pb2 as TSDArchives_sos
|
|
15
|
-
from numbers_parser.generated import
|
|
18
|
+
from numbers_parser.generated import TSDCommandArchives_pb2 as TSDCommandArchives
|
|
19
|
+
from numbers_parser.generated import TSKArchives_pb2 as TSKArchives
|
|
20
|
+
from numbers_parser.generated import TSKArchives_sos_pb2 as TSKArchives_sos
|
|
16
21
|
from numbers_parser.generated import TSPArchiveMessages_pb2 as TSPArchiveMessages
|
|
17
|
-
from numbers_parser.generated import
|
|
22
|
+
from numbers_parser.generated import TSPDatabaseMessages_pb2 as TSPDatabaseMessages
|
|
23
|
+
from numbers_parser.generated import TSPMessages_pb2 as TSPMessages
|
|
24
|
+
from numbers_parser.generated import TSSArchives_pb2 as TSSArchives
|
|
25
|
+
from numbers_parser.generated import TSSArchives_sos_pb2 as TSSArchives_sos
|
|
26
|
+
from numbers_parser.generated import TSTArchives_pb2 as TSTArchives
|
|
27
|
+
from numbers_parser.generated import TSTArchives_sos_pb2 as TSTArchives_sos
|
|
28
|
+
from numbers_parser.generated import TSTCommandArchives_pb2 as TSTCommandArchives
|
|
29
|
+
from numbers_parser.generated import TSTStylePropertyArchiving_pb2 as TSTStylePropertyArchiving
|
|
18
30
|
from numbers_parser.generated import TSWPArchives_pb2 as TSWPArchives
|
|
19
31
|
from numbers_parser.generated import TSWPArchives_sos_pb2 as TSWPArchives_sos
|
|
20
|
-
from numbers_parser.generated import
|
|
21
|
-
from numbers_parser.generated import TSKArchives_sos_pb2 as TSKArchives_sos
|
|
22
|
-
from numbers_parser.generated import TSCHArchives_sos_pb2 as TSCHArchives_sos
|
|
23
|
-
from numbers_parser.generated import TSDArchives_pb2 as TSDArchives
|
|
24
|
-
from numbers_parser.generated import TNArchives_sos_pb2 as TNArchives_sos
|
|
25
|
-
from numbers_parser.generated import TSKArchives_pb2 as TSKArchives
|
|
26
|
-
from numbers_parser.generated import TSTCommandArchives_pb2 as TSTCommandArchives
|
|
27
|
-
from numbers_parser.generated import TSCHArchives_pb2 as TSCHArchives
|
|
28
|
-
from numbers_parser.generated import TSACommandArchives_sos_pb2 as TSACommandArchives_sos
|
|
29
|
-
from numbers_parser.generated import TSTArchives_sos_pb2 as TSTArchives_sos
|
|
30
|
-
from numbers_parser.generated import TSAArchives_pb2 as TSAArchives
|
|
31
|
-
from numbers_parser.generated import TSCEArchives_pb2 as TSCEArchives
|
|
32
|
-
from numbers_parser.generated import TSCHArchives_Common_pb2 as TSCHArchives_Common
|
|
32
|
+
from numbers_parser.generated import TSWPCommandArchives_pb2 as TSWPCommandArchives
|
|
33
33
|
|
|
34
34
|
PROTO_FILES = [
|
|
35
35
|
TNArchives,
|