tableio 0.3__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.3/tableio.egg-info → tableio-0.5}/PKG-INFO +9 -6
- {tableio-0.3 → tableio-0.5}/README.md +2 -2
- {tableio-0.3 → tableio-0.5}/README_pypi.md +6 -3
- {tableio-0.3 → tableio-0.5}/pyproject.toml +1 -1
- {tableio-0.3 → tableio-0.5}/setup.py +3 -3
- {tableio-0.3 → tableio-0.5}/src/tableio/__init__.py +3 -1
- tableio-0.5/src/tableio/_archive_rewrite.py +80 -0
- tableio-0.5/src/tableio/border_helper.py +196 -0
- {tableio-0.3 → tableio-0.5}/src/tableio/capability.py +5 -1
- {tableio-0.3 → tableio-0.5}/src/tableio/factory.py +15 -9
- {tableio-0.3 → tableio-0.5}/src/tableio/tableio.py +68 -37
- {tableio-0.3 → tableio-0.5}/src/tableio/tableio_csv.py +2 -1
- {tableio-0.3 → tableio-0.5}/src/tableio/tableio_excel_openpyxl.py +90 -2
- {tableio-0.3 → tableio-0.5}/src/tableio/tableio_excel_pylightxl.py +84 -61
- {tableio-0.3 → tableio-0.5}/src/tableio/tableio_excel_xlsxwriter.py +64 -15
- {tableio-0.3 → tableio-0.5}/src/tableio/tableio_mformatbased.py +2 -1
- {tableio-0.3 → tableio-0.5}/src/tableio/tableio_ods_odfdo.py +190 -15
- {tableio-0.3 → tableio-0.5}/src/tableio/tableio_spreadsheetbased.py +42 -18
- tableio-0.5/src/tableio/tableio_types.py +132 -0
- {tableio-0.3 → tableio-0.5/tableio.egg-info}/PKG-INFO +9 -6
- {tableio-0.3 → tableio-0.5}/tableio.egg-info/SOURCES.txt +2 -0
- {tableio-0.3 → tableio-0.5}/tableio.egg-info/requires.txt +2 -2
- tableio-0.3/src/tableio/tableio_types.py +0 -61
- {tableio-0.3 → tableio-0.5}/LICENSE.txt +0 -0
- {tableio-0.3 → tableio-0.5}/setup.cfg +0 -0
- {tableio-0.3 → tableio-0.5}/src/tableio/color.py +0 -0
- {tableio-0.3 → tableio-0.5}/src/tableio/optional_args.py +0 -0
- {tableio-0.3 → tableio-0.5}/src/tableio/py.typed +0 -0
- {tableio-0.3 → tableio-0.5}/src/tableio/reg_pkg_formats.py +0 -0
- {tableio-0.3 → tableio-0.5}/src/tableio/tableio_excelbased.py +0 -0
- {tableio-0.3 → tableio-0.5}/src/tableio/tableio_mformat.py +0 -0
- {tableio-0.3 → tableio-0.5}/src/tableio/tableio_textbased.py +0 -0
- {tableio-0.3 → tableio-0.5}/src/tableio/value_type.py +0 -0
- {tableio-0.3 → tableio-0.5}/src/tableio/valueconversion.py +0 -0
- {tableio-0.3 → tableio-0.5}/tableio.egg-info/dependency_links.txt +0 -0
- {tableio-0.3 → 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>
|
|
@@ -17,8 +17,8 @@ Requires-Dist: build>=1.4.2
|
|
|
17
17
|
Requires-Dist: wheel>=0.46.3
|
|
18
18
|
Requires-Dist: mformat-ext>=0.6
|
|
19
19
|
Requires-Dist: openpyxl>=3.1.5
|
|
20
|
-
Requires-Dist: types-openpyxl>=3.1.5.
|
|
21
|
-
Requires-Dist: odfdo>=3.22.
|
|
20
|
+
Requires-Dist: types-openpyxl>=3.1.5.20260402
|
|
21
|
+
Requires-Dist: odfdo>=3.22.4
|
|
22
22
|
Requires-Dist: xlsxwriter>=3.2.9
|
|
23
23
|
Requires-Dist: pylightxl>=1.61
|
|
24
24
|
Dynamic: author
|
|
@@ -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,14 +139,16 @@ a better start by reading the examples.
|
|
|
138
139
|
|
|
139
140
|
| Version | Date | Python version | Comment |
|
|
140
141
|
|---------|-------------|----------------|-----------------------------|
|
|
141
|
-
| 0.
|
|
142
|
+
| 0.5 | 2026 Apr 07 | 3.12 or newer | Added table border styles |
|
|
143
|
+
| 0.4 | 2026 Apr 06 | 3.12 or newer | Fix .xlsx and .ods syntax |
|
|
144
|
+
| 0.3 | 2026 Apr 01 | 3.12 or newer | Improved API, more backends |
|
|
142
145
|
| 0.2 | 2026 Mar 28 | 3.12 or newer | More features, API changes |
|
|
143
146
|
| 0.1 | 2026 Mar 23 | 3.12 or newer | First released version |
|
|
144
147
|
|
|
145
148
|
## Test summary
|
|
146
149
|
|
|
147
|
-
- Test result:
|
|
150
|
+
- Test result: 1173 passed in 18s
|
|
148
151
|
- No flake8 warnings.
|
|
149
152
|
- No mypy errors found.
|
|
150
|
-
- Built version(s): 0.
|
|
153
|
+
- Built version(s): 0.5
|
|
151
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,14 +111,16 @@ a better start by reading the examples.
|
|
|
110
111
|
|
|
111
112
|
| Version | Date | Python version | Comment |
|
|
112
113
|
|---------|-------------|----------------|-----------------------------|
|
|
113
|
-
| 0.
|
|
114
|
+
| 0.5 | 2026 Apr 07 | 3.12 or newer | Added table border styles |
|
|
115
|
+
| 0.4 | 2026 Apr 06 | 3.12 or newer | Fix .xlsx and .ods syntax |
|
|
116
|
+
| 0.3 | 2026 Apr 01 | 3.12 or newer | Improved API, more backends |
|
|
114
117
|
| 0.2 | 2026 Mar 28 | 3.12 or newer | More features, API changes |
|
|
115
118
|
| 0.1 | 2026 Mar 23 | 3.12 or newer | First released version |
|
|
116
119
|
|
|
117
120
|
## Test summary
|
|
118
121
|
|
|
119
|
-
- Test result:
|
|
122
|
+
- Test result: 1173 passed in 18s
|
|
120
123
|
- No flake8 warnings.
|
|
121
124
|
- No mypy errors found.
|
|
122
|
-
- Built version(s): 0.
|
|
125
|
+
- Built version(s): 0.5
|
|
123
126
|
- Build and test using Python 3.14.3
|
|
@@ -5,7 +5,7 @@ from setuptools import setup
|
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
7
|
name='tableio',
|
|
8
|
-
version='0.
|
|
8
|
+
version='0.5',
|
|
9
9
|
description='Uniform way to write table data to and read from different ' \
|
|
10
10
|
'file formats',
|
|
11
11
|
author='Tom Björkholm',
|
|
@@ -21,8 +21,8 @@ setup(
|
|
|
21
21
|
'wheel >= 0.46.3',
|
|
22
22
|
'mformat-ext >= 0.6',
|
|
23
23
|
'openpyxl >= 3.1.5',
|
|
24
|
-
'types-openpyxl >= 3.1.5.
|
|
25
|
-
'odfdo >= 3.22.
|
|
24
|
+
'types-openpyxl >= 3.1.5.20260402',
|
|
25
|
+
'odfdo >= 3.22.4',
|
|
26
26
|
'xlsxwriter >= 3.2.9',
|
|
27
27
|
'pylightxl >= 1.61'
|
|
28
28
|
]
|
|
@@ -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,80 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
"""Helpers for rewriting spreadsheet ZIP archives safely.
|
|
3
|
+
|
|
4
|
+
Spreadsheet writers first save library output to a temporary archive.
|
|
5
|
+
These helpers then build a rewritten copy in a second temporary archive
|
|
6
|
+
and replace the first archive only after both ZIP files are closed.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Copyright (c) 2026 Tom Björkholm
|
|
10
|
+
# MIT License
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from tempfile import NamedTemporaryFile
|
|
14
|
+
from typing import Callable, Optional
|
|
15
|
+
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def temporary_output_path(source_path: Path, suffix: str) -> Path:
|
|
19
|
+
"""Return one missing temporary path next to ``source_path``.
|
|
20
|
+
|
|
21
|
+
The temporary file is created in the same directory as
|
|
22
|
+
``source_path`` so a later ``Path.replace()`` stays on the same
|
|
23
|
+
filesystem. The file name uses ``suffix`` and the returned path does
|
|
24
|
+
not exist when this function returns.
|
|
25
|
+
"""
|
|
26
|
+
with NamedTemporaryFile(delete=False, dir=source_path.parent,
|
|
27
|
+
suffix=suffix) as temp_file:
|
|
28
|
+
temp_path = Path(temp_file.name)
|
|
29
|
+
temp_path.unlink()
|
|
30
|
+
return temp_path
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def rewrite_zip_archive(
|
|
34
|
+
archive_path: Path,
|
|
35
|
+
rewrite_entry: Callable[[ZipInfo, bytes], Optional[bytes]],
|
|
36
|
+
extra_entries: Optional[dict[str, bytes]] = None) -> None:
|
|
37
|
+
"""Rewrite one ZIP archive by copying it into a new archive.
|
|
38
|
+
|
|
39
|
+
The original archive at ``archive_path`` is read entry by entry and
|
|
40
|
+
a rewritten archive is written to a sibling temporary file. The
|
|
41
|
+
callback receives each original ``ZipInfo`` together with its bytes.
|
|
42
|
+
Returning ``None`` drops the entry. Any mapping passed in
|
|
43
|
+
``extra_entries`` is appended after copied entries, except for names
|
|
44
|
+
that were already written.
|
|
45
|
+
|
|
46
|
+
The original archive is replaced only after both ZIP files have been
|
|
47
|
+
closed. This avoids replacing an archive while it is still open,
|
|
48
|
+
which is a safer pattern on Windows as well as on Unix-like systems.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
archive_path: Path to the archive to rewrite in place.
|
|
52
|
+
rewrite_entry: Callback invoked once for each original ZIP entry.
|
|
53
|
+
The callback receives the original ``ZipInfo`` together with
|
|
54
|
+
the entry bytes. It returns replacement bytes for the output
|
|
55
|
+
archive, or ``None`` to drop the entry completely.
|
|
56
|
+
extra_entries: Optional mapping of extra archive members to add
|
|
57
|
+
after copying rewritten entries. Names that were already
|
|
58
|
+
written are left unchanged.
|
|
59
|
+
"""
|
|
60
|
+
temp_path = temporary_output_path(archive_path, archive_path.suffix)
|
|
61
|
+
try:
|
|
62
|
+
with ZipFile(archive_path, 'r') as source_zip, \
|
|
63
|
+
ZipFile(temp_path, 'w',
|
|
64
|
+
compression=ZIP_DEFLATED) as target_zip:
|
|
65
|
+
written_names: set[str] = set()
|
|
66
|
+
for item in source_zip.infolist():
|
|
67
|
+
data = rewrite_entry(item, source_zip.read(item.filename))
|
|
68
|
+
if data is None:
|
|
69
|
+
continue
|
|
70
|
+
target_zip.writestr(item, data)
|
|
71
|
+
written_names.add(item.filename)
|
|
72
|
+
if extra_entries is not None:
|
|
73
|
+
for entry_name, entry_data in extra_entries.items():
|
|
74
|
+
if entry_name in written_names:
|
|
75
|
+
continue
|
|
76
|
+
target_zip.writestr(entry_name, entry_data)
|
|
77
|
+
temp_path.replace(archive_path)
|
|
78
|
+
finally:
|
|
79
|
+
if temp_path.exists():
|
|
80
|
+
temp_path.unlink()
|
|
@@ -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)
|
|
@@ -274,6 +274,17 @@ class FactoryFormatInfo:
|
|
|
274
274
|
raise TableIOFactoryNoCapabilityMatch(msg)
|
|
275
275
|
return ret
|
|
276
276
|
|
|
277
|
+
def correct_implementation_name(self, implementation_name: str) -> str:
|
|
278
|
+
"""Correct the implementation name to the correct case."""
|
|
279
|
+
if implementation_name in self._registry:
|
|
280
|
+
return implementation_name
|
|
281
|
+
if implementation_name.lower() in self._lower2correct:
|
|
282
|
+
return self._lower2correct[implementation_name.lower()]
|
|
283
|
+
raise TableIOFactoryNoSuchError(
|
|
284
|
+
f'Implementation "{implementation_name}" is not registered. ' +
|
|
285
|
+
'Available implementations: ' +
|
|
286
|
+
f'{", ".join(list(self._registry.keys()))}')
|
|
287
|
+
|
|
277
288
|
|
|
278
289
|
class TableIOFactory:
|
|
279
290
|
"""Factory class for creating instances of TableIO subclasses.
|
|
@@ -416,21 +427,16 @@ class TableIOFactory:
|
|
|
416
427
|
best_matches = format_info.best_match_names(
|
|
417
428
|
capabilities=capabilities, empty_is_ok=False)
|
|
418
429
|
if implementation is not None:
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
raise TableIOFactoryNoSuchError(
|
|
422
|
-
f'Implementation "{implementation}" is not registered. ' +
|
|
423
|
-
'Available implementations: ' +
|
|
424
|
-
f'{", ".join(list(fikeys))}'
|
|
425
|
-
)
|
|
430
|
+
corrected_implementation = \
|
|
431
|
+
format_info.correct_implementation_name(implementation)
|
|
426
432
|
bmatches_impls = [impl.implementation
|
|
427
433
|
for impl in best_matches.combined()]
|
|
428
|
-
if
|
|
434
|
+
if corrected_implementation not in bmatches_impls:
|
|
429
435
|
msg3 = f'Implementation "{implementation}" '
|
|
430
436
|
msg3 += 'does not match the capabilities. Available matches: '
|
|
431
437
|
msg3 += f'{", ".join(bmatches_impls)}'
|
|
432
438
|
raise TableIOFactoryNoCapabilityMatch(msg3)
|
|
433
|
-
chosen_impl =
|
|
439
|
+
chosen_impl = corrected_implementation
|
|
434
440
|
else:
|
|
435
441
|
chosen_impl = best_matches.combined()[0].implementation
|
|
436
442
|
format_class = format_info._registry[chosen_impl] # pylint: disable=protected-access # noqa: E501
|