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.
Files changed (36) hide show
  1. {tableio-0.3/tableio.egg-info → tableio-0.5}/PKG-INFO +9 -6
  2. {tableio-0.3 → tableio-0.5}/README.md +2 -2
  3. {tableio-0.3 → tableio-0.5}/README_pypi.md +6 -3
  4. {tableio-0.3 → tableio-0.5}/pyproject.toml +1 -1
  5. {tableio-0.3 → tableio-0.5}/setup.py +3 -3
  6. {tableio-0.3 → tableio-0.5}/src/tableio/__init__.py +3 -1
  7. tableio-0.5/src/tableio/_archive_rewrite.py +80 -0
  8. tableio-0.5/src/tableio/border_helper.py +196 -0
  9. {tableio-0.3 → tableio-0.5}/src/tableio/capability.py +5 -1
  10. {tableio-0.3 → tableio-0.5}/src/tableio/factory.py +15 -9
  11. {tableio-0.3 → tableio-0.5}/src/tableio/tableio.py +68 -37
  12. {tableio-0.3 → tableio-0.5}/src/tableio/tableio_csv.py +2 -1
  13. {tableio-0.3 → tableio-0.5}/src/tableio/tableio_excel_openpyxl.py +90 -2
  14. {tableio-0.3 → tableio-0.5}/src/tableio/tableio_excel_pylightxl.py +84 -61
  15. {tableio-0.3 → tableio-0.5}/src/tableio/tableio_excel_xlsxwriter.py +64 -15
  16. {tableio-0.3 → tableio-0.5}/src/tableio/tableio_mformatbased.py +2 -1
  17. {tableio-0.3 → tableio-0.5}/src/tableio/tableio_ods_odfdo.py +190 -15
  18. {tableio-0.3 → tableio-0.5}/src/tableio/tableio_spreadsheetbased.py +42 -18
  19. tableio-0.5/src/tableio/tableio_types.py +132 -0
  20. {tableio-0.3 → tableio-0.5/tableio.egg-info}/PKG-INFO +9 -6
  21. {tableio-0.3 → tableio-0.5}/tableio.egg-info/SOURCES.txt +2 -0
  22. {tableio-0.3 → tableio-0.5}/tableio.egg-info/requires.txt +2 -2
  23. tableio-0.3/src/tableio/tableio_types.py +0 -61
  24. {tableio-0.3 → tableio-0.5}/LICENSE.txt +0 -0
  25. {tableio-0.3 → tableio-0.5}/setup.cfg +0 -0
  26. {tableio-0.3 → tableio-0.5}/src/tableio/color.py +0 -0
  27. {tableio-0.3 → tableio-0.5}/src/tableio/optional_args.py +0 -0
  28. {tableio-0.3 → tableio-0.5}/src/tableio/py.typed +0 -0
  29. {tableio-0.3 → tableio-0.5}/src/tableio/reg_pkg_formats.py +0 -0
  30. {tableio-0.3 → tableio-0.5}/src/tableio/tableio_excelbased.py +0 -0
  31. {tableio-0.3 → tableio-0.5}/src/tableio/tableio_mformat.py +0 -0
  32. {tableio-0.3 → tableio-0.5}/src/tableio/tableio_textbased.py +0 -0
  33. {tableio-0.3 → tableio-0.5}/src/tableio/value_type.py +0 -0
  34. {tableio-0.3 → tableio-0.5}/src/tableio/valueconversion.py +0 -0
  35. {tableio-0.3 → tableio-0.5}/tableio.egg-info/dependency_links.txt +0 -0
  36. {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
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.20260322
21
- Requires-Dist: odfdo>=3.22.3
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.3 | 2023 Apr 01 | 3.12 or newer | Improved API, more backends |
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: 1010 passed in 14s
150
+ - Test result: 1173 passed in 18s
148
151
  - No flake8 warnings.
149
152
  - No mypy errors found.
150
- - Built version(s): 0.3
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: 1010 passed in 14s
96
+ - Test result: 1173 passed in 18s
97
97
  - No flake8 warnings.
98
98
  - No mypy errors found.
99
- - Built version(s): 0.3
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.3 | 2023 Apr 01 | 3.12 or newer | Improved API, more backends |
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: 1010 passed in 14s
122
+ - Test result: 1173 passed in 18s
120
123
  - No flake8 warnings.
121
124
  - No mypy errors found.
122
- - Built version(s): 0.3
125
+ - Built version(s): 0.5
123
126
  - Build and test using Python 3.14.3
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tableio"
7
- version = "0.3"
7
+ version = "0.5"
8
8
  authors = [
9
9
  { name="Tom Björkholm", email="klausuler_linnet0q@icloud.com" },
10
10
  ]
@@ -5,7 +5,7 @@ from setuptools import setup
5
5
 
6
6
  setup(
7
7
  name='tableio',
8
- version='0.3',
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.20260322',
25
- 'odfdo >= 3.22.3',
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 Box, FileAccess, Position, TableIO
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
- fikeys = list(format_info._registry.keys()) # pylint: disable=protected-access # noqa: E501
420
- if implementation not in fikeys:
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 implementation not in bmatches_impls:
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 = implementation
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