numbers-parser 4.10.4__py3-none-any.whl → 4.10.6__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 +1 -1
- numbers_parser/_cat_numbers.py +4 -4
- numbers_parser/_unpack_numbers.py +5 -5
- numbers_parser/cell.py +150 -128
- numbers_parser/constants.py +5 -6
- numbers_parser/containers.py +11 -8
- numbers_parser/document.py +147 -108
- numbers_parser/exceptions.py +0 -7
- numbers_parser/formula.py +3 -4
- numbers_parser/generated/TNArchives_pb2.py +24 -26
- numbers_parser/generated/TSAArchives_pb2.py +26 -28
- numbers_parser/generated/TSCEArchives_pb2.py +8 -10
- numbers_parser/generated/TSCHArchives_GEN_pb2.py +28 -34
- numbers_parser/generated/TSCHArchives_pb2.py +61 -65
- numbers_parser/generated/TSDArchives_pb2.py +100 -110
- numbers_parser/generated/TSKArchives_pb2.py +168 -170
- numbers_parser/generated/TSPArchiveMessages_pb2.py +32 -34
- numbers_parser/generated/TSPMessages_pb2.py +40 -40
- numbers_parser/generated/TSSArchives_pb2.py +36 -36
- numbers_parser/generated/TSTArchives_pb2.py +321 -325
- numbers_parser/generated/TSTStylePropertyArchiving_pb2.py +8 -10
- numbers_parser/generated/TSWPArchives_pb2.py +250 -286
- numbers_parser/generated/TSWPCommandArchives_pb2.py +95 -95
- numbers_parser/iwafile.py +17 -17
- numbers_parser/iwork.py +33 -27
- numbers_parser/model.py +94 -52
- numbers_parser/numbers_uuid.py +6 -4
- {numbers_parser-4.10.4.dist-info → numbers_parser-4.10.6.dist-info}/METADATA +9 -9
- numbers_parser-4.10.6.dist-info/RECORD +59 -0
- numbers_parser-4.10.4.dist-info/RECORD +0 -59
- numbers_parser/{mapping.py → generated/mapping.py} +24 -24
- {numbers_parser-4.10.4.dist-info → numbers_parser-4.10.6.dist-info}/LICENSE.rst +0 -0
- {numbers_parser-4.10.4.dist-info → numbers_parser-4.10.6.dist-info}/WHEEL +0 -0
- {numbers_parser-4.10.4.dist-info → numbers_parser-4.10.6.dist-info}/entry_points.txt +0 -0
numbers_parser/cell.py
CHANGED
|
@@ -10,7 +10,7 @@ from fractions import Fraction
|
|
|
10
10
|
from hashlib import sha1
|
|
11
11
|
from os.path import basename
|
|
12
12
|
from struct import pack, unpack
|
|
13
|
-
from typing import Any, List, Tuple, Union
|
|
13
|
+
from typing import Any, List, Optional, Tuple, Union
|
|
14
14
|
from warnings import warn
|
|
15
15
|
|
|
16
16
|
import sigfig
|
|
@@ -104,8 +104,7 @@ __all__ = [
|
|
|
104
104
|
|
|
105
105
|
|
|
106
106
|
class BackgroundImage:
|
|
107
|
-
"""
|
|
108
|
-
A named document style that can be applied to cells.
|
|
107
|
+
"""A named document style that can be applied to cells.
|
|
109
108
|
|
|
110
109
|
.. code-block:: python
|
|
111
110
|
|
|
@@ -129,7 +128,7 @@ class BackgroundImage:
|
|
|
129
128
|
Path to the image file.
|
|
130
129
|
"""
|
|
131
130
|
|
|
132
|
-
def __init__(self, data: bytes = None, filename: str = None):
|
|
131
|
+
def __init__(self, data: Optional[bytes] = None, filename: Optional[str] = None) -> None:
|
|
133
132
|
self._data = data
|
|
134
133
|
self._filename = basename(filename)
|
|
135
134
|
|
|
@@ -180,13 +179,15 @@ class Alignment(_Alignment):
|
|
|
180
179
|
if isinstance(horizontal, str):
|
|
181
180
|
horizontal = horizontal.lower()
|
|
182
181
|
if horizontal not in HORIZONTAL_MAP:
|
|
183
|
-
|
|
182
|
+
msg = "invalid horizontal alignment"
|
|
183
|
+
raise TypeError(msg)
|
|
184
184
|
horizontal = HORIZONTAL_MAP[horizontal]
|
|
185
185
|
|
|
186
186
|
if isinstance(vertical, str):
|
|
187
187
|
vertical = vertical.lower()
|
|
188
188
|
if vertical not in VERTICAL_MAP:
|
|
189
|
-
|
|
189
|
+
msg = "invalid vertical alignment"
|
|
190
|
+
raise TypeError(msg)
|
|
190
191
|
vertical = VERTICAL_MAP[vertical]
|
|
191
192
|
|
|
192
193
|
return super(_Alignment, cls).__new__(cls, (horizontal, vertical))
|
|
@@ -199,8 +200,7 @@ RGB = namedtuple("RGB", ["r", "g", "b"])
|
|
|
199
200
|
|
|
200
201
|
@dataclass
|
|
201
202
|
class Style:
|
|
202
|
-
"""
|
|
203
|
-
A named document style that can be applied to cells.
|
|
203
|
+
"""A named document style that can be applied to cells.
|
|
204
204
|
|
|
205
205
|
Parameters
|
|
206
206
|
----------
|
|
@@ -296,10 +296,7 @@ class Style:
|
|
|
296
296
|
|
|
297
297
|
@classmethod
|
|
298
298
|
def from_storage(cls, cell: object, model: object):
|
|
299
|
-
if cell._image_data is not None
|
|
300
|
-
bg_image = BackgroundImage(*cell._image_data)
|
|
301
|
-
else:
|
|
302
|
-
bg_image = None
|
|
299
|
+
bg_image = BackgroundImage(*cell._image_data) if cell._image_data is not None else None
|
|
303
300
|
return Style(
|
|
304
301
|
alignment=model.cell_alignment(cell),
|
|
305
302
|
bg_image=bg_image,
|
|
@@ -326,13 +323,16 @@ class Style:
|
|
|
326
323
|
self.font_color = rgb_color(self.font_color)
|
|
327
324
|
|
|
328
325
|
if not isinstance(self.font_size, float):
|
|
329
|
-
|
|
326
|
+
msg = "size must be a float number of points"
|
|
327
|
+
raise TypeError(msg)
|
|
330
328
|
if not isinstance(self.font_name, str):
|
|
331
|
-
|
|
329
|
+
msg = "font name must be a string"
|
|
330
|
+
raise TypeError(msg)
|
|
332
331
|
|
|
333
332
|
for attr in ["bold", "italic", "underline", "strikethrough"]:
|
|
334
333
|
if not isinstance(getattr(self, attr), bool):
|
|
335
|
-
|
|
334
|
+
msg = f"{attr} argument must be boolean"
|
|
335
|
+
raise TypeError(msg)
|
|
336
336
|
|
|
337
337
|
def __setattr__(self, name: str, value: Any) -> None:
|
|
338
338
|
"""Detect changes to cell styles and flag the style for
|
|
@@ -359,11 +359,13 @@ def rgb_color(color) -> RGB:
|
|
|
359
359
|
return color
|
|
360
360
|
if isinstance(color, tuple):
|
|
361
361
|
if not (len(color) == 3 and all(isinstance(x, int) for x in color)):
|
|
362
|
-
|
|
362
|
+
msg = "RGB color must be an RGB or a tuple of 3 integers"
|
|
363
|
+
raise TypeError(msg)
|
|
363
364
|
return RGB(*color)
|
|
364
365
|
elif isinstance(color, list):
|
|
365
366
|
return [rgb_color(c) for c in color]
|
|
366
|
-
|
|
367
|
+
msg = "RGB color must be an RGB or a tuple of 3 integers"
|
|
368
|
+
raise TypeError(msg)
|
|
367
369
|
|
|
368
370
|
|
|
369
371
|
def alignment(value) -> Alignment:
|
|
@@ -374,9 +376,11 @@ def alignment(value) -> Alignment:
|
|
|
374
376
|
return value
|
|
375
377
|
if isinstance(value, tuple):
|
|
376
378
|
if not (len(value) == 2 and all(isinstance(x, (int, str)) for x in value)):
|
|
377
|
-
|
|
379
|
+
msg = "Alignment must be an Alignment or a tuple of 2 integers/strings"
|
|
380
|
+
raise TypeError(msg)
|
|
378
381
|
return Alignment(*value)
|
|
379
|
-
|
|
382
|
+
msg = "Alignment must be an Alignment or a tuple of 2 integers/strings"
|
|
383
|
+
raise TypeError(msg)
|
|
380
384
|
|
|
381
385
|
|
|
382
386
|
BORDER_STYLE_MAP = {"solid": 0, "dashes": 1, "dots": 2, "none": 3}
|
|
@@ -390,8 +394,7 @@ class BorderType(IntEnum):
|
|
|
390
394
|
|
|
391
395
|
|
|
392
396
|
class Border:
|
|
393
|
-
"""
|
|
394
|
-
Create a cell border to use with the :py:class:`~numbers_parser.Table` method
|
|
397
|
+
"""Create a cell border to use with the :py:class:`~numbers_parser.Table` method
|
|
395
398
|
:py:meth:`~numbers_parser.Table.set_cell_border`.
|
|
396
399
|
|
|
397
400
|
.. code-block:: python
|
|
@@ -426,9 +429,10 @@ class Border:
|
|
|
426
429
|
color: RGB = None,
|
|
427
430
|
style: BorderType = None,
|
|
428
431
|
_order: int = 0,
|
|
429
|
-
):
|
|
432
|
+
) -> None:
|
|
430
433
|
if not isinstance(width, float):
|
|
431
|
-
|
|
434
|
+
msg = "width must be a float number of points"
|
|
435
|
+
raise TypeError(msg)
|
|
432
436
|
self.width = width
|
|
433
437
|
|
|
434
438
|
if color is None:
|
|
@@ -440,7 +444,8 @@ class Border:
|
|
|
440
444
|
if isinstance(style, str):
|
|
441
445
|
style = style.lower()
|
|
442
446
|
if style not in BORDER_STYLE_MAP:
|
|
443
|
-
|
|
447
|
+
msg = "invalid border style"
|
|
448
|
+
raise TypeError(msg)
|
|
444
449
|
self.style = BORDER_STYLE_MAP[style]
|
|
445
450
|
else:
|
|
446
451
|
self.style = style
|
|
@@ -453,7 +458,7 @@ class Border:
|
|
|
453
458
|
|
|
454
459
|
def __eq__(self, value: object) -> bool:
|
|
455
460
|
return all(
|
|
456
|
-
[self.width == value.width, self.color == value.color, self.style == value.style]
|
|
461
|
+
[self.width == value.width, self.color == value.color, self.style == value.style],
|
|
457
462
|
)
|
|
458
463
|
|
|
459
464
|
|
|
@@ -464,7 +469,7 @@ class CellBorder:
|
|
|
464
469
|
right_merged: bool = False,
|
|
465
470
|
bottom_merged: bool = False,
|
|
466
471
|
left_merged: bool = False,
|
|
467
|
-
):
|
|
472
|
+
) -> None:
|
|
468
473
|
self._top = None
|
|
469
474
|
self._right = None
|
|
470
475
|
self._bottom = None
|
|
@@ -538,14 +543,14 @@ class CellBorder:
|
|
|
538
543
|
class MergeReference:
|
|
539
544
|
"""Cell reference for cells eliminated by a merge."""
|
|
540
545
|
|
|
541
|
-
def __init__(self, row_start: int, col_start: int, row_end: int, col_end: int):
|
|
546
|
+
def __init__(self, row_start: int, col_start: int, row_end: int, col_end: int) -> None:
|
|
542
547
|
self.rect = (row_start, col_start, row_end, col_end)
|
|
543
548
|
|
|
544
549
|
|
|
545
550
|
class MergeAnchor:
|
|
546
551
|
"""Cell reference for the merged cell."""
|
|
547
552
|
|
|
548
|
-
def __init__(self, size: Tuple):
|
|
553
|
+
def __init__(self, size: Tuple) -> None:
|
|
549
554
|
self.size = size
|
|
550
555
|
|
|
551
556
|
|
|
@@ -574,20 +579,19 @@ class CellStorageFlags:
|
|
|
574
579
|
fields = [
|
|
575
580
|
f"{k[1:]}={v}" for k, v in asdict(self).items() if k.endswith("_id") and v is not None
|
|
576
581
|
]
|
|
577
|
-
|
|
578
|
-
return fields
|
|
582
|
+
return ", ".join([x for x in fields if x if not None])
|
|
579
583
|
|
|
580
584
|
def flags(self):
|
|
581
585
|
return [x.name for x in fields(self)]
|
|
582
586
|
|
|
583
587
|
|
|
584
588
|
class Cell(CellStorageFlags, Cacheable):
|
|
585
|
-
"""
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
"""
|
|
589
|
+
""".. NOTE::
|
|
590
|
+
|
|
591
|
+
Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
|
|
592
|
+
""" # fmt: skip
|
|
589
593
|
|
|
590
|
-
def __init__(self, row: int, col: int, value):
|
|
594
|
+
def __init__(self, row: int, col: int, value) -> None:
|
|
591
595
|
self._value = value
|
|
592
596
|
self.row = row
|
|
593
597
|
self.col = col
|
|
@@ -599,14 +603,13 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
599
603
|
self._seconds = None
|
|
600
604
|
super().__init__()
|
|
601
605
|
|
|
602
|
-
def __str__(self):
|
|
606
|
+
def __str__(self) -> str:
|
|
603
607
|
table_name = self._model.table_name(self._table_id)
|
|
604
608
|
sheet_name = self._model.sheet_name(self._model.table_id_to_sheet_id(self._table_id))
|
|
605
609
|
cell_str = f"{sheet_name}@{table_name}[{self.row},{self.col}]:"
|
|
606
610
|
cell_str += f"table_id={self._table_id}, type={self._type.name}, "
|
|
607
611
|
cell_str += f"value={self._value}, flags={self._flags:08x}, extras={self._extras:04x}"
|
|
608
|
-
|
|
609
|
-
return cell_str
|
|
612
|
+
return ", ".join([cell_str, super().__str__()])
|
|
610
613
|
|
|
611
614
|
@property
|
|
612
615
|
def image_filename(self):
|
|
@@ -643,14 +646,14 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
643
646
|
@property
|
|
644
647
|
@cache(num_args=0)
|
|
645
648
|
def formula(self) -> str:
|
|
646
|
-
"""
|
|
647
|
-
str: The formula in a cell.
|
|
649
|
+
"""str: The formula in a cell.
|
|
648
650
|
|
|
649
651
|
Formula evaluation relies on Numbers storing current values which should
|
|
650
652
|
usually be the case. In cells containing a formula, :py:meth:`numbers_parser.Cell.value`
|
|
651
653
|
returns computed value of the formula.
|
|
652
654
|
|
|
653
|
-
Returns
|
|
655
|
+
Returns
|
|
656
|
+
-------
|
|
654
657
|
str:
|
|
655
658
|
The text of the foruma in a cell, or `None` if there is no formula
|
|
656
659
|
present in a cell.
|
|
@@ -668,8 +671,7 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
668
671
|
|
|
669
672
|
@property
|
|
670
673
|
def bullets(self) -> Union[List[str], None]:
|
|
671
|
-
r"""
|
|
672
|
-
List[str] | None: The bullets in a cell, or ``None``
|
|
674
|
+
r"""List[str] | None: The bullets in a cell, or ``None``.
|
|
673
675
|
|
|
674
676
|
Cells that contain bulleted or numbered lists are identified
|
|
675
677
|
by :py:attr:`numbers_parser.Cell.is_bulleted`. For these cells,
|
|
@@ -680,7 +682,6 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
680
682
|
|
|
681
683
|
Example
|
|
682
684
|
-------
|
|
683
|
-
|
|
684
685
|
.. code-block:: python
|
|
685
686
|
|
|
686
687
|
doc = Document("bullets.numbers")
|
|
@@ -698,8 +699,7 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
698
699
|
|
|
699
700
|
@property
|
|
700
701
|
def formatted_value(self) -> str:
|
|
701
|
-
"""
|
|
702
|
-
str: The formatted value of the cell as it appears in Numbers.
|
|
702
|
+
"""str: The formatted value of the cell as it appears in Numbers.
|
|
703
703
|
|
|
704
704
|
Interactive elements are converted into a suitable text format where
|
|
705
705
|
supported, or as their number values where there is no suitable
|
|
@@ -742,7 +742,8 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
742
742
|
def style(self) -> Union[Style, None]:
|
|
743
743
|
"""Style | None: The :class:`Style` associated with the cell or ``None``.
|
|
744
744
|
|
|
745
|
-
Warns
|
|
745
|
+
Warns
|
|
746
|
+
-----
|
|
746
747
|
UnsupportedWarning: On assignment; use
|
|
747
748
|
:py:meth:`numbers_parser.Table.set_cell_style` instead.
|
|
748
749
|
"""
|
|
@@ -762,7 +763,8 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
762
763
|
def border(self) -> Union[CellBorder, None]:
|
|
763
764
|
"""CellBorder| None: The :class:`CellBorder` associated with the cell or ``None``.
|
|
764
765
|
|
|
765
|
-
Warns
|
|
766
|
+
Warns
|
|
767
|
+
-----
|
|
766
768
|
UnsupportedWarning: On assignment; use
|
|
767
769
|
:py:meth:`numbers_parser.Table.set_cell_border` instead.
|
|
768
770
|
"""
|
|
@@ -818,8 +820,13 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
818
820
|
return cell
|
|
819
821
|
|
|
820
822
|
@classmethod
|
|
821
|
-
def _from_storage( # noqa: PLR0913,
|
|
822
|
-
cls,
|
|
823
|
+
def _from_storage( # noqa: PLR0913, PLR0912
|
|
824
|
+
cls,
|
|
825
|
+
table_id: int,
|
|
826
|
+
row: int,
|
|
827
|
+
col: int,
|
|
828
|
+
buffer: bytearray,
|
|
829
|
+
model: object,
|
|
823
830
|
) -> None:
|
|
824
831
|
d128 = None
|
|
825
832
|
double = None
|
|
@@ -827,7 +834,8 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
827
834
|
|
|
828
835
|
version = buffer[0]
|
|
829
836
|
if version != 5:
|
|
830
|
-
|
|
837
|
+
msg = f"Cell storage version {version} is unsupported"
|
|
838
|
+
raise UnsupportedError(msg)
|
|
831
839
|
|
|
832
840
|
offset = 12
|
|
833
841
|
storage_flags = CellStorageFlags()
|
|
@@ -921,7 +929,8 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
921
929
|
elif cell_type == CURRENCY_CELL_TYPE:
|
|
922
930
|
cell = NumberCell(row, col, d128, cell_type=CellType.CURRENCY)
|
|
923
931
|
else:
|
|
924
|
-
|
|
932
|
+
msg = f"Cell type ID {cell_type} is not recognised"
|
|
933
|
+
raise UnsupportedError(msg)
|
|
925
934
|
|
|
926
935
|
cell._copy_flags(storage_flags)
|
|
927
936
|
cell._buffer = buffer
|
|
@@ -1026,7 +1035,7 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
1026
1035
|
flags = 2
|
|
1027
1036
|
length += 8
|
|
1028
1037
|
cell_type = TSTArchives.durationCellType
|
|
1029
|
-
value =
|
|
1038
|
+
value = pack("<d", float(self.value.total_seconds()))
|
|
1030
1039
|
elif isinstance(self, EmptyCell):
|
|
1031
1040
|
flags = 0
|
|
1032
1041
|
cell_type = TSTArchives.emptyCellValueType
|
|
@@ -1136,8 +1145,12 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
1136
1145
|
|
|
1137
1146
|
image_id = style.cell_properties.cell_fill.image.imagedata.identifier
|
|
1138
1147
|
datas = self._model.objects[PACKAGE_ID].datas
|
|
1139
|
-
stored_filename =
|
|
1140
|
-
|
|
1148
|
+
stored_filename = next(
|
|
1149
|
+
x.file_name for x in datas if x.identifier == image_id
|
|
1150
|
+
) # pragma: nocover (issue-1333)
|
|
1151
|
+
preferred_filename = next(
|
|
1152
|
+
x.preferred_file_name for x in datas if x.identifier == image_id
|
|
1153
|
+
) # pragma: nocover (issue-1333)
|
|
1141
1154
|
all_paths = self._model.objects.file_store.keys()
|
|
1142
1155
|
image_pathnames = [x for x in all_paths if x == f"Data/{stored_filename}"]
|
|
1143
1156
|
|
|
@@ -1147,6 +1160,7 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
1147
1160
|
RuntimeWarning,
|
|
1148
1161
|
stacklevel=3,
|
|
1149
1162
|
)
|
|
1163
|
+
return None
|
|
1150
1164
|
else:
|
|
1151
1165
|
image_data = self._model.objects.file_store[image_pathnames[0]]
|
|
1152
1166
|
digest = sha1(image_data).digest()
|
|
@@ -1182,7 +1196,9 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
1182
1196
|
)
|
|
1183
1197
|
else:
|
|
1184
1198
|
formatted_value = _decode_number_format(
|
|
1185
|
-
custom_format,
|
|
1199
|
+
custom_format,
|
|
1200
|
+
self._d128,
|
|
1201
|
+
format_map[format_uuid].name,
|
|
1186
1202
|
)
|
|
1187
1203
|
elif format.format_type == FormatType.DECIMAL:
|
|
1188
1204
|
return _format_decimal(self._d128, format)
|
|
@@ -1309,7 +1325,7 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
1309
1325
|
self,
|
|
1310
1326
|
format_id: int,
|
|
1311
1327
|
format_type: Union[FormattingType, CustomFormattingType],
|
|
1312
|
-
control_id: int = None,
|
|
1328
|
+
control_id: Optional[int] = None,
|
|
1313
1329
|
is_currency: bool = False,
|
|
1314
1330
|
) -> None:
|
|
1315
1331
|
self._is_currency = is_currency
|
|
@@ -1339,12 +1355,12 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
1339
1355
|
|
|
1340
1356
|
|
|
1341
1357
|
class NumberCell(Cell):
|
|
1342
|
-
"""
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
"""
|
|
1358
|
+
""".. NOTE::
|
|
1359
|
+
|
|
1360
|
+
Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
|
|
1361
|
+
""" # fmt: skip
|
|
1346
1362
|
|
|
1347
|
-
def __init__(self, row: int, col: int, value: float, cell_type=CellType.NUMBER):
|
|
1363
|
+
def __init__(self, row: int, col: int, value: float, cell_type=CellType.NUMBER) -> None:
|
|
1348
1364
|
self._type = cell_type
|
|
1349
1365
|
super().__init__(row, col, value)
|
|
1350
1366
|
|
|
@@ -1354,7 +1370,7 @@ class NumberCell(Cell):
|
|
|
1354
1370
|
|
|
1355
1371
|
|
|
1356
1372
|
class TextCell(Cell):
|
|
1357
|
-
def __init__(self, row: int, col: int, value: str):
|
|
1373
|
+
def __init__(self, row: int, col: int, value: str) -> None:
|
|
1358
1374
|
self._type = CellType.TEXT
|
|
1359
1375
|
super().__init__(row, col, value)
|
|
1360
1376
|
|
|
@@ -1364,12 +1380,12 @@ class TextCell(Cell):
|
|
|
1364
1380
|
|
|
1365
1381
|
|
|
1366
1382
|
class RichTextCell(Cell):
|
|
1367
|
-
"""
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
"""
|
|
1383
|
+
""".. NOTE::
|
|
1384
|
+
|
|
1385
|
+
Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
|
|
1386
|
+
""" # fmt: skip
|
|
1371
1387
|
|
|
1372
|
-
def __init__(self, row: int, col: int, value):
|
|
1388
|
+
def __init__(self, row: int, col: int, value) -> None:
|
|
1373
1389
|
super().__init__(row, col, value["text"])
|
|
1374
1390
|
self._type = CellType.RICH_TEXT
|
|
1375
1391
|
self._bullets = value["bullets"]
|
|
@@ -1401,8 +1417,7 @@ class RichTextCell(Cell):
|
|
|
1401
1417
|
|
|
1402
1418
|
@property
|
|
1403
1419
|
def hyperlinks(self) -> Union[List[Tuple], None]:
|
|
1404
|
-
"""
|
|
1405
|
-
List[Tuple] | None: the hyperlinks in a cell or ``None``
|
|
1420
|
+
"""List[Tuple] | None: the hyperlinks in a cell or ``None``.
|
|
1406
1421
|
|
|
1407
1422
|
Numbers does not support hyperlinks to cells within a spreadsheet, but does
|
|
1408
1423
|
allow embedding links in cells. When cells contain hyperlinks,
|
|
@@ -1412,7 +1427,6 @@ class RichTextCell(Cell):
|
|
|
1412
1427
|
|
|
1413
1428
|
Example
|
|
1414
1429
|
-------
|
|
1415
|
-
|
|
1416
1430
|
.. code-block:: python
|
|
1417
1431
|
|
|
1418
1432
|
cell = table.cell(0, 0)
|
|
@@ -1427,12 +1441,12 @@ class BulletedTextCell(RichTextCell):
|
|
|
1427
1441
|
|
|
1428
1442
|
|
|
1429
1443
|
class EmptyCell(Cell):
|
|
1430
|
-
"""
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
"""
|
|
1444
|
+
""".. NOTE::
|
|
1445
|
+
|
|
1446
|
+
Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
|
|
1447
|
+
""" # fmt: skip
|
|
1434
1448
|
|
|
1435
|
-
def __init__(self, row: int, col: int):
|
|
1449
|
+
def __init__(self, row: int, col: int) -> None:
|
|
1436
1450
|
super().__init__(row, col, None)
|
|
1437
1451
|
self._type = CellType.EMPTY
|
|
1438
1452
|
|
|
@@ -1446,12 +1460,12 @@ class EmptyCell(Cell):
|
|
|
1446
1460
|
|
|
1447
1461
|
|
|
1448
1462
|
class BoolCell(Cell):
|
|
1449
|
-
"""
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
"""
|
|
1463
|
+
""".. NOTE::
|
|
1464
|
+
|
|
1465
|
+
Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
|
|
1466
|
+
""" # fmt: skip
|
|
1453
1467
|
|
|
1454
|
-
def __init__(self, row: int, col: int, value: bool):
|
|
1468
|
+
def __init__(self, row: int, col: int, value: bool) -> None:
|
|
1455
1469
|
super().__init__(row, col, value)
|
|
1456
1470
|
self._type = CellType.BOOL
|
|
1457
1471
|
self._value = value
|
|
@@ -1462,12 +1476,12 @@ class BoolCell(Cell):
|
|
|
1462
1476
|
|
|
1463
1477
|
|
|
1464
1478
|
class DateCell(Cell):
|
|
1465
|
-
"""
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
"""
|
|
1479
|
+
""".. NOTE::
|
|
1480
|
+
|
|
1481
|
+
Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
|
|
1482
|
+
""" # fmt: skip
|
|
1469
1483
|
|
|
1470
|
-
def __init__(self, row: int, col: int, value: DateTime):
|
|
1484
|
+
def __init__(self, row: int, col: int, value: DateTime) -> None:
|
|
1471
1485
|
super().__init__(row, col, value)
|
|
1472
1486
|
self._type = CellType.DATE
|
|
1473
1487
|
|
|
@@ -1477,7 +1491,7 @@ class DateCell(Cell):
|
|
|
1477
1491
|
|
|
1478
1492
|
|
|
1479
1493
|
class DurationCell(Cell):
|
|
1480
|
-
def __init__(self, row: int, col: int, value: Duration):
|
|
1494
|
+
def __init__(self, row: int, col: int, value: Duration) -> None:
|
|
1481
1495
|
super().__init__(row, col, value)
|
|
1482
1496
|
self._type = CellType.DURATION
|
|
1483
1497
|
|
|
@@ -1487,12 +1501,12 @@ class DurationCell(Cell):
|
|
|
1487
1501
|
|
|
1488
1502
|
|
|
1489
1503
|
class ErrorCell(Cell):
|
|
1490
|
-
"""
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
"""
|
|
1504
|
+
""".. NOTE::
|
|
1505
|
+
|
|
1506
|
+
Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
|
|
1507
|
+
""" # fmt: skip
|
|
1494
1508
|
|
|
1495
|
-
def __init__(self, row: int, col: int):
|
|
1509
|
+
def __init__(self, row: int, col: int) -> None:
|
|
1496
1510
|
super().__init__(row, col, None)
|
|
1497
1511
|
self._type = CellType.ERROR
|
|
1498
1512
|
|
|
@@ -1502,12 +1516,12 @@ class ErrorCell(Cell):
|
|
|
1502
1516
|
|
|
1503
1517
|
|
|
1504
1518
|
class MergedCell(Cell):
|
|
1505
|
-
"""
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
"""
|
|
1519
|
+
""".. NOTE::
|
|
1520
|
+
|
|
1521
|
+
Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
|
|
1522
|
+
""" # fmt: skip
|
|
1509
1523
|
|
|
1510
|
-
def __init__(self, row: int, col: int):
|
|
1524
|
+
def __init__(self, row: int, col: int) -> None:
|
|
1511
1525
|
super().__init__(row, col, None)
|
|
1512
1526
|
self._type = CellType.MERGED
|
|
1513
1527
|
|
|
@@ -1648,7 +1662,8 @@ def _decode_number_format(format, value, name): # noqa: PLR0912
|
|
|
1648
1662
|
if format.currency_code != "":
|
|
1649
1663
|
# Replace currency code with symbol and no-break space
|
|
1650
1664
|
custom_format_string = custom_format_string.replace(
|
|
1651
|
-
"\u00a4",
|
|
1665
|
+
"\u00a4",
|
|
1666
|
+
format.currency_code + "\u00a0",
|
|
1652
1667
|
)
|
|
1653
1668
|
|
|
1654
1669
|
if (match := re.search(r"([#0.,]+(E[+]\d+)?)", custom_format_string)) is None:
|
|
@@ -1799,7 +1814,9 @@ def _format_decimal(value: float, format, percent: bool = False) -> str:
|
|
|
1799
1814
|
else:
|
|
1800
1815
|
formatted_value = sigfig.round(value, MAX_SIGNIFICANT_DIGITS, type=str, warn=False)
|
|
1801
1816
|
formatted_value = sigfig.round(
|
|
1802
|
-
formatted_value,
|
|
1817
|
+
formatted_value,
|
|
1818
|
+
decimals=format.decimal_places,
|
|
1819
|
+
type=str,
|
|
1803
1820
|
)
|
|
1804
1821
|
if format.show_thousands_separator:
|
|
1805
1822
|
formatted_value = sigfig.round(formatted_value, spacer=",", spacing=3, type=str)
|
|
@@ -1832,16 +1849,16 @@ def _format_currency(value: float, format) -> str:
|
|
|
1832
1849
|
return symbol + formatted_value
|
|
1833
1850
|
|
|
1834
1851
|
|
|
1835
|
-
INT_TO_BASE_CHAR = [str(x) for x in range(
|
|
1852
|
+
INT_TO_BASE_CHAR = [str(x) for x in range(10)] + [chr(x) for x in range(ord("A"), ord("Z") + 1)]
|
|
1836
1853
|
|
|
1837
1854
|
|
|
1838
1855
|
def _invert_bit_str(value: str) -> str:
|
|
1839
|
-
"""Invert a binary value"""
|
|
1856
|
+
"""Invert a binary value."""
|
|
1840
1857
|
return "".join(["0" if b == "1" else "1" for b in value])
|
|
1841
1858
|
|
|
1842
1859
|
|
|
1843
1860
|
def _twos_complement(value: int, base: int) -> str:
|
|
1844
|
-
"""Calculate the twos complement of a negative integer with minimum 32-bit precision"""
|
|
1861
|
+
"""Calculate the twos complement of a negative integer with minimum 32-bit precision."""
|
|
1845
1862
|
num_bits = max([32, math.ceil(math.log2(abs(value))) + 1])
|
|
1846
1863
|
bin_value = bin(abs(value))[2:]
|
|
1847
1864
|
inverted_bin_value = _invert_bit_str(bin_value).rjust(num_bits, "1")
|
|
@@ -1930,7 +1947,7 @@ def _format_scientific(value: float, format) -> str:
|
|
|
1930
1947
|
return f"{formatted_value:.{format.decimal_places}E}"
|
|
1931
1948
|
|
|
1932
1949
|
|
|
1933
|
-
def _unit_format(unit: str, value: int, style: int, abbrev: str = None):
|
|
1950
|
+
def _unit_format(unit: str, value: int, style: int, abbrev: Optional[str] = None):
|
|
1934
1951
|
plural = "" if value == 1 else "s"
|
|
1935
1952
|
if abbrev is None:
|
|
1936
1953
|
abbrev = unit[0]
|
|
@@ -1985,8 +2002,7 @@ range_parts = re.compile(r"(\$?)([A-Z]{1,3})(\$?)(\d+)")
|
|
|
1985
2002
|
|
|
1986
2003
|
|
|
1987
2004
|
def xl_cell_to_rowcol(cell_str: str) -> tuple:
|
|
1988
|
-
"""
|
|
1989
|
-
Convert a cell reference in A1 notation to a zero indexed row and column.
|
|
2005
|
+
"""Convert a cell reference in A1 notation to a zero indexed row and column.
|
|
1990
2006
|
|
|
1991
2007
|
Parameters
|
|
1992
2008
|
----------
|
|
@@ -2003,7 +2019,8 @@ def xl_cell_to_rowcol(cell_str: str) -> tuple:
|
|
|
2003
2019
|
|
|
2004
2020
|
match = range_parts.match(cell_str)
|
|
2005
2021
|
if not match:
|
|
2006
|
-
|
|
2022
|
+
msg = f"invalid cell reference {cell_str}"
|
|
2023
|
+
raise IndexError(msg)
|
|
2007
2024
|
|
|
2008
2025
|
col_str = match.group(2)
|
|
2009
2026
|
row_str = match.group(4)
|
|
@@ -2023,8 +2040,7 @@ def xl_cell_to_rowcol(cell_str: str) -> tuple:
|
|
|
2023
2040
|
|
|
2024
2041
|
|
|
2025
2042
|
def xl_range(first_row, first_col, last_row, last_col):
|
|
2026
|
-
"""
|
|
2027
|
-
Convert zero indexed row and col cell references to a A1:B1 range string.
|
|
2043
|
+
"""Convert zero indexed row and col cell references to a A1:B1 range string.
|
|
2028
2044
|
|
|
2029
2045
|
Parameters
|
|
2030
2046
|
----------
|
|
@@ -2052,8 +2068,7 @@ def xl_range(first_row, first_col, last_row, last_col):
|
|
|
2052
2068
|
|
|
2053
2069
|
|
|
2054
2070
|
def xl_rowcol_to_cell(row, col, row_abs=False, col_abs=False):
|
|
2055
|
-
"""
|
|
2056
|
-
Convert a zero indexed row and column cell reference to a A1 style string.
|
|
2071
|
+
"""Convert a zero indexed row and column cell reference to a A1 style string.
|
|
2057
2072
|
|
|
2058
2073
|
Parameters
|
|
2059
2074
|
----------
|
|
@@ -2072,10 +2087,12 @@ def xl_rowcol_to_cell(row, col, row_abs=False, col_abs=False):
|
|
|
2072
2087
|
A1 style string.
|
|
2073
2088
|
"""
|
|
2074
2089
|
if row < 0:
|
|
2075
|
-
|
|
2090
|
+
msg = f"row reference {row} below zero"
|
|
2091
|
+
raise IndexError(msg)
|
|
2076
2092
|
|
|
2077
2093
|
if col < 0:
|
|
2078
|
-
|
|
2094
|
+
msg = f"column reference {col} below zero"
|
|
2095
|
+
raise IndexError(msg)
|
|
2079
2096
|
|
|
2080
2097
|
row += 1 # Change to 1-index.
|
|
2081
2098
|
row_abs = "$" if row_abs else ""
|
|
@@ -2086,8 +2103,7 @@ def xl_rowcol_to_cell(row, col, row_abs=False, col_abs=False):
|
|
|
2086
2103
|
|
|
2087
2104
|
|
|
2088
2105
|
def xl_col_to_name(col, col_abs=False):
|
|
2089
|
-
"""
|
|
2090
|
-
Convert a zero indexed column cell reference to a string.
|
|
2106
|
+
"""Convert a zero indexed column cell reference to a string.
|
|
2091
2107
|
|
|
2092
2108
|
Parameters
|
|
2093
2109
|
----------
|
|
@@ -2095,12 +2111,15 @@ def xl_col_to_name(col, col_abs=False):
|
|
|
2095
2111
|
The column number (zero indexed).
|
|
2096
2112
|
col_abs: bool, default: False
|
|
2097
2113
|
If ``True``, make the column absolute.
|
|
2098
|
-
|
|
2114
|
+
|
|
2115
|
+
Returns
|
|
2116
|
+
-------
|
|
2099
2117
|
str:
|
|
2100
2118
|
Column in A1 notation.
|
|
2101
2119
|
"""
|
|
2102
2120
|
if col < 0:
|
|
2103
|
-
|
|
2121
|
+
msg = f"column reference {col} below zero"
|
|
2122
|
+
raise IndexError(msg)
|
|
2104
2123
|
|
|
2105
2124
|
col += 1 # Change to 1-index.
|
|
2106
2125
|
col_str = ""
|
|
@@ -2149,7 +2168,8 @@ class Formatting:
|
|
|
2149
2168
|
def __post_init__(self):
|
|
2150
2169
|
if not isinstance(self.type, FormattingType):
|
|
2151
2170
|
type_name = type(self.type).__name__
|
|
2152
|
-
|
|
2171
|
+
msg = f"Invalid format type '{type_name}'"
|
|
2172
|
+
raise TypeError(msg)
|
|
2153
2173
|
|
|
2154
2174
|
if self.use_accounting_style and self.negative_style != NegativeNumberStyle.MINUS:
|
|
2155
2175
|
warn(
|
|
@@ -2161,12 +2181,12 @@ class Formatting:
|
|
|
2161
2181
|
if self.type == FormattingType.DATETIME:
|
|
2162
2182
|
formats = re.sub(r"[^a-zA-Z\s]", " ", self.date_time_format).split()
|
|
2163
2183
|
for el in formats:
|
|
2164
|
-
if el not in DATETIME_FIELD_MAP
|
|
2165
|
-
|
|
2184
|
+
if el not in DATETIME_FIELD_MAP:
|
|
2185
|
+
msg = f"Invalid format specifier '{el}' in date/time format"
|
|
2186
|
+
raise TypeError(msg)
|
|
2166
2187
|
|
|
2167
|
-
if self.type == FormattingType.CURRENCY:
|
|
2168
|
-
|
|
2169
|
-
raise TypeError(f"Unsupported currency code '{self.currency_code}'")
|
|
2188
|
+
if self.type == FormattingType.CURRENCY and self.currency_code not in CURRENCIES:
|
|
2189
|
+
raise TypeError(f"Unsupported currency code '{self.currency_code}'")
|
|
2170
2190
|
|
|
2171
2191
|
if self.decimal_places is None:
|
|
2172
2192
|
if self.type == FormattingType.CURRENCY:
|
|
@@ -2179,10 +2199,12 @@ class Formatting:
|
|
|
2179
2199
|
and not self.base_use_minus_sign
|
|
2180
2200
|
and self.base not in (2, 8, 16)
|
|
2181
2201
|
):
|
|
2182
|
-
|
|
2202
|
+
msg = f"base_use_minus_sign must be True for base {self.base}"
|
|
2203
|
+
raise TypeError(msg)
|
|
2183
2204
|
|
|
2184
2205
|
if self.type == FormattingType.BASE and (self.base < 2 or self.base > MAX_BASE):
|
|
2185
|
-
|
|
2206
|
+
msg = "base must be in range 2-36"
|
|
2207
|
+
raise TypeError(msg)
|
|
2186
2208
|
|
|
2187
2209
|
|
|
2188
2210
|
@dataclass
|
|
@@ -2199,11 +2221,11 @@ class CustomFormatting:
|
|
|
2199
2221
|
def __post_init__(self):
|
|
2200
2222
|
if not isinstance(self.type, CustomFormattingType):
|
|
2201
2223
|
type_name = type(self.type).__name__
|
|
2202
|
-
|
|
2224
|
+
msg = f"Invalid format type '{type_name}'"
|
|
2225
|
+
raise TypeError(msg)
|
|
2203
2226
|
|
|
2204
|
-
if self.type == CustomFormattingType.TEXT:
|
|
2205
|
-
|
|
2206
|
-
raise TypeError("Custom formats only allow one text substitution")
|
|
2227
|
+
if self.type == CustomFormattingType.TEXT and self.format.count("%s") > 1:
|
|
2228
|
+
raise TypeError("Custom formats only allow one text substitution")
|
|
2207
2229
|
|
|
2208
2230
|
@classmethod
|
|
2209
2231
|
def from_archive(cls, archive: object):
|