numbers-parser 4.7.1__py3-none-any.whl → 4.8.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.
@@ -1,11 +1,26 @@
1
- from typing import Generator, Tuple, Union
1
+ from typing import Dict, Iterator, List, Optional, 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
+ from pendulum import DateTime, Duration
5
+
6
+ from numbers_parser.cell import (
7
+ Border,
8
+ Cell,
9
+ CustomFormatting,
10
+ CustomFormattingType,
11
+ DateCell,
12
+ Formatting,
13
+ FormattingType,
14
+ MergedCell,
15
+ NumberCell,
16
+ Style,
17
+ TextCell,
18
+ UnsupportedWarning,
19
+ xl_cell_to_rowcol,
20
+ )
5
21
  from numbers_parser.cell_storage import CellStorage
6
22
  from numbers_parser.constants import (
7
23
  DEFAULT_COLUMN_COUNT,
8
- DEFAULT_NUM_HEADERS,
9
24
  DEFAULT_ROW_COUNT,
10
25
  MAX_COL_COUNT,
11
26
  MAX_HEADER_COUNT,
@@ -16,51 +31,68 @@ from numbers_parser.file import write_numbers_file
16
31
  from numbers_parser.model import _NumbersModel
17
32
  from numbers_parser.numbers_cache import Cacheable, cache
18
33
 
34
+ __all__ = ["Document", "Sheet", "Table"]
35
+
36
+
37
+ class Sheet:
38
+ pass
39
+
40
+
41
+ class Table:
42
+ pass
43
+
19
44
 
20
45
  class Document:
46
+ """
47
+ Create an instance of a new Numbers document.
48
+
49
+ If ``filename`` is ``None``, an empty document is created using the defaults
50
+ defined by the class constructor. You can optionionally override these
51
+ defaults at object construction time.
52
+
53
+ Parameters
54
+ ----------
55
+ filename: str, optional
56
+ Apple Numbers document to read.
57
+ sheet_name: *str*, *optional*, *default*: ``Sheet 1``
58
+ Name of the first sheet in a new document
59
+ table_name: *str*, *optional*, *default*: ``Table 1``
60
+ Name of the first table in the first sheet of a new
61
+ num_header_rows: int, optional, default: 1
62
+ Number of header rows in the first table of a new document.
63
+ num_header_cols: int, optional, default: 1
64
+ Number of header columns in the first table of a new document.
65
+ num_rows: int, optional, default: 12
66
+ Number of rows in the first table of a new document.
67
+ num_cols: int, optional, default: 8
68
+ Number of columns in the first table of a new document.
69
+
70
+ Raises
71
+ ------
72
+ IndexError:
73
+ If the sheet name already exists in the document.
74
+ IndexError:
75
+ If the table name already exists in the first sheet.
76
+ """
77
+
21
78
  def __init__( # noqa: PLR0913
22
79
  self,
23
- filename: str = None,
24
- sheet_name: str = None,
25
- table_name: str = None,
26
- num_header_rows: int = None,
27
- num_header_cols: int = None,
28
- num_rows: int = None,
29
- num_cols: int = None,
80
+ filename: Optional[str] = None,
81
+ sheet_name: Optional[str] = "Sheet 1",
82
+ table_name: Optional[str] = "Table 1",
83
+ num_header_rows: Optional[int] = 1,
84
+ num_header_cols: Optional[int] = 1,
85
+ num_rows: Optional[int] = DEFAULT_ROW_COUNT,
86
+ num_cols: Optional[int] = DEFAULT_COLUMN_COUNT,
30
87
  ):
31
- if filename is not None and (
32
- (sheet_name is not None)
33
- or (table_name is not None)
34
- or (num_header_rows is not None)
35
- or (num_header_cols is not None)
36
- or (num_rows is not None)
37
- or (num_cols is not None)
38
- ):
39
- warn(
40
- "can't set table/sheet attributes on load of existing document",
41
- RuntimeWarning,
42
- stacklevel=2,
43
- )
44
-
45
88
  self._model = _NumbersModel(filename)
46
89
  refs = self._model.sheet_ids()
47
90
  self._sheets = ItemsList(self._model, refs, Sheet)
48
91
 
49
92
  if filename is None:
50
- if sheet_name is not None:
51
- self.sheets[0].name = sheet_name
93
+ self.sheets[0].name = sheet_name
52
94
  table = self.sheets[0].tables[0]
53
- if table_name is not None:
54
- table.name = table_name
55
-
56
- if num_header_rows is None:
57
- num_header_rows = DEFAULT_NUM_HEADERS
58
- if num_header_cols is None:
59
- num_header_cols = DEFAULT_NUM_HEADERS
60
- if num_rows is None:
61
- num_rows = DEFAULT_ROW_COUNT
62
- if num_cols is None:
63
- num_cols = DEFAULT_COLUMN_COUNT
95
+ table.name = table_name
64
96
 
65
97
  # Table starts as 1x1 with no headers
66
98
  table.add_row(num_rows - 1)
@@ -69,30 +101,72 @@ class Document:
69
101
  table.num_header_cols = num_header_cols
70
102
 
71
103
  @property
72
- def sheets(self) -> list:
73
- """Return a list of all sheets in the document."""
104
+ def sheets(self) -> List[Sheet]:
105
+ """List[:class:`Sheet`]: A list of sheets in the document."""
74
106
  return self._sheets
75
107
 
76
108
  @property
77
- def styles(self) -> list:
78
- """Return a list of styles available in the document."""
109
+ def styles(self) -> Dict[str, Style]:
110
+ """Dict[str, :class:`Style`]: A dict mapping style names to to the corresponding style."""
79
111
  return self._model.styles
80
112
 
81
- def save(self, filename):
113
+ @property
114
+ def custom_formats(self) -> Dict[str, CustomFormatting]:
115
+ """
116
+ Dict[str, :class:`CustomFormatting`]: A dict mapping custom format names
117
+ to the corresponding custom format.
118
+ """
119
+ return self._model.custom_formats
120
+
121
+ def save(self, filename: str) -> None:
122
+ """
123
+ Save the document in the specified filename.
124
+
125
+ Parameters
126
+ ----------
127
+ filename: str
128
+ The path to save the document to. If the file already exists,
129
+ it will be overwritten.
130
+ """
82
131
  for sheet in self.sheets:
83
132
  for table in sheet.tables:
84
- self._model.recalculate_table_data(table._table_id, table._data)
133
+ if self._model.is_a_pivot_table(table._table_id):
134
+ table_name = self._model.table_name(table._table_id)
135
+ warn(
136
+ f"Not modifying pivot table '{table_name}'",
137
+ UnsupportedWarning,
138
+ stacklevel=2,
139
+ )
140
+ else:
141
+ self._model.recalculate_table_data(table._table_id, table._data)
85
142
  write_numbers_file(filename, self._model.file_store)
86
143
 
87
144
  def add_sheet(
88
145
  self,
89
- sheet_name=None,
90
- table_name=None,
91
- num_rows=DEFAULT_ROW_COUNT,
92
- num_cols=DEFAULT_COLUMN_COUNT,
93
- ) -> object:
94
- """Add a new sheet to the current document. If no sheet name is provided,
95
- the next available numbered sheet will be generated.
146
+ sheet_name: Optional[str] = None,
147
+ table_name: Optional[str] = "Table 1",
148
+ num_rows: Optional[int] = DEFAULT_ROW_COUNT,
149
+ num_cols: Optional[int] = DEFAULT_COLUMN_COUNT,
150
+ ) -> None:
151
+ """
152
+ Add a new sheet to the current document.
153
+
154
+ If no sheet name is provided, the next available numbered sheet
155
+ will be generated in the series ``Sheet 1``, ``Sheet 2``, etc.
156
+
157
+ Parameters
158
+ ----------
159
+ sheet_name: str, optional
160
+ The name of the sheet to add to the document
161
+ table_name: *str*, *optional*, *default*: ``Table 1``
162
+ The name of the table created in the new sheet
163
+ num_rows: int, optional, default: 12
164
+ The number of columns in the newly created table
165
+ num_cols: int, optional, default: 8
166
+ The number of columns in the newly created table
167
+
168
+ Raises:
169
+ IndexError: If the sheet name already exists in the document.
96
170
  """
97
171
  if sheet_name is not None:
98
172
  if sheet_name in self._sheets:
@@ -103,9 +177,6 @@ class Document:
103
177
  sheet_num += 1
104
178
  sheet_name = f"Sheet {sheet_num}"
105
179
 
106
- if table_name is None:
107
- table_name = "Table 1"
108
-
109
180
  prev_table_id = self._sheets[-1]._tables[0]._table_id
110
181
  new_sheet_id = self._model.add_sheet(sheet_name)
111
182
  new_sheet = Sheet(self._model, new_sheet_id)
@@ -119,12 +190,41 @@ class Document:
119
190
  )
120
191
  self._sheets.append(new_sheet)
121
192
 
122
- return new_sheet
123
-
124
193
  def add_style(self, **kwargs) -> Style:
125
- """Add a new style to the current document. If no style name is
126
- provided, the next available numbered style will be generated.
127
- """
194
+ r"""
195
+ Add a new style to the current document.
196
+
197
+ If no style name is provided, the next available numbered style
198
+ will be generated in the series ``Custom Style 1``, ``Custom Style 2``, etc.
199
+
200
+ Parameters
201
+ ----------
202
+ kwargs: dict, optional
203
+ Key-value pairs to pass to the :class:`Style` constructor
204
+
205
+ Raises
206
+ ------
207
+ TypeError:
208
+ If ``font_size`` is not a ``float``, ``font_name`` is not a ``str``,
209
+ or if any of the ``bool`` parameters are invalid.
210
+
211
+ Example
212
+ -------
213
+
214
+ .. code-block:: python
215
+
216
+ red_text = doc.add_style(
217
+ name="Red Text",
218
+ font_name="Lucida Grande",
219
+ font_color=RGB(230, 25, 25),
220
+ font_size=14.0,
221
+ bold=True,
222
+ italic=True,
223
+ alignment=Alignment("right", "top"),
224
+ )
225
+ table.write("B2", "Red", style=red_text)
226
+ table.set_cell_style("C2", red_text)
227
+ """ # noqa: E501
128
228
  if "name" in kwargs and kwargs["name"] is not None and kwargs["name"] in self._model.styles:
129
229
  raise IndexError(f"style '{kwargs['name']}' already exists")
130
230
  style = Style(**kwargs)
@@ -134,8 +234,89 @@ class Document:
134
234
  self._model.styles[style.name] = style
135
235
  return style
136
236
 
237
+ def add_custom_format(self, **kwargs) -> CustomFormatting:
238
+ r"""
239
+ Add a new custom format to the current document.
240
+
241
+ All custom formatting styles share a name and a type, described in the **Common**
242
+ parameters in the following table. Additional key-value pairs configure the format
243
+ depending upon the value of ``kwargs["type"]``. Supported values for
244
+ ``kwargs["type"]`` are:
245
+
246
+ * ``"datetime"``: A date and time value with custom formatting.
247
+ * ``"number"``: A decimal number.
248
+ * ``"text"``: A simple text string.
249
+
250
+ :Common Keys:
251
+ * **name** (``str``) – The name of the custom format. If no name is provided,
252
+ one is generated using the scheme ``Custom Format``, ``Custom Format 1``, ``Custom Format 2``, etc.
253
+ * **type** (``str``, *optional*, default: ``number``) – The type of format to
254
+ create Supported formats are ``number``, ``datetime`` and ``text``.
255
+
256
+ :``"number"``:
257
+ * **integer_format** (``PaddingType``, *optional*, default: ``PaddingType.NONE``) – How
258
+ to pad integers.
259
+ * **decimal_format** (``PaddingType``, *optional*, default: ``PaddingType.NONE``) – How
260
+ to pad decimals.
261
+ * **num_integers** (``int``, *optional*, default: ``0``) – Integer precision
262
+ when integers are padded.
263
+ * **num_decimals** (``int``, *optional*, default: ``0``) – Integer precision
264
+ when decimals are padded.
265
+ * **show_thousands_separator** (``bool``, *optional*, default: ``False``) – ``True``
266
+ if the number should include a thousands seperator.
267
+
268
+ :``"datetime"``:
269
+ * **format** (``str``, *optional*, default: ``"d MMM y"``) – A POSIX strftime-like
270
+ formatting string of `Numbers date/time directives <#datetime-formats>`_.
271
+
272
+ :``"text"``:
273
+ * **format** (``str``, *optional*, default: ``"%s"``) – Text format.
274
+ The cell value is inserted in place of %s. Only one substitution is allowed by
275
+ Numbers, and multiple %s formatting references raise a TypeError exception
276
+
277
+ Example
278
+ -------
279
+
280
+ .. code-block:: python
281
+
282
+ long_date = doc.add_custom_format(
283
+ name="Long Date",
284
+ type="date",
285
+ date_time_format="EEEE, d MMMM yyyy")
286
+ table.set_cell_formatting("C1", "custom", format=long_date)
287
+ """ # noqa: E501
288
+ if (
289
+ "name" in kwargs
290
+ and kwargs["name"] is not None
291
+ and kwargs["name"] in self._model.custom_formats
292
+ ):
293
+ raise IndexError(f"format '{kwargs['name']}' already exists")
294
+
295
+ if "type" in kwargs:
296
+ format_type = kwargs["type"].upper()
297
+ try:
298
+ kwargs["type"] = CustomFormattingType[format_type]
299
+ except (KeyError, AttributeError):
300
+ raise TypeError(f"unsuported cell format type '{format_type}'") from None
301
+
302
+ custom_format = CustomFormatting(**kwargs)
303
+ if custom_format.name is None:
304
+ custom_format.name = self._model.custom_format_name()
305
+ if custom_format.type == CustomFormattingType.NUMBER:
306
+ self._model.add_custom_decimal_format_archive(custom_format)
307
+ elif custom_format.type == CustomFormattingType.DATETIME:
308
+ self._model.add_custom_datetime_format_archive(custom_format)
309
+ else:
310
+ self._model.add_custom_text_format_archive(custom_format)
311
+ return custom_format
312
+
137
313
 
138
314
  class Sheet:
315
+ """
316
+ .. NOTE::
317
+ Do not instantiate directly. Sheets are created by :py:class:`~numbers_parser.Document`.
318
+ """
319
+
139
320
  def __init__(self, model, sheet_id):
140
321
  self._sheet_id = sheet_id
141
322
  self._model = model
@@ -143,29 +324,66 @@ class Sheet:
143
324
  self._tables = ItemsList(self._model, refs, Table)
144
325
 
145
326
  @property
146
- def tables(self):
327
+ def tables(self) -> List[Table]:
328
+ """List[:class:`Table`]: A list of tables in the sheet."""
147
329
  return self._tables
148
330
 
149
331
  @property
150
- def name(self):
151
- """Return the sheets name."""
332
+ def name(self) -> str:
333
+ """str: The name of the sheet."""
152
334
  return self._model.sheet_name(self._sheet_id)
153
335
 
154
336
  @name.setter
155
- def name(self, value):
156
- """Set the sheet's name."""
337
+ def name(self, value: str):
157
338
  self._model.sheet_name(self._sheet_id, value)
158
339
 
159
340
  def add_table( # noqa: PLR0913
160
341
  self,
161
- table_name=None,
162
- x=None,
163
- y=None,
164
- num_rows=DEFAULT_ROW_COUNT,
165
- num_cols=DEFAULT_COLUMN_COUNT,
166
- ) -> object:
167
- """Add a new table to the current sheet. If no table name is provided,
168
- the next available numbered table will be generated.
342
+ table_name: Optional[str] = None,
343
+ x: Optional[float] = None,
344
+ y: Optional[float] = None,
345
+ num_rows: Optional[int] = DEFAULT_ROW_COUNT,
346
+ num_cols: Optional[int] = DEFAULT_COLUMN_COUNT,
347
+ ) -> Table:
348
+ """Add a new table to the current sheet.
349
+
350
+ If no table name is provided, the next available numbered table
351
+ will be generated in the series ``Table 1``, ``Table 2``, etc.
352
+
353
+ By default, new tables are positioned at a fixed offset below the last
354
+ table vertically in a sheet and on the left side of the sheet. Large
355
+ table headers and captions may result in new tables overlapping existing
356
+ ones. The ``add_table`` method takes optional coordinates for
357
+ positioning a table. A table's height and coordinates can also be
358
+ queried to help aligning new tables:
359
+
360
+ .. code:: python
361
+
362
+ (x, y) = sheet.table[0].coordinates
363
+ y += sheet.table[0].height + 200.0
364
+ new_table = sheet.add_table("Offset Table", x, y)
365
+
366
+ Parameters
367
+ ----------
368
+ table_name: str, optional
369
+ The name of the new table.
370
+ x: float, optional
371
+ The x offset for the table in points.
372
+ y: float, optional
373
+ The y offset for the table in points.
374
+ num_rows: int, optional, default: 12
375
+ The number of rows for the new table.
376
+ num_cols: int, optional, default: 10
377
+ The number of columns for the new table.
378
+
379
+ Returns
380
+ -------
381
+ Table
382
+ The newly created table.
383
+
384
+ Raises
385
+ ------
386
+ IndexError: If the table name already exists.
169
387
  """
170
388
  from_table_id = self._tables[-1]._table_id
171
389
  return self._add_table(table_name, from_table_id, x, y, num_rows, num_cols)
@@ -189,7 +407,12 @@ class Sheet:
189
407
  return self._tables[-1]
190
408
 
191
409
 
192
- class Table(Cacheable):
410
+ class Table(Cacheable): # noqa: F811
411
+ """
412
+ .. NOTE::
413
+ Do not instantiate directly. Tables are created by :py:class:`~numbers_parser.Document`.
414
+ """
415
+
193
416
  def __init__(self, model, table_id):
194
417
  super().__init__()
195
418
  self._model = model
@@ -202,45 +425,52 @@ class Table(Cacheable):
202
425
  self._model.set_table_data(table_id, self._data)
203
426
  merge_cells = self._model.merge_cells(table_id)
204
427
 
205
- for row_num in range(self.num_rows):
428
+ for row in range(self.num_rows):
206
429
  self._data.append([])
207
- for col_num in range(self.num_cols):
208
- cell_storage = model.table_cell_decode(table_id, row_num, col_num)
430
+ for col in range(self.num_cols):
431
+ cell_storage = model.table_cell_decode(table_id, row, col)
209
432
  if cell_storage is None:
210
- if merge_cells.is_merge_reference((row_num, col_num)):
211
- cell = Cell.merged_cell(table_id, row_num, col_num, model)
433
+ if merge_cells.is_merge_reference((row, col)):
434
+ cell = Cell.merged_cell(table_id, row, col, model)
212
435
  else:
213
- cell = Cell.empty_cell(table_id, row_num, col_num, model)
436
+ cell = Cell.empty_cell(table_id, row, col, model)
214
437
  else:
215
438
  cell = Cell.from_storage(cell_storage)
216
- self._data[row_num].append(cell)
439
+ self._data[row].append(cell)
217
440
 
218
441
  @property
219
442
  def name(self) -> str:
220
- """Return the table's name."""
443
+ """str: The table's name."""
221
444
  return self._model.table_name(self._table_id)
222
445
 
223
446
  @name.setter
224
447
  def name(self, value: str):
225
- """Set the table's name."""
226
448
  self._model.table_name(self._table_id, value)
227
449
 
228
450
  @property
229
- def table_name_enabled(self):
451
+ def table_name_enabled(self) -> bool:
452
+ """bool: ``True`` if the table name is visible, ``False`` otherwise."""
230
453
  return self._model.table_name_enabled(self._table_id)
231
454
 
232
455
  @table_name_enabled.setter
233
- def table_name_enabled(self, enabled):
456
+ def table_name_enabled(self, enabled: bool):
234
457
  self._model.table_name_enabled(self._table_id, enabled)
235
458
 
236
459
  @property
237
460
  def num_header_rows(self) -> int:
238
- """Return the number of header rows."""
461
+ """
462
+ int: The number of header rows.
463
+
464
+ Raises
465
+ ------
466
+ ValueError:
467
+ If the number of headers is negative, exceeds the number of rows in the
468
+ table, or exceeds Numbers maxinum number of headers (``MAX_HEADER_COUNT``).
469
+ """
239
470
  return self._model.num_header_rows(self._table_id)
240
471
 
241
472
  @num_header_rows.setter
242
473
  def num_header_rows(self, num_headers: int):
243
- """Return the number of header rows."""
244
474
  if num_headers < 0:
245
475
  raise ValueError("Number of headers cannot be negative")
246
476
  elif num_headers > self.num_rows:
@@ -251,12 +481,19 @@ class Table(Cacheable):
251
481
 
252
482
  @property
253
483
  def num_header_cols(self) -> int:
254
- """Return the number of header columns."""
484
+ """
485
+ int: The number of header columns.
486
+
487
+ Raises
488
+ ------
489
+ ValueError:
490
+ If the number of headers is negative, exceeds the number of rows in the
491
+ table, or exceeds Numbers maxinum number of headers (``MAX_HEADER_COUNT``).
492
+ """
255
493
  return self._model.num_header_cols(self._table_id)
256
494
 
257
495
  @num_header_cols.setter
258
496
  def num_header_cols(self, num_headers: int):
259
- """Return the number of header columns."""
260
497
  if num_headers < 0:
261
498
  raise ValueError("Number of headers cannot be negative")
262
499
  elif num_headers > self.num_cols:
@@ -267,35 +504,66 @@ class Table(Cacheable):
267
504
 
268
505
  @property
269
506
  def height(self) -> int:
270
- """Return the table's height in points."""
507
+ """int: The table's height in points."""
271
508
  return self._model.table_height(self._table_id)
272
509
 
273
510
  @property
274
511
  def width(self) -> int:
275
- """Return the table's width in points."""
512
+ """int: The table's width in points."""
276
513
  return self._model.table_width(self._table_id)
277
514
 
278
- def row_height(self, row_num: int, height: int = None) -> int:
279
- """Return the height of a table row. Set the height if not None."""
280
- return self._model.row_height(self._table_id, row_num, height)
281
-
282
- def col_width(self, col_num: int, width: int = None) -> int:
283
- """Return the width of a table column. Set the width if not None."""
284
- return self._model.col_width(self._table_id, col_num, width)
515
+ def row_height(self, row: int, height: int = None) -> int:
516
+ """
517
+ The height of a table row in points.
518
+
519
+ Parameters
520
+ ----------
521
+ row: int
522
+ The row number (zero indexed).
523
+ height: int
524
+ The height of the row in points. If not ``None``, set the row height.
525
+
526
+ Returns
527
+ -------
528
+ int:
529
+ The height of the table row.
530
+ """
531
+ return self._model.row_height(self._table_id, row, height)
532
+
533
+ def col_width(self, col: int, width: int = None) -> int:
534
+ """The width of a table column in points.
535
+
536
+ Parameters
537
+ ----------
538
+ col: int
539
+ The column number (zero indexed).
540
+ width: int
541
+ The width of the column in points. If not ``None``, set the column width.
542
+
543
+ Returns
544
+ -------
545
+ int:
546
+ The width of the table column.
547
+ """
548
+ return self._model.col_width(self._table_id, col, width)
285
549
 
286
550
  @property
287
551
  def coordinates(self) -> Tuple[float]:
288
- """Return the table's x,y offsets in points."""
552
+ """Tuple[float]: The table's x, y offsets in points."""
289
553
  return self._model.table_coordinates(self._table_id)
290
554
 
291
- def rows(self, values_only: bool = False) -> list:
555
+ def rows(self, values_only: bool = False) -> Union[List[List[Cell]], List[List[str]]]:
292
556
  """Return all rows of cells for the Table.
293
557
 
294
- Args:
295
- values_only: if True, return cell values instead of Cell objects
558
+ Parameters
559
+ ----------
560
+ values_only:
561
+ If ``True``, return cell values instead of :class:`Cell` objects
296
562
 
297
- Returns:
298
- rows: list of rows; each row is a list of Cell objects
563
+ Returns
564
+ -------
565
+ List[List[Cell]] | List[List[str]]:
566
+ List of rows; each row is a list of :class:`Cell` objects, or string values.
299
567
  """
300
568
  if values_only:
301
569
  return [[cell.value for cell in row] for row in self._data]
@@ -304,48 +572,122 @@ class Table(Cacheable):
304
572
 
305
573
  @property
306
574
  @cache(num_args=0)
307
- def merge_ranges(self) -> list:
575
+ def merge_ranges(self) -> List[str]:
576
+ """List[str]: The merge ranges of cells in A1 notation.
577
+
578
+ Example
579
+ -------
580
+
581
+ .. code-block:: python
582
+
583
+ >>> table.merge_ranges
584
+ ['A4:A10']
585
+ >>> table.cell("A4")
586
+ <numbers_parser.cell.TextCell object at 0x1035f4a90>
587
+ >>> table.cell("A5")
588
+ <numbers_parser.cell.MergedCell object at 0x1035f5310>
589
+ """
308
590
  merge_cells = self._model.merge_cells(self._table_id).merge_cell_names()
309
591
  return sorted(set(list(merge_cells)))
310
592
 
311
593
  def cell(self, *args) -> Union[Cell, MergedCell]:
594
+ """
595
+ Return a single cell in the table.
596
+
597
+ The ``cell()`` method supports two forms of notation to designate the position
598
+ of cells: **Row-column** notation and **A1** notation:
599
+
600
+ .. code-block:: python
601
+
602
+ (0, 0) # Row-column notation.
603
+ ("A1") # The same cell in A1 notation.
604
+
605
+ Parameters
606
+ ----------
607
+ param1: int
608
+ The row number (zero indexed).
609
+ param2: int
610
+ The column number (zero indexed).
611
+
612
+ Returns
613
+ -------
614
+ Cell | MergedCell:
615
+ A cell with the base class :class:`Cell` or, if merged, a :class:`MergedCell`.
616
+
617
+ Example
618
+ -------
619
+
620
+ .. code-block:: python
621
+
622
+ >>> doc = Document("mydoc.numbers")
623
+ >>> sheets = doc.sheets
624
+ >>> tables = sheets["Sheet 1"].tables
625
+ >>> table = tables["Table 1"]
626
+ >>> table.cell(1,0)
627
+ <numbers_parser.cell.TextCell object at 0x105a80a10>
628
+ >>> table.cell(1,0).value
629
+ 'Debit'
630
+ >>> table.cell("B2")
631
+ <numbers_parser.cell.TextCell object at 0x105a80b90>
632
+ >>> table.cell("B2").value
633
+ 1234.50
634
+ """ # noqa: E501
312
635
  if isinstance(args[0], str):
313
- (row_num, col_num) = xl_cell_to_rowcol(args[0])
636
+ (row, col) = xl_cell_to_rowcol(args[0])
314
637
  elif len(args) != 2:
315
638
  raise IndexError("invalid cell reference " + str(args))
316
639
  else:
317
- (row_num, col_num) = args
640
+ (row, col) = args
318
641
 
319
- if row_num >= self.num_rows or row_num < 0:
320
- raise IndexError(f"row {row_num} out of range")
321
- if col_num >= self.num_cols or col_num < 0:
322
- raise IndexError(f"column {col_num} out of range")
642
+ if row >= self.num_rows or row < 0:
643
+ raise IndexError(f"row {row} out of range")
644
+ if col >= self.num_cols or col < 0:
645
+ raise IndexError(f"column {col} out of range")
323
646
 
324
- return self._data[row_num][col_num]
647
+ return self._data[row][col]
325
648
 
326
649
  def iter_rows( # noqa: PLR0913
327
650
  self,
328
- min_row: int = None,
329
- max_row: int = None,
330
- min_col: int = None,
331
- max_col: int = None,
332
- values_only: bool = False,
333
- ) -> Generator[tuple, None, None]:
334
- """Produces cells from a table, by row. Specify the iteration range using
335
- the indexes of the rows and columns.
336
-
337
- Args:
338
- min_row: smallest row index (zero indexed)
339
- max_row: largest row (zero indexed)
340
- min_col: smallest row index (zero indexed)
341
- max_col: largest row (zero indexed)
342
- values_only: return cell values rather than Cell objects
343
-
344
- Returns:
345
- generator: tuple of cells
346
-
347
- Raises:
348
- IndexError: row or column values are out of range for the table
651
+ min_row: Optional[int] = None,
652
+ max_row: Optional[int] = None,
653
+ min_col: Optional[int] = None,
654
+ max_col: Optional[int] = None,
655
+ values_only: Optional[bool] = False,
656
+ ) -> Iterator[Union[Tuple[Cell], Tuple[str]]]:
657
+ """Produces cells from a table, by row.
658
+
659
+ Specify the iteration range using the indexes of the rows and columns.
660
+
661
+ Parameters
662
+ ----------
663
+ min_row: int, optional
664
+ Starting row number (zero indexed), or ``0`` if ``None``.
665
+ max_row: int, optional
666
+ End row number (zero indexed), or all rows if ``None``.
667
+ min_col: int, optional
668
+ Starting column number (zero indexed) or ``0`` if ``None``.
669
+ max_col: int, optional
670
+ End column number (zero indexed), or all columns if ``None``.
671
+ values_only: bool, optional
672
+ If ``True``, yield cell values rather than :class:`Cell` objects
673
+
674
+ Yields
675
+ ------
676
+ Tuple[Cell] | Tuple[str]:
677
+ :class:`Cell` objects or string values for the row
678
+
679
+ Raises
680
+ ------
681
+ IndexError:
682
+ If row or column values are out of range for the table
683
+
684
+ Example
685
+ -------
686
+
687
+ .. code:: python
688
+
689
+ for row in table.iter_rows(min_row=2, max_row=7, values_only=True):
690
+ sum += row
349
691
  """
350
692
  min_row = min_row or 0
351
693
  max_row = max_row or self.num_rows - 1
@@ -362,35 +704,54 @@ class Table(Cacheable):
362
704
  raise IndexError(f"column {max_col} out of range")
363
705
 
364
706
  rows = self.rows()
365
- for row_num in range(min_row, max_row + 1):
707
+ for row in range(min_row, max_row + 1):
366
708
  if values_only:
367
- yield tuple(cell.value for cell in rows[row_num][min_col : max_col + 1])
709
+ yield tuple(cell.value for cell in rows[row][min_col : max_col + 1])
368
710
  else:
369
- yield tuple(rows[row_num][min_col : max_col + 1])
711
+ yield tuple(rows[row][min_col : max_col + 1])
370
712
 
371
713
  def iter_cols( # noqa: PLR0913
372
714
  self,
373
- min_col: int = None,
374
- max_col: int = None,
375
- min_row: int = None,
376
- max_row: int = None,
377
- values_only: bool = False,
378
- ) -> Generator[tuple, None, None]:
379
- """Produces cells from a table, by column. Specify the iteration range using
380
- the indexes of the rows and columns.
381
-
382
- Args:
383
- min_col: smallest row index (zero indexed)
384
- max_col: largest row (zero indexed)
385
- min_row: smallest row index (zero indexed)
386
- max_row: largest row (zero indexed)
387
- values_only: return cell values rather than Cell objects
388
-
389
- Returns:
390
- generator: tuple of cells
391
-
392
- Raises:
393
- IndexError: row or column values are out of range for the table
715
+ min_col: Optional[int] = None,
716
+ max_col: Optional[int] = None,
717
+ min_row: Optional[int] = None,
718
+ max_row: Optional[int] = None,
719
+ values_only: Optional[bool] = False,
720
+ ) -> Iterator[Union[Tuple[Cell], Tuple[str]]]:
721
+ """Produces cells from a table, by column.
722
+
723
+ Specify the iteration range using the indexes of the rows and columns.
724
+
725
+ Parameters
726
+ ----------
727
+ min_col: int, optional
728
+ Starting column number (zero indexed) or ``0`` if ``None``.
729
+ max_col: int, optional
730
+ End column number (zero indexed), or all columns if ``None``.
731
+ min_row: int, optional
732
+ Starting row number (zero indexed), or ``0`` if ``None``.
733
+ max_row: int, optional
734
+ End row number (zero indexed), or all rows if ``None``.
735
+ values_only: bool, optional
736
+ If ``True``, yield cell values rather than :class:`Cell` objects.
737
+
738
+ Yields
739
+ ------
740
+ Tuple[Cell] | Tuple[str]:
741
+ :class:`Cell` objects or string values for the row
742
+
743
+ Raises
744
+ ------
745
+ IndexError:
746
+ If row or column values are out of range for the table
747
+
748
+ Example
749
+ =======
750
+
751
+ .. code:: python
752
+
753
+ for col in table.iter_cols(min_row=2, max_row=7):
754
+ sum += col.value
394
755
  """
395
756
  min_row = min_row or 0
396
757
  max_row = max_row or self.num_rows - 1
@@ -407,91 +768,287 @@ class Table(Cacheable):
407
768
  raise IndexError(f"column {max_col} out of range")
408
769
 
409
770
  rows = self.rows()
410
- for col_num in range(min_col, max_col + 1):
771
+ for col in range(min_col, max_col + 1):
411
772
  if values_only:
412
- yield tuple(row[col_num].value for row in rows[min_row : max_row + 1])
773
+ yield tuple(row[col].value for row in rows[min_row : max_row + 1])
413
774
  else:
414
- yield tuple(row[col_num] for row in rows[min_row : max_row + 1])
775
+ yield tuple(row[col] for row in rows[min_row : max_row + 1])
415
776
 
416
777
  def _validate_cell_coords(self, *args):
417
- """Check first arguments are value cell references and pad
418
- the table with empty cells if outside current bounds.
419
- """
420
778
  if isinstance(args[0], str):
421
- (row_num, col_num) = xl_cell_to_rowcol(args[0])
779
+ (row, col) = xl_cell_to_rowcol(args[0])
422
780
  values = args[1:]
423
781
  elif len(args) < 2:
424
782
  raise IndexError("invalid cell reference " + str(args))
425
783
  else:
426
- (row_num, col_num) = args[0:2]
784
+ (row, col) = args[0:2]
427
785
  values = args[2:]
428
786
 
429
- if row_num >= MAX_ROW_COUNT:
430
- raise IndexError(f"{row_num} exceeds maximum row {MAX_ROW_COUNT-1}")
431
- if col_num >= MAX_COL_COUNT:
432
- raise IndexError(f"{col_num} exceeds maximum column {MAX_COL_COUNT-1}")
787
+ if row >= MAX_ROW_COUNT:
788
+ raise IndexError(f"{row} exceeds maximum row {MAX_ROW_COUNT-1}")
789
+ if col >= MAX_COL_COUNT:
790
+ raise IndexError(f"{col} exceeds maximum column {MAX_COL_COUNT-1}")
433
791
 
434
- for _ in range(self.num_rows, row_num + 1):
792
+ for _ in range(self.num_rows, row + 1):
435
793
  self.add_row()
436
794
 
437
- for _ in range(self.num_cols, col_num + 1):
795
+ for _ in range(self.num_cols, col + 1):
438
796
  self.add_column()
439
797
 
440
- return (row_num, col_num) + tuple(values)
798
+ return (row, col) + tuple(values)
441
799
 
442
- def write(self, *args, style=None, formatting=None):
800
+ def write(self, *args, style: Optional[Union[Style, str, None]] = None) -> None:
801
+ """
802
+ Write a value to a cell and update the style/cell type.
803
+
804
+ The ``write()`` method supports two forms of notation to designate the position
805
+ of cells: **Row-column** notation and **A1** notation:
806
+
807
+ .. code-block:: python
808
+
809
+ (0, 0) # Row-column notation.
810
+ ("A1") # The same cell in A1 notation.
811
+
812
+ Parameters
813
+ ----------
814
+
815
+ param1: int
816
+ The row number (zero indexed)
817
+ param2: int
818
+ The column number (zero indexed)
819
+ param3: str | int | float | bool | DateTime | Duration
820
+ The value to write to the cell. The generated cell type
821
+
822
+ Warns
823
+ -----
824
+ RuntimeWarning:
825
+ If the default value is a float that is rounded to the maximum number
826
+ of supported digits.
827
+
828
+ Raises
829
+ ------
830
+ IndexError:
831
+ If the style name cannot be foiund in the document.
832
+ TypeError:
833
+ If the style parameter is an invalid type.
834
+ ValueError:
835
+ If the cell type cannot be determined from the type of `param3`.
836
+
837
+ Example
838
+ -------
839
+
840
+ .. code:: python
841
+
842
+ doc = Document("write.numbers")
843
+ sheets = doc.sheets
844
+ tables = sheets[0].tables
845
+ table = tables[0]
846
+ table.write(1, 1, "This is new text")
847
+ table.write("B7", datetime(2020, 12, 25))
848
+ doc.save("new-sheet.numbers")
849
+ """
443
850
  # TODO: write needs to retain/init the border
444
- (row_num, col_num, value) = self._validate_cell_coords(*args)
445
- self._data[row_num][col_num] = Cell.from_value(row_num, col_num, value)
446
- self._data[row_num][col_num]._storage = CellStorage(
447
- self._model, self._table_id, None, row_num, col_num
448
- )
851
+ (row, col, value) = self._validate_cell_coords(*args)
852
+ self._data[row][col] = Cell.from_value(row, col, value)
853
+ storage = CellStorage(self._model, self._table_id, None, row, col)
854
+ storage.update_value(value, self._data[row][col])
855
+ self._data[row][col].update_storage(storage)
856
+
449
857
  merge_cells = self._model.merge_cells(self._table_id)
450
- self._data[row_num][col_num]._table_id = self._table_id
451
- self._data[row_num][col_num]._model = self._model
452
- self._data[row_num][col_num]._set_merge(merge_cells.get((row_num, col_num)))
858
+ self._data[row][col]._table_id = self._table_id
859
+ self._data[row][col]._model = self._model
860
+ self._data[row][col]._set_merge(merge_cells.get((row, col)))
453
861
 
454
862
  if style is not None:
455
- self.set_cell_style(row_num, col_num, style)
456
- if formatting is not None:
457
- self.set_cell_formatting(row_num, col_num, formatting)
863
+ self.set_cell_style(row, col, style)
458
864
 
459
865
  def set_cell_style(self, *args):
460
- (row_num, col_num, style) = self._validate_cell_coords(*args)
866
+ (row, col, style) = self._validate_cell_coords(*args)
461
867
  if isinstance(style, Style):
462
- self._data[row_num][col_num]._style = style
868
+ self._data[row][col]._style = style
463
869
  elif isinstance(style, str):
464
870
  if style not in self._model.styles:
465
871
  raise IndexError(f"style '{style}' does not exist")
466
- self._data[row_num][col_num]._style = self._model.styles[style]
872
+ self._data[row][col]._style = self._model.styles[style]
467
873
  else:
468
874
  raise TypeError("style must be a Style object or style name")
469
875
 
470
- def set_cell_formatting(self, *args):
471
- (row_num, col_num, formatting) = self._validate_cell_coords(*args)
472
- if not isinstance(formatting, dict):
473
- raise TypeError("formatting values must be a dict")
876
+ def add_row(
877
+ self,
878
+ num_rows: Optional[int] = 1,
879
+ start_row: Optional[Union[int, None]] = None,
880
+ default: Optional[Union[str, int, float, bool, DateTime, Duration]] = None,
881
+ ) -> None:
882
+ """
883
+ Add or insert rows to the table.
884
+
885
+ Parameters
886
+ ----------
887
+ num_rows: int, optional, default: 1
888
+ The number of rows to add to the table.
889
+ start_row: int, optional, default: None
890
+ The start row number (zero indexed), or ``None`` to add a row to
891
+ the end of the table.
892
+ default: str | int | float | bool | DateTime | Duration, optional, default: None
893
+ The default value for cells. Supported values are those supported by
894
+ :py:meth:`numbers_parser.Table.write` which will determine the new
895
+ cell type.
896
+
897
+ Warns
898
+ -----
899
+ RuntimeWarning:
900
+ If the default value is a float that is rounded to the maximum number
901
+ of supported digits.
902
+
903
+ Raises
904
+ ------
905
+ IndexError:
906
+ If the start_row is out of range for the table.
907
+ ValueError:
908
+ If the default value is unsupported by :py:meth:`numbers_parser.Table.write`.
909
+ """
910
+ if start_row is not None and (start_row < 0 or start_row >= self.num_rows):
911
+ raise IndexError("Row number not in range for table")
474
912
 
475
- self._data[row_num][col_num].set_formatting(formatting)
913
+ if start_row is None:
914
+ start_row = self.num_rows
915
+ self.num_rows += num_rows
916
+ self._model.number_of_rows(self._table_id, self.num_rows)
476
917
 
477
- def add_row(self, num_rows=1):
478
918
  row = [
479
- Cell.empty_cell(self._table_id, self.num_rows - 1, col_num, self._model)
480
- for col_num in range(self.num_cols)
919
+ Cell.empty_cell(self._table_id, self.num_rows - 1, col, self._model)
920
+ for col in range(self.num_cols)
481
921
  ]
482
- for _ in range(num_rows):
483
- self._data.append(row.copy())
484
- self.num_rows += 1
485
- self._model.number_of_rows(self._table_id, self.num_rows)
486
-
487
- def add_column(self, num_cols=1):
488
- for _ in range(num_cols):
489
- for row_num in range(self.num_rows):
490
- self._data[row_num].append(
491
- Cell.empty_cell(self._table_id, row_num, self.num_cols - 1, self._model)
492
- )
493
- self.num_cols += 1
494
- self._model.number_of_columns(self._table_id, self.num_cols)
922
+ rows = [row.copy() for _ in range(num_rows)]
923
+ self._data[start_row:start_row] = rows
924
+
925
+ if default is not None:
926
+ for row in range(start_row, start_row + num_rows):
927
+ for col in range(self.num_cols):
928
+ self.write(row, col, default)
929
+
930
+ def add_column(
931
+ self,
932
+ num_cols: Optional[int] = 1,
933
+ start_col: Optional[Union[int, None]] = None,
934
+ default: Optional[Union[str, int, float, bool, DateTime, Duration]] = None,
935
+ ) -> None:
936
+ """
937
+ Add or insert columns to the table.
938
+
939
+ Parameters
940
+ ----------
941
+ num_cols: int, optional, default: 1
942
+ The number of columns to add to the table.
943
+ start_col: int, optional, default: None
944
+ The start column number (zero indexed), or ``None`` to add a column to
945
+ the end of the table.
946
+ default: str | int | float | bool | DateTime | Duration, optional, default: None
947
+ The default value for cells. Supported values are those supported by
948
+ :py:meth:`numbers_parser.Table.write` which will determine the new
949
+ cell type.
950
+
951
+ Warns
952
+ -----
953
+ RuntimeWarning:
954
+ If the default value is a float that is rounded to the maximum number
955
+ of supported digits.
956
+
957
+ Raises
958
+ ------
959
+ IndexError:
960
+ If the start_col is out of range for the table.
961
+ ValueError:
962
+ If the default value is unsupported by :py:meth:`numbers_parser.Table.write`.
963
+ """
964
+ if start_col is not None and (start_col < 0 or start_col >= self.num_cols):
965
+ raise IndexError("Column number not in range for table")
966
+
967
+ if start_col is None:
968
+ start_col = self.num_cols
969
+ self.num_cols += num_cols
970
+ self._model.number_of_columns(self._table_id, self.num_cols)
971
+
972
+ for row in range(self.num_rows):
973
+ cols = [
974
+ Cell.empty_cell(self._table_id, row, col, self._model) for col in range(num_cols)
975
+ ]
976
+ self._data[row][start_col:start_col] = cols
977
+
978
+ if default is not None:
979
+ for col in range(start_col, start_col + num_cols):
980
+ self.write(row, col, default)
981
+
982
+ def delete_row(
983
+ self,
984
+ num_rows: Optional[int] = 1,
985
+ start_row: Optional[Union[int, None]] = None,
986
+ ) -> None:
987
+ """
988
+ Delete rows from the table.
989
+
990
+ Parameters
991
+ ----------
992
+ num_rows: int, optional, default: 1
993
+ The number of rows to add to the table.
994
+ start_row: int, optional, default: None
995
+ The start row number (zero indexed), or ``None`` to delete rows
996
+ from the end of the table.
997
+
998
+ Warns
999
+ -----
1000
+ RuntimeWarning:
1001
+ If the default value is a float that is rounded to the maximum number
1002
+ of supported digits.
1003
+
1004
+ Raises
1005
+ ------
1006
+ IndexError:
1007
+ If the start_row is out of range for the table.
1008
+ """
1009
+ if start_row is not None and (start_row < 0 or start_row >= self.num_rows):
1010
+ raise IndexError("Row number not in range for table")
1011
+
1012
+ if start_row is not None:
1013
+ del self._data[start_row : start_row + num_rows]
1014
+ else:
1015
+ del self._data[-num_rows:]
1016
+
1017
+ self.num_rows -= num_rows
1018
+ self._model.number_of_rows(self._table_id, self.num_rows)
1019
+
1020
+ def delete_column(
1021
+ self,
1022
+ num_cols: Optional[int] = 1,
1023
+ start_col: Optional[Union[int, None]] = None,
1024
+ ) -> None:
1025
+ """
1026
+ Add or delete columns columns from the table.
1027
+
1028
+ Parameters
1029
+ ----------
1030
+ num_cols: int, optional, default: 1
1031
+ The number of columns to add to the table.
1032
+ start_col: int, optional, default: None
1033
+ The start column number (zero indexed), or ``None`` to add delete columns
1034
+ from the end of the table.
1035
+
1036
+ Raises
1037
+ ------
1038
+ IndexError:
1039
+ If the start_col is out of range for the table.
1040
+ """
1041
+ if start_col is not None and (start_col < 0 or start_col >= self.num_cols):
1042
+ raise IndexError("Column number not in range for table")
1043
+
1044
+ for row in range(self.num_rows):
1045
+ if start_col is not None:
1046
+ del self._data[row][start_col : start_col + num_cols]
1047
+ else:
1048
+ del self._data[row][-num_cols:]
1049
+
1050
+ self.num_cols -= num_cols
1051
+ self._model.number_of_columns(self._table_id, self.num_cols)
495
1052
 
496
1053
  def merge_cells(self, cell_range):
497
1054
  """Convert a cell range or list of cell ranges into merged cells."""
@@ -507,19 +1064,17 @@ class Table(Cacheable):
507
1064
 
508
1065
  merge_cells = self._model.merge_cells(self._table_id)
509
1066
  merge_cells.add_anchor(row_start, col_start, (num_rows, num_cols))
510
- for row_num in range(row_start + 1, row_end + 1):
511
- for col_num in range(col_start + 1, col_end + 1):
512
- self._data[row_num][col_num] = MergedCell(row_num, col_num)
513
- merge_cells.add_reference(
514
- row_num, col_num, (row_start, col_start, row_end, col_end)
515
- )
1067
+ for row in range(row_start + 1, row_end + 1):
1068
+ for col in range(col_start + 1, col_end + 1):
1069
+ self._data[row][col] = MergedCell(row, col)
1070
+ merge_cells.add_reference(row, col, (row_start, col_start, row_end, col_end))
516
1071
 
517
- for row_num, row in enumerate(self._data):
518
- for col_num, cell in enumerate(row):
519
- cell._set_merge(merge_cells.get((row_num, col_num)))
1072
+ for row, cells in enumerate(self._data):
1073
+ for col, cell in enumerate(cells):
1074
+ cell._set_merge(merge_cells.get((row, col)))
520
1075
 
521
1076
  def set_cell_border(self, *args):
522
- (row_num, col_num, *args) = self._validate_cell_coords(*args)
1077
+ (row, col, *args) = self._validate_cell_coords(*args)
523
1078
  if len(args) == 2:
524
1079
  (side, border_value) = args
525
1080
  length = 1
@@ -536,12 +1091,12 @@ class Table(Cacheable):
536
1091
 
537
1092
  if isinstance(side, list):
538
1093
  for s in side:
539
- self.set_cell_border(row_num, col_num, s, border_value, length)
1094
+ self.set_cell_border(row, col, s, border_value, length)
540
1095
  return
541
1096
 
542
- if self._data[row_num][col_num].is_merged and side in ["bottom", "right"]:
1097
+ if self._data[row][col].is_merged and side in ["bottom", "right"]:
543
1098
  warn(
544
- f"cell [{row_num},{col_num}] is merged; {side} border not set",
1099
+ f"cell [{row},{col}] is merged; {side} border not set",
545
1100
  RuntimeWarning,
546
1101
  stacklevel=2,
547
1102
  )
@@ -550,16 +1105,165 @@ class Table(Cacheable):
550
1105
  self._model.extract_strokes(self._table_id)
551
1106
 
552
1107
  if side in ["top", "bottom"]:
553
- for border_col_num in range(col_num, col_num + length):
554
- self._model.set_cell_border(
555
- self._table_id, row_num, border_col_num, side, border_value
556
- )
1108
+ for border_col_num in range(col, col + length):
1109
+ self._model.set_cell_border(self._table_id, row, border_col_num, side, border_value)
557
1110
  elif side in ["left", "right"]:
558
- for border_row_num in range(row_num, row_num + length):
559
- self._model.set_cell_border(
560
- self._table_id, border_row_num, col_num, side, border_value
561
- )
1111
+ for border_row_num in range(row, row + length):
1112
+ self._model.set_cell_border(self._table_id, border_row_num, col, side, border_value)
562
1113
  else:
563
1114
  raise TypeError("side must be a valid border segment")
564
1115
 
565
- self._model.add_stroke(self._table_id, row_num, col_num, side, border_value, length)
1116
+ self._model.add_stroke(self._table_id, row, col, side, border_value, length)
1117
+
1118
+ def set_cell_formatting(self, *args: str, **kwargs) -> None:
1119
+ r"""
1120
+ Set the data format for a cell.
1121
+
1122
+ Cell references can be **row-column** offsers or Excel/Numbers-style **A1** notation.
1123
+
1124
+ :Parameters:
1125
+ * **args** (*list*, *optional*) – Positional arguments for cell reference and data format type (see below)
1126
+ * **kwargs** (*dict*, *optional*) - Key-value pairs defining a formatting options for each data format (see below).
1127
+
1128
+ :Args (row-column):
1129
+ * **param1** (``int``): The row number (zero indexed).
1130
+ * **param2** (``int``): The column number (zero indexed).
1131
+ * **param3** (``str``): Data format type for the cell (see "data formats" below).
1132
+
1133
+ :Args (A1):
1134
+ * **param1** (``str``): A cell reference using Excel/Numbers-style A1 notation.
1135
+ * **param2** (``str``): Data format type for the cell (see "data formats" below).
1136
+
1137
+ :Warns:
1138
+ * **RuntimeWarning** -
1139
+ If ``use_accounting_style`` is used with
1140
+ any ``negative_style`` other than ``NegativeNumberStyle.MINUS``.
1141
+
1142
+ All formatting styles share a name and a type, described in the **Common**
1143
+ parameters in the following table. Additional key-value pairs configure the format
1144
+ depending upon the value of ``kwargs["type"]``. Supported values for
1145
+ ``kwargs["type"]`` are:
1146
+
1147
+ * ``"base"``: A number base in the range 2-36.
1148
+ * ``"currency"``: A decimal formatted with a currency symbol.
1149
+ * ``"datetime"``: A date and time value with custom formatting.
1150
+ * ``"fraction"``: A number formatted as the nearest fraction.
1151
+ * ``"percentage"``: A number formatted as a percentage
1152
+ * ``"number"``: A decimal number.
1153
+ * ``"scientific"``: A decimal number with scientific notation.
1154
+
1155
+ :Common keys:
1156
+ * **name** (``str``) – The name of the custom format. If no name is provided,
1157
+ one is generated using the scheme ``Custom Format``, ``Custom Format 1``, ``Custom Format 2``, etc.
1158
+ * **type** (``str``, *optional*, default: ``number``) – The type of format to
1159
+ create Supported formats are ``number``, ``datetime`` and ``text``.
1160
+
1161
+ :``"base"``:
1162
+ * **base_use_minus_sign** (``int``, *optional*, default: ``10``) – The integer
1163
+ base to represent the number from 2-36.
1164
+ * **base_use_minus_sign** (``bool``, *optional*, default: ``True``) – If ``True``
1165
+ use a standard minus sign, otherwise format as two's compliment (only
1166
+ possible for binary, octal and hexadecimal.
1167
+ * **base_places** (``int``, *optional*, default: ``0``) – The number of
1168
+ decimal places, or ``None`` for automatic.
1169
+
1170
+ :``"currency"``:
1171
+ * **currency** (``str``, *optional*, default: ``"GBP"``) – An ISO currency
1172
+ code, e.g. ``"GBP"`` or ``"USD"``.
1173
+ * **decimal_places** (``int``, *optional*, default: ``2``) – The number of
1174
+ decimal places, or ``None`` for automatic.
1175
+ * **negative_style** (``NegativeNumberStyle``, *optional*, default: ``NegativeNumberStyle.MINUS``) – How negative numbers are represented.
1176
+ See `Negative number formats <#negative-formats>`_.
1177
+ * **show_thousands_separator** (``bool``, *optional*, default: ``False``) – ``True``
1178
+ if the number should include a thousands seperator, e.g. ``,``
1179
+ * **use_accounting_style** (``bool``, *optional*, default: ``False``) – ``True``
1180
+ if the currency symbol should be formatted to the left of the cell and
1181
+ separated from the number value by a tab.
1182
+
1183
+ :``"datetime"``:
1184
+ * **date_time_format** (``str``, *optional*, default: ``"dd MMM YYY HH:MM"``) – A POSIX
1185
+ strftime-like formatting string of `Numbers date/time
1186
+ directives <#datetime-formats>`_.
1187
+
1188
+ :``"fraction"``:
1189
+ * **fraction_accuracy** (``FractionAccuracy``, *optional*, default: ``FractionAccuracy.THREE`` – The
1190
+ precision of the faction.
1191
+
1192
+ :``"percentage"``:
1193
+ * **decimal_places** (``float``, *optional*, default: ``None``) – number of
1194
+ decimal places, or ``None`` for automatic.
1195
+ * **negative_style** (``NegativeNumberStyle``, *optional*, default: ``NegativeNumberStyle.MINUS``) – How negative numbers are represented.
1196
+ See `Negative number formats <#negative-formats>`_.
1197
+ * **show_thousands_separator** (``bool``, *optional*, default: ``False``) – ``True``
1198
+ if the number should include a thousands seperator, e.g. ``,``
1199
+
1200
+ :``"scientific"``:
1201
+ * **decimal_places** (``float``, *optional*, default: ``None``) – number of
1202
+ decimal places, or ``None`` for automatic.
1203
+
1204
+ Example
1205
+
1206
+ .. code:: python
1207
+
1208
+ >>> table.set_cell_formatting("C1", "date", date_time_format="EEEE, d MMMM yyyy")
1209
+ >>> table.set_cell_formatting(0, 4, "number", decimal_places=3, negative_style=NegativeNumberStyle.RED)
1210
+
1211
+ """ # noqa: E501
1212
+ (row, col, *args) = self._validate_cell_coords(*args)
1213
+ if len(args) == 1:
1214
+ format_type = args[0]
1215
+ elif len(args) > 1:
1216
+ raise TypeError("too many positional arguments to set_cell_formatting")
1217
+ else:
1218
+ raise TypeError("no type defined for cell format")
1219
+
1220
+ if format_type == "custom":
1221
+ self.set_cell_custom_format(row, col, **kwargs)
1222
+ else:
1223
+ self.set_cell_data_format(row, col, format_type, **kwargs)
1224
+
1225
+ def set_cell_custom_format(self, row: int, col: int, **kwargs) -> None:
1226
+ if "format" not in kwargs:
1227
+ raise TypeError("no format provided for custom format")
1228
+
1229
+ custom_format = kwargs["format"]
1230
+ if isinstance(custom_format, CustomFormatting):
1231
+ custom_format = kwargs["format"]
1232
+ elif isinstance(custom_format, str):
1233
+ if custom_format not in self._model.custom_formats:
1234
+ raise IndexError(f"format '{custom_format}' does not exist")
1235
+ custom_format = self._model.custom_formats[custom_format]
1236
+ else:
1237
+ raise TypeError("format must be a CustomFormatting object or format name")
1238
+
1239
+ cell = self._data[row][col]
1240
+ if custom_format.type == CustomFormattingType.DATETIME and not isinstance(cell, DateCell):
1241
+ type_name = type(cell).__name__
1242
+ raise TypeError(f"cannot use date/time formatting for cells of type {type_name}")
1243
+ elif custom_format.type == CustomFormattingType.NUMBER and not isinstance(cell, NumberCell):
1244
+ type_name = type(cell).__name__
1245
+ raise TypeError(f"cannot use number formatting for cells of type {type_name}")
1246
+ elif custom_format.type == CustomFormattingType.TEXT and not isinstance(cell, TextCell):
1247
+ type_name = type(cell).__name__
1248
+ raise TypeError(f"cannot use text formatting for cells of type {type_name}")
1249
+
1250
+ format_id = self._model.custom_format_id(self._table_id, custom_format)
1251
+ cell._set_formatting(format_id, custom_format.type)
1252
+
1253
+ def set_cell_data_format(self, row: int, col: int, format_type: str, **kwargs) -> None:
1254
+ try:
1255
+ format_type = FormattingType[format_type.upper()]
1256
+ except (KeyError, AttributeError):
1257
+ raise TypeError(f"unsuported cell format type '{format_type}'") from None
1258
+
1259
+ cell = self._data[row][col]
1260
+ if format_type == FormattingType.DATETIME and not isinstance(cell, DateCell):
1261
+ type_name = type(cell).__name__
1262
+ raise TypeError(f"cannot use date/time formatting for cells of type {type_name}")
1263
+ elif not isinstance(cell, NumberCell) and not isinstance(cell, DateCell):
1264
+ type_name = type(cell).__name__
1265
+ raise TypeError(f"cannot set formatting for cells of type {type_name}")
1266
+
1267
+ format = Formatting(type=format_type, **kwargs)
1268
+ format_id = self._model.format_archive(self._table_id, format_type, format)
1269
+ cell._set_formatting(format_id, format_type)