tableio 0.4__tar.gz → 0.5__tar.gz
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.
- {tableio-0.4/tableio.egg-info → tableio-0.5}/PKG-INFO +5 -3
- {tableio-0.4 → tableio-0.5}/README.md +2 -2
- {tableio-0.4 → tableio-0.5}/README_pypi.md +4 -2
- {tableio-0.4 → tableio-0.5}/pyproject.toml +1 -1
- {tableio-0.4 → tableio-0.5}/setup.py +1 -1
- {tableio-0.4 → tableio-0.5}/src/tableio/__init__.py +3 -1
- tableio-0.5/src/tableio/border_helper.py +196 -0
- {tableio-0.4 → tableio-0.5}/src/tableio/capability.py +5 -1
- {tableio-0.4 → tableio-0.5}/src/tableio/tableio.py +66 -37
- {tableio-0.4 → tableio-0.5}/src/tableio/tableio_csv.py +2 -1
- {tableio-0.4 → tableio-0.5}/src/tableio/tableio_excel_openpyxl.py +33 -1
- {tableio-0.4 → tableio-0.5}/src/tableio/tableio_excel_pylightxl.py +2 -1
- {tableio-0.4 → tableio-0.5}/src/tableio/tableio_excel_xlsxwriter.py +64 -15
- {tableio-0.4 → tableio-0.5}/src/tableio/tableio_mformatbased.py +2 -1
- {tableio-0.4 → tableio-0.5}/src/tableio/tableio_ods_odfdo.py +88 -13
- {tableio-0.4 → tableio-0.5}/src/tableio/tableio_spreadsheetbased.py +42 -18
- tableio-0.5/src/tableio/tableio_types.py +132 -0
- {tableio-0.4 → tableio-0.5/tableio.egg-info}/PKG-INFO +5 -3
- {tableio-0.4 → tableio-0.5}/tableio.egg-info/SOURCES.txt +1 -0
- tableio-0.4/src/tableio/tableio_types.py +0 -61
- {tableio-0.4 → tableio-0.5}/LICENSE.txt +0 -0
- {tableio-0.4 → tableio-0.5}/setup.cfg +0 -0
- {tableio-0.4 → tableio-0.5}/src/tableio/_archive_rewrite.py +0 -0
- {tableio-0.4 → tableio-0.5}/src/tableio/color.py +0 -0
- {tableio-0.4 → tableio-0.5}/src/tableio/factory.py +0 -0
- {tableio-0.4 → tableio-0.5}/src/tableio/optional_args.py +0 -0
- {tableio-0.4 → tableio-0.5}/src/tableio/py.typed +0 -0
- {tableio-0.4 → tableio-0.5}/src/tableio/reg_pkg_formats.py +0 -0
- {tableio-0.4 → tableio-0.5}/src/tableio/tableio_excelbased.py +0 -0
- {tableio-0.4 → tableio-0.5}/src/tableio/tableio_mformat.py +0 -0
- {tableio-0.4 → tableio-0.5}/src/tableio/tableio_textbased.py +0 -0
- {tableio-0.4 → tableio-0.5}/src/tableio/value_type.py +0 -0
- {tableio-0.4 → tableio-0.5}/src/tableio/valueconversion.py +0 -0
- {tableio-0.4 → tableio-0.5}/tableio.egg-info/dependency_links.txt +0 -0
- {tableio-0.4 → tableio-0.5}/tableio.egg-info/requires.txt +0 -0
- {tableio-0.4 → tableio-0.5}/tableio.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tableio
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5
|
|
4
4
|
Summary: Uniform way to write table data to and read from different file formats
|
|
5
5
|
Author: Tom Björkholm
|
|
6
6
|
Author-email: Tom Björkholm <klausuler_linnet0q@icloud.com>
|
|
@@ -115,6 +115,7 @@ The main features are:
|
|
|
115
115
|
- using multiple sheets in spreadsheets
|
|
116
116
|
- finding location where some data is present in spreadsheet and doing modifications
|
|
117
117
|
at that position or at positions relative to that position.
|
|
118
|
+
- several border styles for tables
|
|
118
119
|
|
|
119
120
|
## Example programs
|
|
120
121
|
|
|
@@ -138,6 +139,7 @@ a better start by reading the examples.
|
|
|
138
139
|
|
|
139
140
|
| Version | Date | Python version | Comment |
|
|
140
141
|
|---------|-------------|----------------|-----------------------------|
|
|
142
|
+
| 0.5 | 2026 Apr 07 | 3.12 or newer | Added table border styles |
|
|
141
143
|
| 0.4 | 2026 Apr 06 | 3.12 or newer | Fix .xlsx and .ods syntax |
|
|
142
144
|
| 0.3 | 2026 Apr 01 | 3.12 or newer | Improved API, more backends |
|
|
143
145
|
| 0.2 | 2026 Mar 28 | 3.12 or newer | More features, API changes |
|
|
@@ -145,8 +147,8 @@ a better start by reading the examples.
|
|
|
145
147
|
|
|
146
148
|
## Test summary
|
|
147
149
|
|
|
148
|
-
- Test result:
|
|
150
|
+
- Test result: 1173 passed in 18s
|
|
149
151
|
- No flake8 warnings.
|
|
150
152
|
- No mypy errors found.
|
|
151
|
-
- Built version(s): 0.
|
|
153
|
+
- Built version(s): 0.5
|
|
152
154
|
- Build and test using Python 3.14.3
|
|
@@ -93,8 +93,8 @@ information about the build system. This README can also be viewed at
|
|
|
93
93
|
|
|
94
94
|
## Test summary
|
|
95
95
|
|
|
96
|
-
- Test result:
|
|
96
|
+
- Test result: 1173 passed in 18s
|
|
97
97
|
- No flake8 warnings.
|
|
98
98
|
- No mypy errors found.
|
|
99
|
-
- Built version(s): 0.
|
|
99
|
+
- Built version(s): 0.5
|
|
100
100
|
- Build and test using Python 3.14.3
|
|
@@ -87,6 +87,7 @@ The main features are:
|
|
|
87
87
|
- using multiple sheets in spreadsheets
|
|
88
88
|
- finding location where some data is present in spreadsheet and doing modifications
|
|
89
89
|
at that position or at positions relative to that position.
|
|
90
|
+
- several border styles for tables
|
|
90
91
|
|
|
91
92
|
## Example programs
|
|
92
93
|
|
|
@@ -110,6 +111,7 @@ a better start by reading the examples.
|
|
|
110
111
|
|
|
111
112
|
| Version | Date | Python version | Comment |
|
|
112
113
|
|---------|-------------|----------------|-----------------------------|
|
|
114
|
+
| 0.5 | 2026 Apr 07 | 3.12 or newer | Added table border styles |
|
|
113
115
|
| 0.4 | 2026 Apr 06 | 3.12 or newer | Fix .xlsx and .ods syntax |
|
|
114
116
|
| 0.3 | 2026 Apr 01 | 3.12 or newer | Improved API, more backends |
|
|
115
117
|
| 0.2 | 2026 Mar 28 | 3.12 or newer | More features, API changes |
|
|
@@ -117,8 +119,8 @@ a better start by reading the examples.
|
|
|
117
119
|
|
|
118
120
|
## Test summary
|
|
119
121
|
|
|
120
|
-
- Test result:
|
|
122
|
+
- Test result: 1173 passed in 18s
|
|
121
123
|
- No flake8 warnings.
|
|
122
124
|
- No mypy errors found.
|
|
123
|
-
- Built version(s): 0.
|
|
125
|
+
- Built version(s): 0.5
|
|
124
126
|
- Build and test using Python 3.14.3
|
|
@@ -11,7 +11,8 @@ from tableio.color import Color
|
|
|
11
11
|
from tableio.factory import create_tableio, filter_args_tableio, \
|
|
12
12
|
list_implementations_tableio, list_registered_tableio
|
|
13
13
|
from tableio.optional_args import CsvDialect, OptionalArgs, OptionalArgsDict
|
|
14
|
-
from tableio.tableio import
|
|
14
|
+
from tableio.tableio import TableIO
|
|
15
|
+
from tableio.tableio_types import TableBorderStyle, Box, FileAccess, Position
|
|
15
16
|
from tableio.value_type import CellT, DictData, DictDataMap, Fmt, \
|
|
16
17
|
FmtDictData, FmtDictRow, FmtListData, FmtListRow, ListData, \
|
|
17
18
|
ListDataSeq, ReadResult, Value, ValueFmt
|
|
@@ -20,6 +21,7 @@ __all__ = ['create_tableio',
|
|
|
20
21
|
'TableIO',
|
|
21
22
|
'FileAccess',
|
|
22
23
|
'Box',
|
|
24
|
+
'TableBorderStyle',
|
|
23
25
|
'Position',
|
|
24
26
|
'OptionalArgs',
|
|
25
27
|
'OptionalArgsDict',
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
"""Helpers for working with normalized table borders."""
|
|
3
|
+
|
|
4
|
+
# Copyright (c) 2026 Tom Björkholm
|
|
5
|
+
# MIT License
|
|
6
|
+
|
|
7
|
+
from enum import IntEnum
|
|
8
|
+
from typing import NamedTuple, Optional
|
|
9
|
+
|
|
10
|
+
from tableio.capability import Capabilities, CapabilityNotSupported, \
|
|
11
|
+
Strictness
|
|
12
|
+
from tableio.tableio_types import TableBorderStyle
|
|
13
|
+
from tableio.value_type import Fmt
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BorderWeight(IntEnum):
|
|
17
|
+
"""Semantic weight for one border edge."""
|
|
18
|
+
|
|
19
|
+
NONE = 0
|
|
20
|
+
THIN = 1
|
|
21
|
+
THICK = 2
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CellBorder(NamedTuple):
|
|
25
|
+
"""Border weights for the four edges of one cell."""
|
|
26
|
+
|
|
27
|
+
top: BorderWeight
|
|
28
|
+
right: BorderWeight
|
|
29
|
+
bottom: BorderWeight
|
|
30
|
+
left: BorderWeight
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
NO_BORDERS = CellBorder(
|
|
34
|
+
BorderWeight.NONE,
|
|
35
|
+
BorderWeight.NONE,
|
|
36
|
+
BorderWeight.NONE,
|
|
37
|
+
BorderWeight.NONE)
|
|
38
|
+
"""The absence of borders on all four cell edges."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CellStyleState(NamedTuple):
|
|
42
|
+
"""Combined cell formatting state used by style-caching backends."""
|
|
43
|
+
|
|
44
|
+
fmt: Fmt = Fmt()
|
|
45
|
+
font_size: Optional[int] = None
|
|
46
|
+
borders: CellBorder = NO_BORDERS
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
DEFAULT_CELL_STYLE = CellStyleState()
|
|
50
|
+
"""The default cell formatting state with no extra styling."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class _BorderComponents(NamedTuple):
|
|
54
|
+
"""Normalized border weights for one table style."""
|
|
55
|
+
|
|
56
|
+
outer: BorderWeight
|
|
57
|
+
first_row_separator: BorderWeight
|
|
58
|
+
inner_horizontal: BorderWeight
|
|
59
|
+
inner_vertical: BorderWeight
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _thicker(first: BorderWeight, second: BorderWeight) -> BorderWeight:
|
|
63
|
+
"""Return the thicker of two border weights."""
|
|
64
|
+
if first >= second:
|
|
65
|
+
return first
|
|
66
|
+
return second
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
_STYLE_COMPONENTS: dict[TableBorderStyle, _BorderComponents] = {
|
|
70
|
+
TableBorderStyle.NONE: _BorderComponents(
|
|
71
|
+
outer=BorderWeight.NONE,
|
|
72
|
+
first_row_separator=BorderWeight.NONE,
|
|
73
|
+
inner_horizontal=BorderWeight.NONE,
|
|
74
|
+
inner_vertical=BorderWeight.NONE),
|
|
75
|
+
TableBorderStyle.OUTER_THIN: _BorderComponents(
|
|
76
|
+
outer=BorderWeight.THIN,
|
|
77
|
+
first_row_separator=BorderWeight.NONE,
|
|
78
|
+
inner_horizontal=BorderWeight.NONE,
|
|
79
|
+
inner_vertical=BorderWeight.NONE),
|
|
80
|
+
TableBorderStyle.OUTER_THICK: _BorderComponents(
|
|
81
|
+
outer=BorderWeight.THICK,
|
|
82
|
+
first_row_separator=BorderWeight.NONE,
|
|
83
|
+
inner_horizontal=BorderWeight.NONE,
|
|
84
|
+
inner_vertical=BorderWeight.NONE),
|
|
85
|
+
TableBorderStyle.OUTER_FIRST_ROW_THIN: _BorderComponents(
|
|
86
|
+
outer=BorderWeight.THIN,
|
|
87
|
+
first_row_separator=BorderWeight.THIN,
|
|
88
|
+
inner_horizontal=BorderWeight.NONE,
|
|
89
|
+
inner_vertical=BorderWeight.NONE),
|
|
90
|
+
TableBorderStyle.OUTER_FIRST_ROW_THICK: _BorderComponents(
|
|
91
|
+
outer=BorderWeight.THICK,
|
|
92
|
+
first_row_separator=BorderWeight.THICK,
|
|
93
|
+
inner_horizontal=BorderWeight.NONE,
|
|
94
|
+
inner_vertical=BorderWeight.NONE),
|
|
95
|
+
TableBorderStyle.OUTER_THICK_FIRST_ROW_THIN: _BorderComponents(
|
|
96
|
+
outer=BorderWeight.THICK,
|
|
97
|
+
first_row_separator=BorderWeight.THIN,
|
|
98
|
+
inner_horizontal=BorderWeight.NONE,
|
|
99
|
+
inner_vertical=BorderWeight.NONE),
|
|
100
|
+
TableBorderStyle.OUTER_FIRST_ROW_THICK_VERTICAL_THIN:
|
|
101
|
+
_BorderComponents(
|
|
102
|
+
outer=BorderWeight.THICK,
|
|
103
|
+
first_row_separator=BorderWeight.THICK,
|
|
104
|
+
inner_horizontal=BorderWeight.NONE,
|
|
105
|
+
inner_vertical=BorderWeight.THIN),
|
|
106
|
+
TableBorderStyle.OUTER_FIRST_ROW_THICK_INNER_THIN: _BorderComponents(
|
|
107
|
+
outer=BorderWeight.THICK,
|
|
108
|
+
first_row_separator=BorderWeight.THICK,
|
|
109
|
+
inner_horizontal=BorderWeight.THIN,
|
|
110
|
+
inner_vertical=BorderWeight.THIN),
|
|
111
|
+
TableBorderStyle.OUTER_THICK_INNER_THIN: _BorderComponents(
|
|
112
|
+
outer=BorderWeight.THICK,
|
|
113
|
+
first_row_separator=BorderWeight.NONE,
|
|
114
|
+
inner_horizontal=BorderWeight.THIN,
|
|
115
|
+
inner_vertical=BorderWeight.THIN),
|
|
116
|
+
TableBorderStyle.ALL_THIN: _BorderComponents(
|
|
117
|
+
outer=BorderWeight.THIN,
|
|
118
|
+
first_row_separator=BorderWeight.NONE,
|
|
119
|
+
inner_horizontal=BorderWeight.THIN,
|
|
120
|
+
inner_vertical=BorderWeight.THIN),
|
|
121
|
+
TableBorderStyle.ALL_THICK: _BorderComponents(
|
|
122
|
+
outer=BorderWeight.THICK,
|
|
123
|
+
first_row_separator=BorderWeight.NONE,
|
|
124
|
+
inner_horizontal=BorderWeight.THICK,
|
|
125
|
+
inner_vertical=BorderWeight.THICK)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class BorderHelper:
|
|
130
|
+
"""Normalize one public table-border style for backend use."""
|
|
131
|
+
|
|
132
|
+
def __init__(self, border_style: TableBorderStyle,
|
|
133
|
+
capabilities: Capabilities):
|
|
134
|
+
"""Initialize the normalized table-border helper."""
|
|
135
|
+
self.border_style = self._checked_style(border_style, capabilities)
|
|
136
|
+
components = _STYLE_COMPONENTS[self.border_style]
|
|
137
|
+
self.outer = components.outer
|
|
138
|
+
self.first_row_separator = components.first_row_separator
|
|
139
|
+
self.inner_horizontal = components.inner_horizontal
|
|
140
|
+
self.inner_vertical = components.inner_vertical
|
|
141
|
+
|
|
142
|
+
@staticmethod
|
|
143
|
+
def _checked_style(border_style: TableBorderStyle,
|
|
144
|
+
capabilities: Capabilities) -> TableBorderStyle:
|
|
145
|
+
"""Return the effective border style after capability handling."""
|
|
146
|
+
if border_style == TableBorderStyle.NONE:
|
|
147
|
+
return border_style
|
|
148
|
+
capability = capabilities.can_write_borders
|
|
149
|
+
if capability.supported:
|
|
150
|
+
return border_style
|
|
151
|
+
if capability.strictness == Strictness.IGNORE:
|
|
152
|
+
return TableBorderStyle.NONE
|
|
153
|
+
raise CapabilityNotSupported('write borders to the table')
|
|
154
|
+
|
|
155
|
+
def has_borders(self) -> bool:
|
|
156
|
+
"""Return whether any border is active in this normalized style."""
|
|
157
|
+
return (
|
|
158
|
+
self.outer != BorderWeight.NONE or
|
|
159
|
+
self.first_row_separator != BorderWeight.NONE or
|
|
160
|
+
self.inner_horizontal != BorderWeight.NONE or
|
|
161
|
+
self.inner_vertical != BorderWeight.NONE)
|
|
162
|
+
|
|
163
|
+
def _horizontal_boundary(self, boundary_index: int,
|
|
164
|
+
row_count: int) -> BorderWeight:
|
|
165
|
+
"""Return the weight of one horizontal table boundary."""
|
|
166
|
+
if boundary_index in (0, row_count):
|
|
167
|
+
weight = self.outer
|
|
168
|
+
else:
|
|
169
|
+
weight = self.inner_horizontal
|
|
170
|
+
if boundary_index == 1:
|
|
171
|
+
weight = _thicker(weight, self.first_row_separator)
|
|
172
|
+
return weight
|
|
173
|
+
|
|
174
|
+
def _vertical_boundary(self, boundary_index: int,
|
|
175
|
+
column_count: int) -> BorderWeight:
|
|
176
|
+
"""Return the weight of one vertical table boundary."""
|
|
177
|
+
if boundary_index in (0, column_count):
|
|
178
|
+
return self.outer
|
|
179
|
+
return self.inner_vertical
|
|
180
|
+
|
|
181
|
+
def cell_border(self, row_index: int, column_index: int, row_count: int,
|
|
182
|
+
column_count: int) -> CellBorder:
|
|
183
|
+
"""Return the four border edges for one cell in a table."""
|
|
184
|
+
if row_count < 1:
|
|
185
|
+
raise ValueError('row_count must be at least 1.')
|
|
186
|
+
if column_count < 1:
|
|
187
|
+
raise ValueError('column_count must be at least 1.')
|
|
188
|
+
if not 0 <= row_index < row_count:
|
|
189
|
+
raise ValueError('row_index is outside the table.')
|
|
190
|
+
if not 0 <= column_index < column_count:
|
|
191
|
+
raise ValueError('column_index is outside the table.')
|
|
192
|
+
return CellBorder(
|
|
193
|
+
top=self._horizontal_boundary(row_index, row_count),
|
|
194
|
+
right=self._vertical_boundary(column_index + 1, column_count),
|
|
195
|
+
bottom=self._horizontal_boundary(row_index + 1, row_count),
|
|
196
|
+
left=self._vertical_boundary(column_index, column_count))
|
|
@@ -90,6 +90,9 @@ class Capabilities(NamedTuple):
|
|
|
90
90
|
can_find_value_position: SingleCapability = SingleCapability()
|
|
91
91
|
"""The reader/writer class can find the position of a value."""
|
|
92
92
|
|
|
93
|
+
can_write_borders: SingleCapability = SingleCapability()
|
|
94
|
+
"""The writer class can write borders to the table."""
|
|
95
|
+
|
|
93
96
|
|
|
94
97
|
def single_capability_match(offered: SingleCapability,
|
|
95
98
|
will_use: SingleCapability,
|
|
@@ -246,4 +249,5 @@ CAP_ALL_IMPLEMENTED = Capabilities(can_read=CAP_IMPLEMENTED,
|
|
|
246
249
|
can_read_box=CAP_IMPLEMENTED,
|
|
247
250
|
can_write_highlight=CAP_IMPLEMENTED,
|
|
248
251
|
multi_sheet=CAP_IMPLEMENTED,
|
|
249
|
-
can_find_value_position=CAP_IMPLEMENTED
|
|
252
|
+
can_find_value_position=CAP_IMPLEMENTED,
|
|
253
|
+
can_write_borders=CAP_IMPLEMENTED)
|
|
@@ -12,11 +12,13 @@ from typing import NamedTuple, Callable, Optional
|
|
|
12
12
|
from mformat.mformat import PathLike
|
|
13
13
|
from tableio.capability import Capabilities, SingleCapability, Strictness, \
|
|
14
14
|
CapabilityNotSupported
|
|
15
|
-
from tableio.tableio_types import Box, Descriptor, FileAccess, Position
|
|
15
|
+
from tableio.tableio_types import Box, Descriptor, FileAccess, Position, \
|
|
16
|
+
TableBorderStyle
|
|
16
17
|
from tableio.value_type import CellT, ListDataSeq, DictDataMap, \
|
|
17
18
|
normalize_dict_data, ReadResult, ListData, Value, Fmt, DictData, \
|
|
18
19
|
FmtListData, FmtDictData, FmtDictRow, row_strip_format_list, \
|
|
19
20
|
row_strip_format_dict
|
|
21
|
+
from tableio.border_helper import BorderHelper
|
|
20
22
|
|
|
21
23
|
__all__ = ['Box', 'Descriptor', 'FileAccess', 'Position', 'TableIO']
|
|
22
24
|
|
|
@@ -174,6 +176,8 @@ class TableIO:
|
|
|
174
176
|
"""If True, data will be written as a range that can be filtered."""
|
|
175
177
|
box: Optional[Box]
|
|
176
178
|
"""The box to write the data into."""
|
|
179
|
+
borders: BorderHelper
|
|
180
|
+
"""The normalized borders of the table."""
|
|
177
181
|
|
|
178
182
|
class ImplMetaForDictWrite(NamedTuple):
|
|
179
183
|
"""Meta data for writing dict table to pass to implementation."""
|
|
@@ -185,22 +189,26 @@ class TableIO:
|
|
|
185
189
|
first_row_format: Optional[Fmt]
|
|
186
190
|
"""The format specification for the first row."""
|
|
187
191
|
|
|
188
|
-
def write_table_listdata(
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
def write_table_listdata(
|
|
193
|
+
self, data: ListDataSeq[CellT],
|
|
194
|
+
filtered_data_range: bool = False,
|
|
195
|
+
box: Optional[Box] = None,
|
|
196
|
+
border_style: TableBorderStyle = TableBorderStyle.NONE) -> \
|
|
197
|
+
Position:
|
|
191
198
|
"""Write a table of list data to the file.
|
|
192
199
|
|
|
193
200
|
Write a table of list data to the file.
|
|
194
201
|
If a box is provided the data will be written into the box.
|
|
195
202
|
The data must fit into the box.
|
|
196
|
-
Notice when
|
|
203
|
+
Notice when specifying a box: It is not allowed to write a
|
|
197
204
|
table that partly overwrites an existing table.
|
|
198
205
|
Args:
|
|
199
206
|
data: The list data to write.
|
|
200
207
|
filtered_data_range: If True, the data written will be
|
|
201
208
|
marked as a data range that can be filtered.
|
|
202
209
|
box: The box to write the data into. If box.bottom or box.right is
|
|
203
|
-
|
|
210
|
+
not None, the data must fill the box.
|
|
211
|
+
border_style: The border style to apply to the written table.
|
|
204
212
|
Raises:
|
|
205
213
|
ValueError: If the data shape is invalid or does not fit in box.
|
|
206
214
|
CapabilityNotSupported: If a requested capability is unsupported
|
|
@@ -214,27 +222,33 @@ class TableIO:
|
|
|
214
222
|
c_box = self._check_box_write(box)
|
|
215
223
|
self._check_listdimensions(data, c_box)
|
|
216
224
|
c_filt_range = self._check_filtered_data_range(filtered_data_range)
|
|
225
|
+
c_borders = self._check_border_style(border_style)
|
|
217
226
|
impl_meta: TableIO.ImplMetaForWrite = \
|
|
218
227
|
TableIO.ImplMetaForWrite(filtered_data_range=c_filt_range,
|
|
219
|
-
box=c_box
|
|
228
|
+
box=c_box,
|
|
229
|
+
borders=c_borders)
|
|
220
230
|
return self._write_table_listdata(data=data, impl_meta=impl_meta)
|
|
221
231
|
|
|
222
|
-
def write_table_fmtlistdata(
|
|
223
|
-
|
|
224
|
-
|
|
232
|
+
def write_table_fmtlistdata(
|
|
233
|
+
self, data: FmtListData,
|
|
234
|
+
filtered_data_range: bool = False,
|
|
235
|
+
box: Optional[Box] = None,
|
|
236
|
+
border_style: TableBorderStyle = TableBorderStyle.NONE) -> \
|
|
237
|
+
Position:
|
|
225
238
|
"""Write a table of list data to the file.
|
|
226
239
|
|
|
227
240
|
Write a table of list data to the file.
|
|
228
241
|
If a box is provided the data will be written into the box.
|
|
229
242
|
The data must fit into the box.
|
|
230
|
-
Notice when
|
|
243
|
+
Notice when specifying a box: It is not allowed to write a
|
|
231
244
|
table that partly overwrites an existing table.
|
|
232
245
|
Args:
|
|
233
246
|
data: The list data to write.
|
|
234
247
|
filtered_data_range: If True, the data written will be
|
|
235
248
|
marked as a data range that can be filtered.
|
|
236
249
|
box: The box to write the data into. If box.bottom or box.right is
|
|
237
|
-
|
|
250
|
+
not None, the data must fill the box.
|
|
251
|
+
border_style: The border style to apply to the written table.
|
|
238
252
|
Raises:
|
|
239
253
|
ValueError: If the data shape is invalid or does not fit in box.
|
|
240
254
|
CapabilityNotSupported: If a requested capability is unsupported
|
|
@@ -248,25 +262,28 @@ class TableIO:
|
|
|
248
262
|
c_box = self._check_box_write(box)
|
|
249
263
|
self._check_listdimensions(row_strip_format_list(data), c_box)
|
|
250
264
|
c_filt_range = self._check_filtered_data_range(filtered_data_range)
|
|
265
|
+
c_borders = self._check_border_style(border_style)
|
|
251
266
|
impl_meta: TableIO.ImplMetaForWrite = \
|
|
252
267
|
TableIO.ImplMetaForWrite(filtered_data_range=c_filt_range,
|
|
253
|
-
box=c_box)
|
|
268
|
+
box=c_box, borders=c_borders)
|
|
254
269
|
return self._write_table_fmtlistdata(data=data, impl_meta=impl_meta)
|
|
255
270
|
|
|
256
|
-
def write_table_dictdata(
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
271
|
+
def write_table_dictdata( # pylint: disable=too-many-arguments,too-many-positional-arguments # noqa: E501
|
|
272
|
+
self, data: DictDataMap[CellT],
|
|
273
|
+
column_order: list[str],
|
|
274
|
+
first_row_format: Optional[Fmt] = None,
|
|
275
|
+
missing_ok: bool = False,
|
|
276
|
+
extra_ok: bool = False,
|
|
277
|
+
filtered_data_range: bool = False,
|
|
278
|
+
box: Optional[Box] = None,
|
|
279
|
+
border_style: TableBorderStyle = TableBorderStyle.NONE) -> \
|
|
280
|
+
Position:
|
|
264
281
|
"""Write a table of dict data to the file.
|
|
265
282
|
|
|
266
283
|
Write a table of dict data to the file.
|
|
267
284
|
If a box is provided the data will be written into the box.
|
|
268
285
|
The data must fit into the box.
|
|
269
|
-
Notice when
|
|
286
|
+
Notice when specifying a box: It is not allowed to write a
|
|
270
287
|
table that partly overwrites an existing table.
|
|
271
288
|
Args:
|
|
272
289
|
data: The dict data to write.
|
|
@@ -284,7 +301,8 @@ class TableIO:
|
|
|
284
301
|
filtered_data_range: If True, the data written will be
|
|
285
302
|
marked as a data range that can be filtered.
|
|
286
303
|
box: The box to write the data into. If box.bottom or box.right is
|
|
287
|
-
|
|
304
|
+
not None, the data must fill the box.
|
|
305
|
+
border_style: The border style to apply to the written table.
|
|
288
306
|
Raises:
|
|
289
307
|
ValueError: If missing_ok is False and data is missing for a
|
|
290
308
|
column in the column_order.
|
|
@@ -303,29 +321,32 @@ class TableIO:
|
|
|
303
321
|
ndata = normalize_dict_data(data, column_order, missing_ok, extra_ok)
|
|
304
322
|
self._check_dictdimensions(ndata, c_box)
|
|
305
323
|
c_filt_range = self._check_filtered_data_range(filtered_data_range)
|
|
324
|
+
c_borders = self._check_border_style(border_style)
|
|
306
325
|
common_impl: TableIO.ImplMetaForWrite = \
|
|
307
326
|
TableIO.ImplMetaForWrite(filtered_data_range=c_filt_range,
|
|
308
|
-
box=c_box)
|
|
327
|
+
box=c_box, borders=c_borders)
|
|
309
328
|
impl_meta: TableIO.ImplMetaForDictWrite = \
|
|
310
329
|
TableIO.ImplMetaForDictWrite(common_impl=common_impl,
|
|
311
330
|
column_order=column_order,
|
|
312
331
|
first_row_format=first_row_format)
|
|
313
332
|
return self._write_table_dictdata(data=ndata, impl_meta=impl_meta)
|
|
314
333
|
|
|
315
|
-
def write_table_fmtdictdata(
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
334
|
+
def write_table_fmtdictdata( # pylint: disable=too-many-arguments,too-many-positional-arguments # noqa: E501
|
|
335
|
+
self, data: FmtDictData,
|
|
336
|
+
column_order: list[str],
|
|
337
|
+
first_row_format: Optional[Fmt] = None,
|
|
338
|
+
missing_ok: bool = False,
|
|
339
|
+
extra_ok: bool = False,
|
|
340
|
+
filtered_data_range: bool = False,
|
|
341
|
+
box: Optional[Box] = None,
|
|
342
|
+
border_style: TableBorderStyle = TableBorderStyle.NONE) -> \
|
|
343
|
+
Position:
|
|
323
344
|
"""Write a table of dict data to the file.
|
|
324
345
|
|
|
325
346
|
Write a table of dict data to the file.
|
|
326
347
|
If a box is provided the data will be written into the box.
|
|
327
348
|
The data must fit into the box.
|
|
328
|
-
Notice when
|
|
349
|
+
Notice when specifying a box: It is not allowed to write a
|
|
329
350
|
table that partly overwrites an existing table.
|
|
330
351
|
Args:
|
|
331
352
|
data: The dict data to write.
|
|
@@ -343,7 +364,8 @@ class TableIO:
|
|
|
343
364
|
filtered_data_range: If True, the data written will be
|
|
344
365
|
marked as a data range that can be filtered.
|
|
345
366
|
box: The box to write the data into. If box.bottom or box.right is
|
|
346
|
-
|
|
367
|
+
not None, the data must fill the box.
|
|
368
|
+
border_style: The border style to apply to the written table.
|
|
347
369
|
Raises:
|
|
348
370
|
ValueError: If missing_ok is False and data is missing for a
|
|
349
371
|
column in the column_order.
|
|
@@ -370,10 +392,12 @@ class TableIO:
|
|
|
370
392
|
FmtDictRow(values=row_values, fmt=row.fmt)
|
|
371
393
|
for row, row_values in zip(data, normalized_values,
|
|
372
394
|
strict=True)]
|
|
373
|
-
c_filt_range = self._check_filtered_data_range(filtered_data_range)
|
|
374
395
|
common_impl: TableIO.ImplMetaForWrite = \
|
|
375
|
-
TableIO.ImplMetaForWrite(
|
|
376
|
-
|
|
396
|
+
TableIO.ImplMetaForWrite(
|
|
397
|
+
filtered_data_range=self._check_filtered_data_range(
|
|
398
|
+
filtered_data_range),
|
|
399
|
+
box=c_box,
|
|
400
|
+
borders=self._check_border_style(border_style))
|
|
377
401
|
impl_meta: TableIO.ImplMetaForDictWrite = \
|
|
378
402
|
TableIO.ImplMetaForDictWrite(common_impl=common_impl,
|
|
379
403
|
column_order=column_order,
|
|
@@ -749,6 +773,11 @@ class TableIO:
|
|
|
749
773
|
return False
|
|
750
774
|
raise CapabilityNotSupported('write a filtered data range')
|
|
751
775
|
|
|
776
|
+
def _check_border_style(self, border_style: TableBorderStyle) -> \
|
|
777
|
+
BorderHelper:
|
|
778
|
+
"""Check if the requested table border style is supported."""
|
|
779
|
+
return BorderHelper(border_style, self.get_capabilities())
|
|
780
|
+
|
|
752
781
|
def _write_heading(self, heading: str, level: int) -> Position:
|
|
753
782
|
"""Write a heading to the file.
|
|
754
783
|
|
|
@@ -181,7 +181,8 @@ class TableIOCsv(TableIOTextBased):
|
|
|
181
181
|
filtered_data_range=not_sup_ignore,
|
|
182
182
|
can_write_box=not_sup_strict,
|
|
183
183
|
can_read_box=not_sup_strict,
|
|
184
|
-
can_write_highlight=not_sup_ignore
|
|
184
|
+
can_write_highlight=not_sup_ignore,
|
|
185
|
+
can_write_borders=not_sup_ignore)
|
|
185
186
|
|
|
186
187
|
def _end_state(self) -> None:
|
|
187
188
|
"""End the state of the CSV file."""
|
|
@@ -8,13 +8,14 @@ from pathlib import Path
|
|
|
8
8
|
from typing import Callable, Optional
|
|
9
9
|
from xml.etree import ElementTree as ET
|
|
10
10
|
from openpyxl import Workbook, load_workbook
|
|
11
|
-
from openpyxl.styles import Font, PatternFill
|
|
11
|
+
from openpyxl.styles import Border, Font, PatternFill, Side
|
|
12
12
|
from openpyxl.utils.cell import get_column_letter, range_boundaries
|
|
13
13
|
from openpyxl.worksheet.table import Table
|
|
14
14
|
from openpyxl.worksheet.worksheet import Worksheet
|
|
15
15
|
from mformat.mformat import PathLike
|
|
16
16
|
from tableio._archive_rewrite import rewrite_zip_archive, \
|
|
17
17
|
temporary_output_path
|
|
18
|
+
from tableio.border_helper import BorderWeight, CellBorder
|
|
18
19
|
from tableio.color import Color
|
|
19
20
|
from tableio.tableio import Descriptor, FileAccess
|
|
20
21
|
from tableio.tableio_excelbased import TableIOExcelBased
|
|
@@ -92,6 +93,7 @@ class TableIOExcelOpenPyXL(TableIOExcelBased):
|
|
|
92
93
|
self.read_workbook: Optional[Workbook] = None
|
|
93
94
|
self.worksheet: Optional[Worksheet] = None
|
|
94
95
|
self.read_worksheet: Optional[Worksheet] = None
|
|
96
|
+
self._border_cache: dict[CellBorder, Border] = {}
|
|
95
97
|
|
|
96
98
|
@classmethod
|
|
97
99
|
def get_capabilities(cls) -> Capabilities:
|
|
@@ -109,6 +111,7 @@ class TableIOExcelOpenPyXL(TableIOExcelBased):
|
|
|
109
111
|
"""Open the Excel workbook."""
|
|
110
112
|
if self.workbook is not None:
|
|
111
113
|
raise RuntimeError(f'File {self.file_name} already open')
|
|
114
|
+
self._border_cache = {}
|
|
112
115
|
if self.file_access == FileAccess.CREATE:
|
|
113
116
|
workbook = Workbook()
|
|
114
117
|
self.workbook = workbook
|
|
@@ -256,6 +259,35 @@ class TableIOExcelOpenPyXL(TableIOExcelBased):
|
|
|
256
259
|
cell.font = Font(bold=fmt.bold, italic=fmt.italic)
|
|
257
260
|
cell.fill = self._highlight_fill(fmt.highlight)
|
|
258
261
|
|
|
262
|
+
@staticmethod
|
|
263
|
+
def _border_side(weight: BorderWeight) -> Side:
|
|
264
|
+
"""Return one OpenPyXL side object for the requested weight."""
|
|
265
|
+
if weight == BorderWeight.NONE:
|
|
266
|
+
return Side()
|
|
267
|
+
if weight == BorderWeight.THIN:
|
|
268
|
+
return Side(style='thin')
|
|
269
|
+
return Side(style='medium')
|
|
270
|
+
|
|
271
|
+
def _border_value(self, borders: CellBorder) -> Border:
|
|
272
|
+
"""Return one cached OpenPyXL border object."""
|
|
273
|
+
cached = self._border_cache.get(borders)
|
|
274
|
+
if cached is not None:
|
|
275
|
+
return cached
|
|
276
|
+
border = Border(
|
|
277
|
+
left=self._border_side(borders.left),
|
|
278
|
+
right=self._border_side(borders.right),
|
|
279
|
+
top=self._border_side(borders.top),
|
|
280
|
+
bottom=self._border_side(borders.bottom))
|
|
281
|
+
self._border_cache[borders] = border
|
|
282
|
+
return border
|
|
283
|
+
|
|
284
|
+
def _set_cell_borders(self, sheet: object, row: int, column: int,
|
|
285
|
+
borders: CellBorder) -> None:
|
|
286
|
+
"""Apply normalized borders to one worksheet cell."""
|
|
287
|
+
worksheet = get_checked_type(sheet, Worksheet)
|
|
288
|
+
cell = worksheet.cell(row=row + 1, column=column + 1)
|
|
289
|
+
cell.border = self._border_value(borders)
|
|
290
|
+
|
|
259
291
|
def _apply_heading_style(self, row: int, column: int, level: int) -> None:
|
|
260
292
|
"""Apply the heading font to one worksheet cell."""
|
|
261
293
|
assert self.worksheet is not None
|
|
@@ -341,7 +341,8 @@ class TableIOExcelPylightxl(TableIOExcelBased):
|
|
|
341
341
|
can_read_box=CAP_IMPLEMENTED,
|
|
342
342
|
can_write_highlight=CAP_IGNORED,
|
|
343
343
|
multi_sheet=CAP_IMPLEMENTED,
|
|
344
|
-
can_find_value_position=CAP_IMPLEMENTED
|
|
344
|
+
can_find_value_position=CAP_IMPLEMENTED,
|
|
345
|
+
can_write_borders=CAP_IGNORED)
|
|
345
346
|
|
|
346
347
|
def open(self) -> None:
|
|
347
348
|
"""Open the workbook."""
|