Coverage for src/rtflite/input.py: 87%
228 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-02-03 15:40 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-02-03 15:40 +0000
1from collections.abc import MutableSequence, Sequence
2from typing import Any, Text, Tuple
4import numpy as np
5import pandas as pd
6from pydantic import BaseModel, ConfigDict, Field
8from .row import Border, Cell, Row, TextContent
9from .strwidth import get_string_width
12class TextAttributes(BaseModel):
13 """Base class for text-related attributes in RTF components"""
15 model_config = ConfigDict(arbitrary_types_allowed=True)
17 text_font: int | Sequence[int] | pd.DataFrame | Tuple | None = Field(
18 default=None, description="Font number for text"
19 )
20 text_format: str | Sequence[str] | pd.DataFrame | Tuple | None = Field(
21 default=None, description="Text formatting (e.g. 'bold', 'italic')"
22 )
23 text_font_size: float | Sequence[float] | pd.DataFrame | Tuple | None = Field(
24 default=None, description="Font size in points"
25 )
26 text_color: str | Sequence[str] | pd.DataFrame | Tuple | None = Field(
27 default=None, description="Text color name or RGB value"
28 )
29 text_background_color: str | Sequence[str] | pd.DataFrame | Tuple | None = Field(
30 default=None, description="Background color name or RGB value"
31 )
32 text_justification: str | Sequence[str] | pd.DataFrame | Tuple | None = Field(
33 default=None,
34 description="Text alignment ('l'=left, 'c'=center, 'r'=right, 'j'=justify)",
35 )
36 text_indent_first: float | Sequence[float] | pd.DataFrame | Tuple | None = Field(
37 default=None, description="First line indent in inches/twips"
38 )
39 text_indent_left: float | Sequence[float] | pd.DataFrame | Tuple | None = Field(
40 default=None, description="Left indent in inches/twips"
41 )
42 text_indent_right: float | Sequence[float] | pd.DataFrame | Tuple | None = Field(
43 default=None, description="Right indent in inches/twips"
44 )
45 text_space: float | Sequence[float] | pd.DataFrame | Tuple | None = Field(
46 default=None, description="Line spacing multiplier"
47 )
48 text_space_before: float | Sequence[float] | pd.DataFrame | Tuple | None = Field(
49 default=None, description="Space before paragraph in twips"
50 )
51 text_space_after: float | Sequence[float] | pd.DataFrame | Tuple | None = Field(
52 default=None, description="Space after paragraph in twips"
53 )
54 text_hyphenation: bool | Sequence[bool] | pd.DataFrame | Tuple | None = Field(
55 default=None, description="Enable automatic hyphenation"
56 )
57 text_convert: bool | Sequence[bool] | pd.DataFrame | Tuple | None = Field(
58 default=None, description="Convert special characters to RTF"
59 )
61 def _encode(self, text: Sequence[str], method: str) -> str:
62 """Convert the RTF title into RTF syntax using the Text class."""
64 dim = [len(text), 1]
66 text_components = []
67 for i in range(dim[0]):
68 text_components.append(
69 TextContent(
70 text=str(text[i]),
71 font=BroadcastValue(value=self.text_font, dimension=dim).iloc(i, 0),
72 size=BroadcastValue(value=self.text_font_size, dimension=dim).iloc(
73 i, 0
74 ),
75 format=BroadcastValue(value=self.text_format, dimension=dim).iloc(
76 i, 0
77 ),
78 color=BroadcastValue(value=self.text_color, dimension=dim).iloc(
79 i, 0
80 ),
81 background_color=BroadcastValue(
82 value=self.text_background_color, dimension=dim
83 ).iloc(i, 0),
84 justification=BroadcastValue(
85 value=self.text_justification, dimension=dim
86 ).iloc(i, 0),
87 indent_first=BroadcastValue(
88 value=self.text_indent_first, dimension=dim
89 ).iloc(i, 0),
90 indent_left=BroadcastValue(
91 value=self.text_indent_left, dimension=dim
92 ).iloc(i, 0),
93 indent_right=BroadcastValue(
94 value=self.text_indent_right, dimension=dim
95 ).iloc(i, 0),
96 space=BroadcastValue(value=self.text_space, dimension=dim).iloc(
97 i, 0
98 ),
99 space_before=BroadcastValue(
100 value=self.text_space_before, dimension=dim
101 ).iloc(i, 0),
102 space_after=BroadcastValue(
103 value=self.text_space_after, dimension=dim
104 ).iloc(i, 0),
105 convert=BroadcastValue(value=self.text_convert, dimension=dim).iloc(
106 i, 0
107 ),
108 hyphenation=BroadcastValue(
109 value=self.text_hyphenation, dimension=dim
110 ).iloc(i, 0),
111 )
112 )
114 if method == "paragraph":
115 return [
116 text_component._as_rtf(method="paragraph")
117 for text_component in text_components
118 ]
120 if method == "line":
121 line = "\\line".join(
122 [
123 text_component._as_rtf(method="plain")
124 for text_component in text_components
125 ]
126 )
128 return TextContent(
129 text=str(line),
130 font=BroadcastValue(value=self.text_font, dimension=dim).iloc(i, 0),
131 size=BroadcastValue(value=self.text_font_size, dimension=dim).iloc(
132 i, 0
133 ),
134 format=BroadcastValue(value=self.text_format, dimension=dim).iloc(i, 0),
135 color=BroadcastValue(value=self.text_color, dimension=dim).iloc(i, 0),
136 background_color=BroadcastValue(
137 value=self.text_background_color, dimension=dim
138 ).iloc(i, 0),
139 justification=BroadcastValue(
140 value=self.text_justification, dimension=dim
141 ).iloc(i, 0),
142 indent_first=BroadcastValue(
143 value=self.text_indent_first, dimension=dim
144 ).iloc(i, 0),
145 indent_left=BroadcastValue(
146 value=self.text_indent_left, dimension=dim
147 ).iloc(i, 0),
148 indent_right=BroadcastValue(
149 value=self.text_indent_right, dimension=dim
150 ).iloc(i, 0),
151 space=BroadcastValue(value=self.text_space, dimension=dim).iloc(i, 0),
152 space_before=BroadcastValue(
153 value=self.text_space_before, dimension=dim
154 ).iloc(i, 0),
155 space_after=BroadcastValue(
156 value=self.text_space_after, dimension=dim
157 ).iloc(i, 0),
158 convert=BroadcastValue(value=self.text_convert, dimension=dim).iloc(
159 i, 0
160 ),
161 hyphenation=BroadcastValue(
162 value=self.text_hyphenation, dimension=dim
163 ).iloc(i, 0),
164 )._as_rtf(method="paragraph_format")
166 raise ValueError(f"Invalid method: {method}")
169class TableAttributes(TextAttributes):
170 """Base class for table-related attributes in RTF components"""
172 model_config = ConfigDict(arbitrary_types_allowed=True)
174 col_rel_width: float | Sequence[float] | None = Field(
175 default=None, description="Relative widths of table columns"
176 )
177 border_left: str | Sequence[str] | pd.DataFrame | Tuple | None = Field(
178 default=None, description="Left border style"
179 )
180 border_right: str | Sequence[str] | pd.DataFrame | Tuple | None = Field(
181 default=None, description="Right border style"
182 )
183 border_top: str | Sequence[str] | pd.DataFrame | Tuple | None = Field(
184 default=None, description="Top border style"
185 )
186 border_bottom: str | Sequence[str] | pd.DataFrame | Tuple | None = Field(
187 default=None, description="Bottom border style"
188 )
189 border_first: str | Sequence[str] | pd.DataFrame | Tuple | None = Field(
190 default=None, description="First row border style"
191 )
192 border_last: str | Sequence[str] | pd.DataFrame | Tuple | None = Field(
193 default=None, description="Last row border style"
194 )
195 border_color_left: str | Sequence[str] | pd.DataFrame | Tuple | None = Field(
196 default=None, description="Left border color"
197 )
198 border_color_right: str | Sequence[str] | pd.DataFrame | Tuple | None = Field(
199 default=None, description="Right border color"
200 )
201 border_color_top: str | Sequence[str] | pd.DataFrame | Tuple | None = Field(
202 default=None, description="Top border color"
203 )
204 border_color_bottom: str | Sequence[str] | pd.DataFrame | Tuple | None = Field(
205 default=None, description="Bottom border color"
206 )
207 border_color_first: str | Sequence[str] | pd.DataFrame | Tuple | None = Field(
208 default=None, description="First row border color"
209 )
210 border_color_last: str | Sequence[str] | pd.DataFrame | Tuple | None = Field(
211 default=None, description="Last row border color"
212 )
213 border_width: int | Sequence[int] | pd.DataFrame | Tuple | None = Field(
214 default=None, description="Border width in twips"
215 )
216 cell_height: float | Sequence[float] | pd.DataFrame | Tuple | None = Field(
217 default=None, description="Cell height in inches"
218 )
219 cell_justification: str | Sequence[str] | pd.DataFrame | Tuple | None = Field(
220 default=None,
221 description="Cell horizontal alignment ('l'=left, 'c'=center, 'r'=right, 'j'=justify)",
222 )
223 cell_vertical_justification: str | Sequence[str] | pd.DataFrame | Tuple | None = (
224 Field(
225 default=None,
226 description="Cell vertical alignment ('top', 'center', 'bottom')",
227 )
228 )
229 cell_nrow: int | Sequence[int] | pd.DataFrame | Tuple | None = Field(
230 default=None, description="Number of rows per cell"
231 )
233 def _get_section_attributes(self, indices) -> dict:
234 """Helper method to collect all attributes for a section"""
235 text_attrs = {
236 "text_font": self.text_font,
237 "text_format": self.text_format,
238 "text_font_size": self.text_font_size,
239 "text_color": self.text_color,
240 "text_background_color": self.text_background_color,
241 "text_justification": self.text_justification,
242 "text_indent_first": self.text_indent_first,
243 "text_indent_left": self.text_indent_left,
244 "text_indent_right": self.text_indent_right,
245 "text_space": self.text_space,
246 "text_space_before": self.text_space_before,
247 "text_space_after": self.text_space_after,
248 "text_hyphenation": self.text_hyphenation,
249 "text_convert": self.text_convert,
250 "col_rel_width": self.col_rel_width,
251 "border_left": self.border_left,
252 "border_right": self.border_right,
253 "border_top": self.border_top,
254 "border_bottom": self.border_bottom,
255 "border_first": self.border_first,
256 "border_last": self.border_last,
257 "border_color_left": self.border_color_left,
258 "border_color_right": self.border_color_right,
259 "border_color_top": self.border_color_top,
260 "border_color_bottom": self.border_color_bottom,
261 "border_color_first": self.border_color_first,
262 "border_color_last": self.border_color_last,
263 "border_width": self.border_width,
264 "cell_height": self.cell_height,
265 "cell_justification": self.cell_justification,
266 "cell_vertical_justification": self.cell_vertical_justification,
267 "cell_nrow": self.cell_nrow,
268 }
270 # Broadcast attributes to section indices
271 return {
272 attr: [BroadcastValue(value=val).iloc(row, col) for row, col in indices]
273 for attr, val in text_attrs.items()
274 }
276 def _encode(
277 self, df: pd.DataFrame, col_widths: Sequence[float]
278 ) -> MutableSequence[str]:
279 dim = df.shape
281 if self.cell_nrow is None:
282 self.cell_nrow = np.zeros(dim)
284 for i in range(dim[0]):
285 for j in range(dim[1]):
286 text = str(BroadcastValue(value=df, dimension=dim).iloc(i, j))
287 font_size = BroadcastValue(
288 value=self.text_font_size, dimension=dim
289 ).iloc(i, j)
290 col_width = BroadcastValue(value=col_widths, dimension=dim).iloc(
291 i, j
292 )
293 cell_text_width = get_string_width(text=text, font_size=font_size)
294 self.cell_nrow[i, j] = np.ceil(cell_text_width / col_width)
296 rows: MutableSequence[str] = []
297 for i in range(dim[0]):
298 row = df.iloc[i]
299 cells = []
301 for j in range(dim[1]):
302 col = df.columns[j]
304 if j == dim[1] - 1:
305 border_right = Border(
306 style=BroadcastValue(
307 value=self.border_right, dimension=dim
308 ).iloc(i, j)
309 )
310 else:
311 border_right = None
313 cell = Cell(
314 text=TextContent(
315 text=str(row[col]),
316 font=BroadcastValue(value=self.text_font, dimension=dim).iloc(
317 i, j
318 ),
319 size=BroadcastValue(
320 value=self.text_font_size, dimension=dim
321 ).iloc(i, j),
322 format=BroadcastValue(
323 value=self.text_format, dimension=dim
324 ).iloc(i, j),
325 color=BroadcastValue(value=self.text_color, dimension=dim).iloc(
326 i, j
327 ),
328 background_color=BroadcastValue(
329 value=self.text_background_color, dimension=dim
330 ).iloc(i, j),
331 justification=BroadcastValue(
332 value=self.text_justification, dimension=dim
333 ).iloc(i, j),
334 indent_first=BroadcastValue(
335 value=self.text_indent_first, dimension=dim
336 ).iloc(i, j),
337 indent_left=BroadcastValue(
338 value=self.text_indent_left, dimension=dim
339 ).iloc(i, j),
340 indent_right=BroadcastValue(
341 value=self.text_indent_right, dimension=dim
342 ).iloc(i, j),
343 space=BroadcastValue(value=self.text_space, dimension=dim).iloc(
344 i, j
345 ),
346 space_before=BroadcastValue(
347 value=self.text_space_before, dimension=dim
348 ).iloc(i, j),
349 space_after=BroadcastValue(
350 value=self.text_space_after, dimension=dim
351 ).iloc(i, j),
352 convert=BroadcastValue(
353 value=self.text_convert, dimension=dim
354 ).iloc(i, j),
355 hyphenation=BroadcastValue(
356 value=self.text_hyphenation, dimension=dim
357 ).iloc(i, j),
358 ),
359 width=col_widths[j],
360 border_left=Border(
361 style=BroadcastValue(
362 value=self.border_left, dimension=dim
363 ).iloc(i, j)
364 ),
365 border_right=border_right,
366 border_top=Border(
367 style=BroadcastValue(value=self.border_top, dimension=dim).iloc(
368 i, j
369 )
370 ),
371 border_bottom=Border(
372 style=BroadcastValue(
373 value=self.border_bottom, dimension=dim
374 ).iloc(i, j)
375 ),
376 vertical_justification=BroadcastValue(
377 value=self.cell_vertical_justification, dimension=dim
378 ).iloc(i, j),
379 )
380 cells.append(cell)
381 rtf_row = Row(
382 row_cells=cells,
383 justification=BroadcastValue(
384 value=self.cell_justification, dimension=dim
385 ).iloc(i, 0),
386 height=BroadcastValue(value=self.cell_height, dimension=dim).iloc(i, 0),
387 )
388 rows.extend(rtf_row._as_rtf())
390 return rows
393class BroadcastValue(BaseModel):
394 model_config = ConfigDict(arbitrary_types_allowed=True)
396 value: int | float | str | Tuple | Sequence[Any] | pd.DataFrame | None = Field(
397 ...,
398 description="The value of the table, can be various types including DataFrame.",
399 )
401 dimension: Tuple[int, int] | None = Field(
402 None, description="Dimensions of the table (rows, columns)"
403 )
405 def iloc(self, row_index: int | slice, column_index: int | slice) -> Any:
406 """
407 Access a value using row and column indices, based on the data type.
409 Parameters:
410 - row_index: The row index or slice (for lists and DataFrames).
411 - column_index: The column index or slice (for DataFrames and dictionaries). Optional for lists.
413 Returns:
414 - The accessed value or an appropriate error message.
415 """
416 if self.value is None:
417 return None
419 if isinstance(self.value, pd.DataFrame):
420 # Handle DataFrame as is
421 try:
422 return self.value.iloc[
423 row_index % self.value.shape[0], column_index % self.value.shape[1]
424 ]
425 except IndexError as e:
426 raise ValueError(f"Invalid DataFrame index or slice: {e}")
428 if isinstance(self.value, list):
429 # Handle list as column based broadcast data frame
430 return self.value[column_index % len(self.value)]
432 if isinstance(self.value, tuple):
433 # Handle Tuple as row based broadcast data frame
434 values = list(self.value)
435 return values[row_index % len(values)]
437 if isinstance(self.value, (int, float, str)):
438 return self.value
440 def to_dataframe(self) -> pd.DataFrame:
441 """
442 Convert the value to a pandas DataFrame based on the dimension variable if it is not None.
444 Returns:
445 - A pandas DataFrame representation of the value.
447 Raises:
448 - ValueError: If the dimension is None or if the value cannot be converted to a DataFrame.
449 """
450 if self.dimension is None:
451 if isinstance(self.value, pd.DataFrame):
452 self.dimension = self.value.shape
453 elif isinstance(self.value, list):
454 self.dimension = (1, len(self.value))
455 elif isinstance(self.value, tuple):
456 self.dimension = (len(self.value), 1)
457 elif isinstance(self.value, (int, float, str)):
458 self.dimension = (1, 1)
459 else:
460 raise ValueError("Dimension must be specified to convert to DataFrame.")
462 if isinstance(self.value, pd.DataFrame):
463 # Ensure the DataFrame can be recycled to match the specified dimensions
464 row_count, col_count = self.value.shape
465 row_repeats = max(1, (self.dimension[0] + row_count - 1) // row_count)
466 recycled_rows = pd.concat(
467 [self.value] * row_repeats, ignore_index=True
468 ).head(self.dimension[0])
470 col_repeats = max(1, (self.dimension[1] + col_count - 1) // col_count)
471 recycled_df = pd.concat([recycled_rows] * col_repeats, axis=1).iloc[
472 :, : self.dimension[1]
473 ]
475 return recycled_df.reset_index(drop=True)
477 if isinstance(self.value, (list, MutableSequence)):
478 recycled_values = self.value * (self.dimension[1] // len(self.value) + 1)
479 return pd.DataFrame(
480 [
481 [
482 recycled_values[i % len(recycled_values)]
483 for i in range(self.dimension[1])
484 ]
485 ]
486 * self.dimension[0]
487 )
489 if isinstance(self.value, tuple):
490 values = list(self.value)
491 return pd.DataFrame(
492 [
493 [values[i % len(values)]] * self.dimension[1]
494 for i in range(self.dimension[0])
495 ]
496 )
498 if isinstance(self.value, (int, float, str)):
499 return pd.DataFrame([[self.value] * self.dimension[1]] * self.dimension[0])
501 raise ValueError("Unsupported value type for DataFrame conversion.")
503 def update_row(self, row_index: int, row_value: list):
504 value = self.to_dataframe()
505 value.iloc[row_index] = row_value
506 return value
508 def update_column(self, column_index: int, column_value: list):
509 value = self.to_dataframe()
510 value.iloc[:, column_index] = column_value
511 return value
513 def update_cell(self, row_index: int, column_index: int, cell_value: Any):
514 value = self.to_dataframe()
515 value.iloc[row_index, column_index] = cell_value
516 return value
519class RTFPage(BaseModel):
520 """RTF page configuration"""
522 width: float | None = Field(default=None, description="Page width in inches")
523 height: float | None = Field(default=None, description="Page height in inches")
524 margin: Sequence[float] | None = Field(
525 default=None,
526 description="Page margins [left, right, top, bottom, header, footer] in inches",
527 )
528 orientation: str | None = Field(
529 default="portrait", description="Page orientation ('portrait' or 'landscape')"
530 )
531 col_width: float | None = Field(
532 default=None, description="Total width of table columns in inches"
533 )
534 nrow: int | None = Field(default=None, description="Number of rows per page")
535 use_color: bool | None = Field(
536 default=False, description="Whether to use color in the document"
537 )
538 page_title: str | None = Field(default="all", description="Title display location")
539 page_footnote: str | None = Field(
540 default="last", description="Footnote display location"
541 )
542 page_source: str | None = Field(
543 default="last", description="Source display location"
544 )
545 border_first: str | None = Field(
546 default="double", description="First row border style"
547 )
548 border_last: str | None = Field(
549 default="double", description="Last row border style"
550 )
552 def _set_default(self):
553 if self.orientation == "portrait":
554 self.width = self.width or 8.5
555 self.height = self.height or 11
556 self.margin = self.margin or [1.25, 1, 1.75, 1.25, 1.75, 1.00625]
557 self.col_width = self.col_width or self.width - 2.25
558 self.nrow = self.nrow or 40
560 if self.orientation == "landscape":
561 self.width = self.width or 11
562 self.height = self.height or 8.5
563 self.margin = self.margin or [1.0, 1.0, 2, 1.25, 1.25, 1.25]
564 self.col_width = self.col_width or self.width - 2.5
565 self.nrow = self.nrow or 24
567 if len(self.margin) != 6:
568 raise ValueError("Margin length must be 6.")
570 return self
573class RTFTitle(TextAttributes):
574 text: str | Sequence[str] | None = Field(
575 default=None, description="Title text content"
576 )
577 text_indent_reference: str | Sequence[str] | None = Field(
578 default="table",
579 description="Reference point for indentation ('page' or 'table')",
580 )
582 def __init__(self, **data):
583 defaults = {
584 "text_font": 1,
585 "text_font_size": 12,
586 "text_justification": "c",
587 "text_indent_first": 0,
588 "text_indent_left": 0,
589 "text_indent_right": 0,
590 "text_space": 1.0,
591 "text_space_before": 180.0,
592 "text_space_after": 180.0,
593 "text_hyphenation": True,
594 "text_convert": True,
595 }
597 # Update defaults with any provided values
598 defaults.update(data)
599 super().__init__(**defaults)
601 def _set_default(self):
602 for attr, value in self.__dict__.items():
603 if isinstance(value, (str, int, float, bool)):
604 setattr(self, attr, [value])
605 if isinstance(value, list):
606 setattr(self, attr, tuple(value))
607 return self
610class RTFColumnHeader(TableAttributes):
611 """Class for RTF column header settings"""
613 model_config = ConfigDict(arbitrary_types_allowed=True)
615 df: str | Sequence[str] | pd.DataFrame | None = Field(
616 default=None, description="Column header table"
617 )
619 def __init__(self, **data):
620 defaults = {
621 "border_left": "single",
622 "border_right": "single",
623 "border_top": "single",
624 "border_bottom": "",
625 "border_width": 15,
626 "cell_height": 0.15,
627 "cell_justification": "c",
628 "cell_vertical_justification": "bottom",
629 "text_font": 1,
630 "text_format": "",
631 "text_font_size": 9,
632 "text_justification": "c",
633 "text_indent_first": 0,
634 "text_indent_left": 0,
635 "text_indent_right": 0,
636 "text_space": 1,
637 "text_space_before": 15,
638 "text_space_after": 15,
639 "text_hyphenation": False,
640 "text_convert": True,
641 }
643 # Update defaults with any provided values
644 defaults.update(data)
645 super().__init__(**defaults)
647 # Convert df to DataFrame during initialization
648 if self.df is not None:
649 self.df = BroadcastValue(value=self.df).to_dataframe()
651 def _set_default(self):
652 for attr, value in self.__dict__.items():
653 if isinstance(value, (str, int, float, bool)):
654 setattr(self, attr, [value])
656 return self
659class RTFBody(TableAttributes):
660 """Class for RTF document body settings"""
662 model_config = ConfigDict(arbitrary_types_allowed=True)
663 df: pd.DataFrame | None = Field(default=None, description="Table data")
665 as_colheader: bool = Field(
666 default=True, description="Whether to display column headers"
667 )
668 group_by: Sequence[str] | None = Field(
669 default=None, description="Column name to group rows by"
670 )
671 page_by: Sequence[str] | None = Field(
672 default=None, description="Column name to create page breaks by"
673 )
674 new_page: bool = Field(default=False, description="Force new page before table")
675 pageby_header: bool = Field(default=True, description="Repeat headers on new pages")
676 pageby_row: str = Field(
677 default="column",
678 description="Page break handling for rows ('column' or 'value')",
679 )
680 subline_by: Sequence[str] | None = Field(
681 default=None, description="Column name to create sublines by"
682 )
683 last_row: bool = Field(
684 default=True,
685 description="Whether the table contains the last row of the final table",
686 )
688 def __init__(self, **data):
689 defaults = {
690 "border_left": "single",
691 "border_right": "single",
692 "border_first": "single",
693 "border_last": "single",
694 "border_width": 15,
695 "cell_height": 0.15,
696 "cell_justification": "c",
697 "cell_vertical_justification": "top",
698 "text_font": 1,
699 "text_font_size": 9,
700 "text_indent_first": 0,
701 "text_indent_left": 0,
702 "text_indent_right": 0,
703 "text_space": 1,
704 "text_space_before": 15,
705 "text_space_after": 15,
706 "text_hyphenation": False,
707 "text_convert": True,
708 }
710 # Update defaults with any provided values
711 defaults.update(data)
712 super().__init__(**defaults)
714 def _set_default(self):
715 for attr, value in self.__dict__.items():
716 if isinstance(value, (str, int, float, bool)) and attr not in [
717 "as_colheader",
718 "page_by",
719 "new_page",
720 "pageby_header",
721 "pageby_row",
722 "subline_by",
723 "last_row",
724 ]:
725 setattr(self, attr, [value])
727 self.border_top = self.border_top or ""
728 self.border_bottom = self.border_bottom or ""
729 self.border_left = self.border_left or "single"
730 self.border_right = self.border_right or "single"
731 self.border_first = self.border_first or "single"
732 self.border_last = self.border_last or "single"
733 self.cell_vertical_justification = self.cell_vertical_justification or "c"
734 self.text_justification = self.text_justification or "c"
736 if self.page_by is None:
737 if self.new_page:
738 raise ValueError(
739 "`new_page` must be `False` if `page_by` is not specified"
740 )
742 return self