numbers-parser 4.17.0.post1__py3-none-any.whl → 4.18.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.
@@ -16,11 +16,19 @@ from numbers_parser import (
16
16
  )
17
17
  from numbers_parser import __name__ as numbers_parser_name
18
18
  from numbers_parser.constants import MAX_SIGNIFICANT_DIGITS
19
- from numbers_parser.experimental import _enable_experimental_features
19
+ from numbers_parser.experimental import ExperimentalFeatures, enable_experimental_feature
20
20
 
21
21
  logger = logging.getLogger(numbers_parser_name)
22
22
 
23
23
 
24
+ def experimental_feature_choice(s: str) -> ExperimentalFeatures:
25
+ try:
26
+ return ExperimentalFeatures[s]
27
+ except KeyError:
28
+ msg = f"invalid experimental feature: {s}"
29
+ raise argparse.ArgumentTypeError(msg) # noqa: B904
30
+
31
+
24
32
  def command_line_parser():
25
33
  parser = argparse.ArgumentParser(
26
34
  description="Export data from Apple Numbers spreadsheet tables",
@@ -70,10 +78,13 @@ def command_line_parser():
70
78
  )
71
79
  parser.add_argument("document", nargs="*", help="Document(s) to export")
72
80
  parser.add_argument("--debug", default=False, action="store_true", help="Enable debug logging")
81
+ experimental_choices = [
82
+ f.name for f in ExperimentalFeatures if f is not ExperimentalFeatures.NONE
83
+ ]
73
84
  parser.add_argument(
74
85
  "--experimental",
75
- default=False,
76
- action="store_true",
86
+ type=experimental_feature_choice,
87
+ choices=[ExperimentalFeatures[name] for name in experimental_choices],
77
88
  help=argparse.SUPPRESS,
78
89
  )
79
90
  return parser
@@ -136,7 +147,7 @@ def main() -> None:
136
147
  else:
137
148
  logger.setLevel("ERROR")
138
149
  if args.experimental:
139
- _enable_experimental_features(True)
150
+ enable_experimental_feature(args.experimental)
140
151
  for filename in args.document:
141
152
  try:
142
153
  if args.list_sheets:
numbers_parser/cell.py CHANGED
@@ -56,7 +56,6 @@ from numbers_parser.constants import (
56
56
  )
57
57
  from numbers_parser.currencies import CURRENCIES, CURRENCY_SYMBOLS
58
58
  from numbers_parser.exceptions import UnsupportedError, UnsupportedWarning
59
- from numbers_parser.formula import Formula
60
59
  from numbers_parser.generated import TSPMessages_pb2 as TSPMessages
61
60
  from numbers_parser.generated import TSTArchives_pb2 as TSTArchives
62
61
  from numbers_parser.generated.TSWPArchives_pb2 import (
@@ -257,7 +256,7 @@ class Style:
257
256
  """
258
257
 
259
258
  alignment: Alignment = DEFAULT_ALIGNMENT_CLASS # : horizontal and vertical alignment
260
- bg_image: object = None # : backgroung image
259
+ bg_image: object = None # : background image
261
260
  bg_color: RGB | list[RGB] = None
262
261
  font_color: RGB = field(default_factory=default_color)
263
262
  font_size: float = DEFAULT_FONT_SIZE
@@ -651,7 +650,7 @@ class Cell(CellStorageFlags, Cacheable):
651
650
  Returns
652
651
  -------
653
652
  str:
654
- The text of the foruma in a cell, or `None` if there is no formula
653
+ The text of the formula in a cell, or `None` if there is no formula
655
654
  present in a cell.
656
655
 
657
656
  """
@@ -660,17 +659,6 @@ class Cell(CellStorageFlags, Cacheable):
660
659
  return table_formulas.formula(self._formula_id, self.row, self.col)
661
660
  return None
662
661
 
663
- @formula.setter
664
- def formula(self, value: str) -> None:
665
- self._formula_id = Formula.from_str(
666
- self._model,
667
- self._table_id,
668
- self.row,
669
- self.col,
670
- value,
671
- )
672
- self._model.add_formula_dependency(self.row, self.col, self._table_id)
673
-
674
662
  @property
675
663
  def is_bulleted(self) -> bool:
676
664
  """bool: ``True`` if the cell contains text bullets."""
@@ -933,7 +921,7 @@ class Cell(CellStorageFlags, Cacheable):
933
921
  elif cell_type == CURRENCY_CELL_TYPE:
934
922
  cell = NumberCell(row, col, d128, cell_type=CellType.CURRENCY)
935
923
  else:
936
- msg = f"Cell type ID {cell_type} is not recognised"
924
+ msg = f"Cell type ID {cell_type} is not recognized"
937
925
  raise UnsupportedError(msg)
938
926
 
939
927
  cell._copy_flags(storage_flags)
@@ -45,7 +45,7 @@ class Document:
45
45
  Create an instance of a new Numbers document.
46
46
 
47
47
  If ``filename`` is ``None``, an empty document is created using the defaults
48
- defined by the class constructor. You can optionionally override these
48
+ defined by the class constructor. You can optionally override these
49
49
  defaults at object construction time.
50
50
 
51
51
  Parameters
@@ -303,7 +303,7 @@ class Document:
303
303
  * **num_decimals** (``int``, *optional*, default: ``0``) - Integer precision
304
304
  when decimals are padded.
305
305
  * **show_thousands_separator** (``bool``, *optional*, default: ``False``) - ``True``
306
- if the number should include a thousands seperator.
306
+ if the number should include a thousands separator.
307
307
 
308
308
  :``"datetime"``:
309
309
  * **format** (``str``, *optional*, default: ``"d MMM y"``) - A POSIX strftime-like
@@ -327,7 +327,7 @@ class Document:
327
327
  try:
328
328
  kwargs["type"] = CustomFormattingType[format_type]
329
329
  except (KeyError, AttributeError):
330
- msg = f"unsuported cell format type '{format_type}'"
330
+ msg = f"unsupported cell format type '{format_type}'"
331
331
  raise TypeError(msg) from None
332
332
 
333
333
  custom_format = CustomFormatting(**kwargs)
@@ -474,7 +474,7 @@ class Table(Cacheable):
474
474
  self._table_id = table_id
475
475
  self.num_rows = self._model.number_of_rows(self._table_id)
476
476
  self.num_cols = self._model.number_of_columns(self._table_id)
477
- # Cache all data now to facilite write(). Performance impact
477
+ # Cache all data now to facilitate write(). Performance impact
478
478
  # of computing all cells is minimal compared to file IO
479
479
  self._data = []
480
480
  self._model.set_table_data(table_id, self._data)
@@ -545,7 +545,7 @@ class Table(Cacheable):
545
545
  ------
546
546
  ValueError:
547
547
  If the number of headers is negative, exceeds the number of rows in the
548
- table, or exceeds Numbers maxinum number of headers (``MAX_HEADER_COUNT``).
548
+ table, or exceeds Numbers maximum number of headers (``MAX_HEADER_COUNT``).
549
549
 
550
550
  """
551
551
  return self._model.num_header_rows(self._table_id)
@@ -579,7 +579,7 @@ class Table(Cacheable):
579
579
  ------
580
580
  ValueError:
581
581
  If the number of headers is negative, exceeds the number of rows in the
582
- table, or exceeds Numbers maxinum number of headers (``MAX_HEADER_COUNT``).
582
+ table, or exceeds Numbers maximum number of headers (``MAX_HEADER_COUNT``).
583
583
 
584
584
  """
585
585
  return self._model.num_header_cols(self._table_id)
@@ -756,6 +756,8 @@ class Table(Cacheable):
756
756
  msg = f"column {col} out of range"
757
757
  raise IndexError(msg)
758
758
 
759
+ if (row_mapper := self._model.table_category_row_map(self._table_id)) is not None:
760
+ return self._data[row_mapper[row]][col]
759
761
  return self._data[row][col]
760
762
 
761
763
  def iter_rows(
@@ -803,10 +805,10 @@ class Table(Cacheable):
803
805
  sum += row
804
806
 
805
807
  """
806
- min_row = min_row or 0
807
- max_row = max_row or self.num_rows - 1
808
- min_col = min_col or 0
809
- max_col = max_col or self.num_cols - 1
808
+ min_row = min_row if min_row is not None else 0
809
+ max_row = max_row if max_row is not None else self.num_rows - 1
810
+ min_col = min_col if min_col is not None else 0
811
+ max_col = max_col if max_col is not None else self.num_cols - 1
810
812
 
811
813
  if min_row < 0:
812
814
  msg = f"row {min_row} out of range"
@@ -822,11 +824,16 @@ class Table(Cacheable):
822
824
  raise IndexError(msg)
823
825
 
824
826
  rows = self.rows()
825
- for row in range(min_row, max_row + 1):
827
+ if (row_mapper := self._model.table_category_row_map(self._table_id)) is not None:
828
+ rows = [rows[row_mapper[row]] for row in range(min_row, max_row + 1)]
829
+ else:
830
+ rows = rows[min_row : max_row + 1]
831
+
832
+ for row in rows:
826
833
  if values_only:
827
- yield tuple(cell.value for cell in rows[row][min_col : max_col + 1])
834
+ yield tuple(cell.value for cell in row[min_col : max_col + 1])
828
835
  else:
829
- yield tuple(rows[row][min_col : max_col + 1])
836
+ yield tuple(row[min_col : max_col + 1])
830
837
 
831
838
  def iter_cols(
832
839
  self,
@@ -873,10 +880,10 @@ class Table(Cacheable):
873
880
  sum += col.value
874
881
 
875
882
  """
876
- min_row = min_row or 0
877
- max_row = max_row or self.num_rows - 1
878
- min_col = min_col or 0
879
- max_col = max_col or self.num_cols - 1
883
+ min_row = min_row if min_row is not None else 0
884
+ max_row = max_row if max_row is not None else self.num_rows - 1
885
+ min_col = min_col if min_col is not None else 0
886
+ max_col = max_col if max_col is not None else self.num_cols - 1
880
887
 
881
888
  if min_row < 0:
882
889
  msg = f"row {min_row} out of range"
@@ -892,11 +899,16 @@ class Table(Cacheable):
892
899
  raise IndexError(msg)
893
900
 
894
901
  rows = self.rows()
902
+ if (row_mapper := self._model.table_category_row_map(self._table_id)) is not None:
903
+ rows = [rows[row_mapper[row_num]] for row_num in range(min_row, max_row + 1)]
904
+ else:
905
+ rows = rows[min_row : max_row + 1]
906
+
895
907
  for col in range(min_col, max_col + 1):
896
908
  if values_only:
897
- yield tuple(row[col].value for row in rows[min_row : max_row + 1])
909
+ yield tuple(row[col].value for row in rows)
898
910
  else:
899
- yield tuple(row[col] for row in rows[min_row : max_row + 1])
911
+ yield tuple(row[col] for row in rows)
900
912
 
901
913
  def _validate_cell_coords(self, *args):
902
914
  if isinstance(args[0], str):
@@ -961,7 +973,7 @@ class Table(Cacheable):
961
973
  Raises
962
974
  ------
963
975
  IndexError:
964
- If the style name cannot be foiund in the document.
976
+ If the style name cannot be found in the document.
965
977
  TypeError:
966
978
  If the style parameter is an invalid type.
967
979
  ValueError:
@@ -1001,8 +1013,8 @@ class Table(Cacheable):
1001
1013
 
1002
1014
  def categorized_data(self) -> dict | None:
1003
1015
  """
1004
- Return the table's data organised into categories, if enabled or ``None``
1005
- if the table has not had categoried enabled.
1016
+ Return the table's data organized into categories, if enabled or ``None``
1017
+ if the table has not had categories enabled.
1006
1018
 
1007
1019
  The data is a dictionary with the category names as keys and a list
1008
1020
  dictionaries for each row in that category of the table. The table heading
@@ -1279,7 +1291,7 @@ class Table(Cacheable):
1279
1291
  """
1280
1292
  Set the borders for a cell.
1281
1293
 
1282
- Cell references can be row-column offsers or Excel/Numbers-style A1 notation. Borders
1294
+ Cell references can be row-column offsets or Excel/Numbers-style A1 notation. Borders
1283
1295
  can be applied to multiple sides of a cell by passing a list of sides. The name(s)
1284
1296
  of the side(s) must be one of ``"top"``, ``"right"``, ``"bottom"`` or ``"left"``.
1285
1297
 
@@ -1299,7 +1311,7 @@ class Table(Cacheable):
1299
1311
  * **param2** (*int*): The column number (zero indexed).
1300
1312
  * **param3** (*str | List[str]*): Which side(s) of the cell to apply the border to.
1301
1313
  * **param4** (:py:class:`Border`): The border to add.
1302
- * **param5** (*int*, *optinal*, default: 1): The length of the stroke to add.
1314
+ * **param5** (*int*, *optional*, default: 1): The length of the stroke to add.
1303
1315
 
1304
1316
  :Args (A1):
1305
1317
  * **param1** (*str*): A cell reference using Excel/Numbers-style A1 notation.
@@ -1380,7 +1392,7 @@ class Table(Cacheable):
1380
1392
  r"""
1381
1393
  Set the data format for a cell.
1382
1394
 
1383
- Cell references can be **row-column** offsers or Excel/Numbers-style **A1** notation.
1395
+ Cell references can be **row-column** offsets or Excel/Numbers-style **A1** notation.
1384
1396
 
1385
1397
  .. code:: python
1386
1398
 
@@ -1463,8 +1475,8 @@ class Table(Cacheable):
1463
1475
  decimal places, or ``None`` for automatic.
1464
1476
 
1465
1477
  :``"custom"``:
1466
- * **format** (*str | CustomFormating*) - The name of a custom
1467
- formatin the document or a :py:class:`~numbers_parser.CustomFormatting`
1478
+ * **format** (*str | CustomFormatting*) - The name of a custom
1479
+ formatting the document or a :py:class:`~numbers_parser.CustomFormatting`
1468
1480
  object.
1469
1481
 
1470
1482
  :``"currency"``:
@@ -1475,7 +1487,7 @@ class Table(Cacheable):
1475
1487
  * **negative_style** (*:py:class:`~numbers_parser.NegativeNumberStyle`, optional, default: NegativeNumberStyle.MINUS*) - How negative numbers are represented.
1476
1488
  See `Negative number formats <#negative-formats>`_.
1477
1489
  * **show_thousands_separator** (*bool, optional, default: False*) - ``True``
1478
- if the number should include a thousands seperator, e.g. ``,``
1490
+ if the number should include a thousands separator, e.g. ``,``
1479
1491
  * **use_accounting_style** (*bool, optional, default: False*) - ``True``
1480
1492
  if the currency symbol should be formatted to the left of the cell and
1481
1493
  separated from the number value by a tab.
@@ -1495,7 +1507,7 @@ class Table(Cacheable):
1495
1507
  * **negative_style** (*:py:class:`~numbers_parser.NegativeNumberStyle`, optional, default: NegativeNumberStyle.MINUS*) - How negative numbers are represented.
1496
1508
  See `Negative number formats <#negative-formats>`_.
1497
1509
  * **show_thousands_separator** (*bool, optional, default: False*) - ``True``
1498
- if the number should include a thousands seperator, e.g. ``,``
1510
+ if the number should include a thousands separator, e.g. ``,``
1499
1511
 
1500
1512
  :``"scientific"``:
1501
1513
  * **decimal_places** (*float, optional, default: None*) - number of
@@ -1586,7 +1598,7 @@ class Table(Cacheable):
1586
1598
  format_type = FormattingType[format_type_name.upper()]
1587
1599
  _ = FORMATTING_ALLOWED_CELLS[format_type_name]
1588
1600
  except (KeyError, AttributeError):
1589
- msg = f"unsuported cell format type '{format_type_name}'"
1601
+ msg = f"unsupported cell format type '{format_type_name}'"
1590
1602
  raise TypeError(msg) from None
1591
1603
 
1592
1604
  cell = self._data[row][col]
@@ -7,7 +7,7 @@ class UnsupportedError(NumbersError):
7
7
 
8
8
 
9
9
  class NotImplementedError(NumbersError):
10
- """Raised for unsuported Protobufs/Formats."""
10
+ """Raised for unsupported Protobufs/Formats."""
11
11
 
12
12
 
13
13
  class FileError(NumbersError):
@@ -1,16 +1,30 @@
1
1
  import logging
2
+ from enum import Flag, auto
2
3
 
3
4
  logger = logging.getLogger(__name__)
4
5
  debug = logger.debug
5
6
 
6
- _EXPERIMENTAL_FEATURES = False
7
7
 
8
+ class ExperimentalFeatures(Flag):
9
+ NONE = auto()
10
+ TESTING = auto()
11
+ GROUPED_CATEGORIES = auto()
8
12
 
9
- def _enable_experimental_features(status: bool) -> None:
10
- global _EXPERIMENTAL_FEATURES
11
- _EXPERIMENTAL_FEATURES = status
12
- debug("Experimental features %s", "on" if status else "off")
13
13
 
14
+ EXPERIMENTAL_FEATURES = ExperimentalFeatures.NONE
14
15
 
15
- def _experimental_features() -> bool:
16
- return _EXPERIMENTAL_FEATURES
16
+
17
+ def enable_experimental_feature(flags: ExperimentalFeatures) -> None:
18
+ global EXPERIMENTAL_FEATURES
19
+ EXPERIMENTAL_FEATURES |= flags
20
+ debug("Experimental features: enabling %s, flags=%s", flags, EXPERIMENTAL_FEATURES)
21
+
22
+
23
+ def disable_experimental_feature(flags: ExperimentalFeatures) -> None:
24
+ global EXPERIMENTAL_FEATURES
25
+ EXPERIMENTAL_FEATURES ^= flags
26
+ debug("Experimental features: disabling %s, flags=%s", flags, EXPERIMENTAL_FEATURES)
27
+
28
+
29
+ def experimental_features() -> ExperimentalFeatures:
30
+ return EXPERIMENTAL_FEATURES