tableio 0.6__tar.gz → 0.7__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.6/tableio.egg-info → tableio-0.7}/PKG-INFO +8 -18
- {tableio-0.6 → tableio-0.7}/README.md +3 -3
- {tableio-0.6 → tableio-0.7}/README_pypi.md +3 -13
- {tableio-0.6 → tableio-0.7}/pyproject.toml +1 -1
- {tableio-0.6 → tableio-0.7}/setup.py +5 -5
- {tableio-0.6 → tableio-0.7}/src/tableio/factory.py +150 -95
- {tableio-0.6 → tableio-0.7}/src/tableio/tableio_excel_openpyxl.py +173 -4
- {tableio-0.6 → tableio-0.7/tableio.egg-info}/PKG-INFO +8 -18
- {tableio-0.6 → tableio-0.7}/tableio.egg-info/requires.txt +4 -4
- {tableio-0.6 → tableio-0.7}/LICENSE.txt +0 -0
- {tableio-0.6 → tableio-0.7}/setup.cfg +0 -0
- {tableio-0.6 → tableio-0.7}/src/tableio/__init__.py +0 -0
- {tableio-0.6 → tableio-0.7}/src/tableio/_archive_rewrite.py +0 -0
- {tableio-0.6 → tableio-0.7}/src/tableio/border_helper.py +0 -0
- {tableio-0.6 → tableio-0.7}/src/tableio/capability.py +0 -0
- {tableio-0.6 → tableio-0.7}/src/tableio/color.py +0 -0
- {tableio-0.6 → tableio-0.7}/src/tableio/optional_args.py +0 -0
- {tableio-0.6 → tableio-0.7}/src/tableio/py.typed +0 -0
- {tableio-0.6 → tableio-0.7}/src/tableio/reg_pkg_formats.py +0 -0
- {tableio-0.6 → tableio-0.7}/src/tableio/tableio.py +0 -0
- {tableio-0.6 → tableio-0.7}/src/tableio/tableio_csv.py +0 -0
- {tableio-0.6 → tableio-0.7}/src/tableio/tableio_excel_pylightxl.py +0 -0
- {tableio-0.6 → tableio-0.7}/src/tableio/tableio_excel_xlsxwriter.py +0 -0
- {tableio-0.6 → tableio-0.7}/src/tableio/tableio_excelbased.py +0 -0
- {tableio-0.6 → tableio-0.7}/src/tableio/tableio_mformat.py +0 -0
- {tableio-0.6 → tableio-0.7}/src/tableio/tableio_mformatbased.py +0 -0
- {tableio-0.6 → tableio-0.7}/src/tableio/tableio_ods_odfdo.py +0 -0
- {tableio-0.6 → tableio-0.7}/src/tableio/tableio_spreadsheetbased.py +0 -0
- {tableio-0.6 → tableio-0.7}/src/tableio/tableio_textbased.py +0 -0
- {tableio-0.6 → tableio-0.7}/src/tableio/tableio_types.py +0 -0
- {tableio-0.6 → tableio-0.7}/src/tableio/value_type.py +0 -0
- {tableio-0.6 → tableio-0.7}/src/tableio/valueconversion.py +0 -0
- {tableio-0.6 → tableio-0.7}/tableio.egg-info/SOURCES.txt +0 -0
- {tableio-0.6 → tableio-0.7}/tableio.egg-info/dependency_links.txt +0 -0
- {tableio-0.6 → tableio-0.7}/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.7
|
|
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>
|
|
@@ -11,14 +11,14 @@ Classifier: Operating System :: OS Independent
|
|
|
11
11
|
Requires-Python: >=3.12
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE.txt
|
|
14
|
-
Requires-Dist: pip>=26.
|
|
14
|
+
Requires-Dist: pip>=26.1.1
|
|
15
15
|
Requires-Dist: setuptools>=82.0.1
|
|
16
|
-
Requires-Dist: build>=1.
|
|
17
|
-
Requires-Dist: wheel>=0.
|
|
16
|
+
Requires-Dist: build>=1.5.0
|
|
17
|
+
Requires-Dist: wheel>=0.47.0
|
|
18
18
|
Requires-Dist: mformat-ext>=0.6
|
|
19
19
|
Requires-Dist: openpyxl>=3.1.5
|
|
20
20
|
Requires-Dist: types-openpyxl>=3.1.5.20260402
|
|
21
|
-
Requires-Dist: odfdo>=3.22.
|
|
21
|
+
Requires-Dist: odfdo>=3.22.7
|
|
22
22
|
Requires-Dist: xlsxwriter>=3.2.9
|
|
23
23
|
Requires-Dist: pylightxl>=1.61
|
|
24
24
|
Dynamic: author
|
|
@@ -135,20 +135,10 @@ readers/writers to the factory.
|
|
|
135
135
|
Even though the API documentation exists, most users and programmers probably get
|
|
136
136
|
a better start by reading the examples.
|
|
137
137
|
|
|
138
|
-
## Version history
|
|
139
|
-
|
|
140
|
-
| Version | Date | Python version | Comment |
|
|
141
|
-
|---------|-------------|----------------|-----------------------------|
|
|
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 |
|
|
145
|
-
| 0.2 | 2026 Mar 28 | 3.12 or newer | More features, API changes |
|
|
146
|
-
| 0.1 | 2026 Mar 23 | 3.12 or newer | First released version |
|
|
147
|
-
|
|
148
138
|
## Test summary
|
|
149
139
|
|
|
150
|
-
- Test result:
|
|
140
|
+
- Test result: 1186 passed in 18s
|
|
151
141
|
- No flake8 warnings.
|
|
152
142
|
- No mypy errors found.
|
|
153
|
-
- Built version(s): 0.
|
|
154
|
-
- Build and test using Python 3.14.
|
|
143
|
+
- Built version(s): 0.7
|
|
144
|
+
- Build and test using Python 3.14.4
|
|
@@ -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: 1186 passed in 18s
|
|
97
97
|
- No flake8 warnings.
|
|
98
98
|
- No mypy errors found.
|
|
99
|
-
- Built version(s): 0.
|
|
100
|
-
- Build and test using Python 3.14.
|
|
99
|
+
- Built version(s): 0.7
|
|
100
|
+
- Build and test using Python 3.14.4
|
|
@@ -107,20 +107,10 @@ readers/writers to the factory.
|
|
|
107
107
|
Even though the API documentation exists, most users and programmers probably get
|
|
108
108
|
a better start by reading the examples.
|
|
109
109
|
|
|
110
|
-
## Version history
|
|
111
|
-
|
|
112
|
-
| Version | Date | Python version | Comment |
|
|
113
|
-
|---------|-------------|----------------|-----------------------------|
|
|
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 |
|
|
117
|
-
| 0.2 | 2026 Mar 28 | 3.12 or newer | More features, API changes |
|
|
118
|
-
| 0.1 | 2026 Mar 23 | 3.12 or newer | First released version |
|
|
119
|
-
|
|
120
110
|
## Test summary
|
|
121
111
|
|
|
122
|
-
- Test result:
|
|
112
|
+
- Test result: 1186 passed in 18s
|
|
123
113
|
- No flake8 warnings.
|
|
124
114
|
- No mypy errors found.
|
|
125
|
-
- Built version(s): 0.
|
|
126
|
-
- Build and test using Python 3.14.
|
|
115
|
+
- Built version(s): 0.7
|
|
116
|
+
- Build and test using Python 3.14.4
|
|
@@ -5,7 +5,7 @@ from setuptools import setup
|
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
7
|
name='tableio',
|
|
8
|
-
version='0.
|
|
8
|
+
version='0.7',
|
|
9
9
|
description='Uniform way to write table data to and read from different ' \
|
|
10
10
|
'file formats',
|
|
11
11
|
author='Tom Björkholm',
|
|
@@ -15,14 +15,14 @@ setup(
|
|
|
15
15
|
package_dir={'tableio': 'src/tableio'},
|
|
16
16
|
package_data={'tableio': ['src/py.typed']},
|
|
17
17
|
install_requires=[ # pylint: disable=duplicate-code
|
|
18
|
-
'pip >= 26.
|
|
18
|
+
'pip >= 26.1.1',
|
|
19
19
|
'setuptools >= 82.0.1',
|
|
20
|
-
'build >= 1.
|
|
21
|
-
'wheel >= 0.
|
|
20
|
+
'build >= 1.5.0',
|
|
21
|
+
'wheel >= 0.47.0',
|
|
22
22
|
'mformat-ext >= 0.6',
|
|
23
23
|
'openpyxl >= 3.1.5',
|
|
24
24
|
'types-openpyxl >= 3.1.5.20260402',
|
|
25
|
-
'odfdo >= 3.22.
|
|
25
|
+
'odfdo >= 3.22.7',
|
|
26
26
|
'xlsxwriter >= 3.2.9',
|
|
27
27
|
'pylightxl >= 1.61'
|
|
28
28
|
]
|
|
@@ -285,6 +285,11 @@ class FactoryFormatInfo:
|
|
|
285
285
|
'Available implementations: ' +
|
|
286
286
|
f'{", ".join(list(self._registry.keys()))}')
|
|
287
287
|
|
|
288
|
+
def get_usage(self, implementation_name: str) -> Descriptor:
|
|
289
|
+
"""Get usage information for one implementation."""
|
|
290
|
+
return self._usage[
|
|
291
|
+
self.correct_implementation_name(implementation_name)]
|
|
292
|
+
|
|
288
293
|
|
|
289
294
|
class TableIOFactory:
|
|
290
295
|
"""Factory class for creating instances of TableIO subclasses.
|
|
@@ -411,34 +416,10 @@ class TableIOFactory:
|
|
|
411
416
|
matched to any implementation.
|
|
412
417
|
"""
|
|
413
418
|
_check_capabilities_for_file_access(file_access, capabilities)
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
correct_name = self._lower2correct[format_name.lower()]
|
|
419
|
-
else:
|
|
420
|
-
raise TableIOFactoryNoSuchError(
|
|
421
|
-
f'Format "{format_name}" is not registered. ' +
|
|
422
|
-
'Available formats: ' +
|
|
423
|
-
f'{", ".join(sorted(list(self._formats.keys())))}'
|
|
424
|
-
)
|
|
425
|
-
assert correct_name is not None
|
|
426
|
-
format_info: FactoryFormatInfo = self._formats[correct_name]
|
|
427
|
-
best_matches = format_info.best_match_names(
|
|
428
|
-
capabilities=capabilities, empty_is_ok=False)
|
|
429
|
-
if implementation is not None:
|
|
430
|
-
corrected_implementation = \
|
|
431
|
-
format_info.correct_implementation_name(implementation)
|
|
432
|
-
bmatches_impls = [impl.implementation
|
|
433
|
-
for impl in best_matches.combined()]
|
|
434
|
-
if corrected_implementation not in bmatches_impls:
|
|
435
|
-
msg3 = f'Implementation "{implementation}" '
|
|
436
|
-
msg3 += 'does not match the capabilities. Available matches: '
|
|
437
|
-
msg3 += f'{", ".join(bmatches_impls)}'
|
|
438
|
-
raise TableIOFactoryNoCapabilityMatch(msg3)
|
|
439
|
-
chosen_impl = corrected_implementation
|
|
440
|
-
else:
|
|
441
|
-
chosen_impl = best_matches.combined()[0].implementation
|
|
419
|
+
format_info = self._format_info(format_name)
|
|
420
|
+
chosen_impl = self._select_implementation_name(
|
|
421
|
+
format_info=format_info, implementation=implementation,
|
|
422
|
+
capabilities=capabilities)
|
|
442
423
|
format_class = format_info._registry[chosen_impl] # pylint: disable=protected-access # noqa: E501
|
|
443
424
|
if args is None:
|
|
444
425
|
return format_class(file_name=file_name, file_access=file_access)
|
|
@@ -447,9 +428,47 @@ class TableIOFactory:
|
|
|
447
428
|
# mypy cannot see which TableIO subclass is being created, so it
|
|
448
429
|
# cannot know which arguments are valid.
|
|
449
430
|
|
|
431
|
+
def _correct_format_name(self, format_name: str) -> str:
|
|
432
|
+
"""Correct a registered format name to its stored case."""
|
|
433
|
+
if format_name in self._formats:
|
|
434
|
+
return format_name
|
|
435
|
+
if format_name.lower() in self._lower2correct:
|
|
436
|
+
return self._lower2correct[format_name.lower()]
|
|
437
|
+
msg = f'Format "{format_name}" is not registered. '
|
|
438
|
+
msg += 'Available formats: '
|
|
439
|
+
msg += f'{", ".join(sorted(list(self._formats.keys())))}'
|
|
440
|
+
raise TableIOFactoryNoSuchError(msg)
|
|
441
|
+
|
|
442
|
+
def _format_info(self, format_name: str) -> FactoryFormatInfo:
|
|
443
|
+
"""Get registered format information by case-insensitive name."""
|
|
444
|
+
return self._formats[self._correct_format_name(format_name)]
|
|
445
|
+
|
|
446
|
+
@staticmethod
|
|
447
|
+
def _select_implementation_name(
|
|
448
|
+
format_info: FactoryFormatInfo,
|
|
449
|
+
implementation: Optional[str],
|
|
450
|
+
capabilities: Optional[Capabilities]) -> str:
|
|
451
|
+
"""Select the implementation name matching the request."""
|
|
452
|
+
best_matches = format_info.best_match_names(
|
|
453
|
+
capabilities=capabilities, empty_is_ok=False)
|
|
454
|
+
if implementation is None:
|
|
455
|
+
return best_matches.combined()[0].implementation
|
|
456
|
+
corrected_implementation = \
|
|
457
|
+
format_info.correct_implementation_name(implementation)
|
|
458
|
+
matching_names = [
|
|
459
|
+
impl.implementation for impl in best_matches.combined()]
|
|
460
|
+
if corrected_implementation in matching_names:
|
|
461
|
+
return corrected_implementation
|
|
462
|
+
msg = f'Implementation "{implementation}" '
|
|
463
|
+
msg += 'does not match the capabilities. Available matches: '
|
|
464
|
+
msg += f'{", ".join(matching_names)}'
|
|
465
|
+
raise TableIOFactoryNoCapabilityMatch(msg)
|
|
466
|
+
|
|
450
467
|
@staticmethod
|
|
451
468
|
def filter_args(args: OptionalArgs, format_name: str,
|
|
452
|
-
implementation: str
|
|
469
|
+
implementation: Optional[str],
|
|
470
|
+
capabilities: Optional[Capabilities] = None
|
|
471
|
+
) -> OptionalArgs:
|
|
453
472
|
"""Filter the arguments for a registered format.
|
|
454
473
|
|
|
455
474
|
Filter the arguments to only include the arguments that are valid for
|
|
@@ -462,23 +481,37 @@ class TableIOFactory:
|
|
|
462
481
|
args: The arguments to filter.
|
|
463
482
|
format_name: The name identifier of the format class to filter the
|
|
464
483
|
arguments for.
|
|
465
|
-
implementation: The implementation name to use.
|
|
484
|
+
implementation: The implementation name to use. If implementation
|
|
485
|
+
is specified as None, filtering is done for the
|
|
486
|
+
implementation create() would use with a None value
|
|
487
|
+
for the implementation parameter.
|
|
488
|
+
capabilities: The capabilities to match. This is used to determine
|
|
489
|
+
the implementation to use if implementation is None.
|
|
466
490
|
Returns:
|
|
467
491
|
The filtered arguments.
|
|
468
492
|
Raises:
|
|
469
493
|
TableIOFactoryNoSuchError: If the format_name is not registered
|
|
470
494
|
or the implementation name is not
|
|
471
495
|
registered.
|
|
496
|
+
TableIOFactoryNoCapabilityMatch: If the capabilities cannot be
|
|
497
|
+
matched to the selected
|
|
498
|
+
implementation.
|
|
472
499
|
"""
|
|
473
500
|
factory = TableIOFactory.i_get_factory()
|
|
474
501
|
return factory.i_filter_args(args=args, format_name=format_name,
|
|
475
|
-
implementation=implementation
|
|
502
|
+
implementation=implementation,
|
|
503
|
+
capabilities=capabilities)
|
|
476
504
|
|
|
477
505
|
def i_filter_args(self, args: OptionalArgs, format_name: str,
|
|
478
|
-
implementation: str
|
|
506
|
+
implementation: Optional[str],
|
|
507
|
+
capabilities: Optional[Capabilities] = None
|
|
508
|
+
) -> OptionalArgs:
|
|
479
509
|
"""Internally filter the arguments for a registered format."""
|
|
480
|
-
|
|
481
|
-
|
|
510
|
+
format_info = self._format_info(format_name)
|
|
511
|
+
chosen_impl = self._select_implementation_name(
|
|
512
|
+
format_info=format_info, implementation=implementation,
|
|
513
|
+
capabilities=capabilities)
|
|
514
|
+
format_usage = format_info.get_usage(chosen_impl)
|
|
482
515
|
if args is None:
|
|
483
516
|
return None
|
|
484
517
|
assert args is not None
|
|
@@ -557,12 +590,13 @@ class TableIOFactory:
|
|
|
557
590
|
return ret
|
|
558
591
|
|
|
559
592
|
@staticmethod
|
|
560
|
-
def get_registered_implementations(
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
593
|
+
def get_registered_implementations( # pylint: disable=too-many-arguments,too-many-positional-arguments # noqa: E501
|
|
594
|
+
format_name: Optional[str] = None,
|
|
595
|
+
lower: bool = False,
|
|
596
|
+
upper: bool = False,
|
|
597
|
+
capabilities: Optional[Capabilities] = None,
|
|
598
|
+
empty_is_ok: bool = False,
|
|
599
|
+
alphabetical: bool = True) -> list[str]:
|
|
566
600
|
"""Get a list of all registered implementation names.
|
|
567
601
|
|
|
568
602
|
The list includes all registered implementation names optionally
|
|
@@ -590,6 +624,12 @@ class TableIOFactory:
|
|
|
590
624
|
If False, a TableIOFactoryNoCapabilityMatch
|
|
591
625
|
error is raised if no implementations match the
|
|
592
626
|
capabilities.
|
|
627
|
+
alphabetical: If True, the implementations are returned in
|
|
628
|
+
alphabetical order. If False, the implementations
|
|
629
|
+
are returned with the strict matches first and the
|
|
630
|
+
nonstrict matches last. Both strict and nonstrict
|
|
631
|
+
matches are sorted by priority, from highest to
|
|
632
|
+
lowest.
|
|
593
633
|
Returns:
|
|
594
634
|
A list of registered implementation name strings optionally
|
|
595
635
|
filtered to only include implementations that offer the
|
|
@@ -602,40 +642,26 @@ class TableIOFactory:
|
|
|
602
642
|
factory = TableIOFactory.i_get_factory()
|
|
603
643
|
return factory.i_get_registered_implementations(
|
|
604
644
|
format_name=format_name, lower=lower, upper=upper,
|
|
605
|
-
capabilities=capabilities, empty_is_ok=empty_is_ok
|
|
645
|
+
capabilities=capabilities, empty_is_ok=empty_is_ok,
|
|
646
|
+
alphabetical=alphabetical)
|
|
606
647
|
|
|
607
|
-
|
|
648
|
+
# pylint: disable-next=too-many-arguments,too-many-positional-arguments
|
|
649
|
+
def i_get_registered_implementations(
|
|
608
650
|
self, format_name: Optional[str] = None,
|
|
609
651
|
lower: bool = False, upper: bool = False,
|
|
610
652
|
capabilities: Optional[Capabilities] = None,
|
|
611
|
-
empty_is_ok: bool = False
|
|
653
|
+
empty_is_ok: bool = False,
|
|
654
|
+
alphabetical: bool = True) -> list[str]:
|
|
612
655
|
"""Internally get a list of registered implementation names."""
|
|
613
656
|
fmtkeys: list[str] = list(self._formats.keys())
|
|
614
657
|
if format_name is not None:
|
|
615
|
-
fmtkeys = [format_name]
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
format_info: FactoryFormatInfo = self._formats[fmtkey]
|
|
621
|
-
implmatches.append(
|
|
622
|
-
format_info.best_match_names(capabilities=capabilities,
|
|
623
|
-
empty_is_ok=True))
|
|
624
|
-
implkeys = {
|
|
625
|
-
impl.implementation for impl in
|
|
626
|
-
BestMatch.add_list(implmatches).combined()
|
|
627
|
-
} # several format names may have the same implementation name
|
|
628
|
-
if not implkeys and not empty_is_ok:
|
|
629
|
-
msg = 'No implementations match the capabilities.'
|
|
630
|
-
raise TableIOFactoryNoCapabilityMatch(msg)
|
|
631
|
-
else:
|
|
632
|
-
implkeys = {
|
|
633
|
-
implkey
|
|
634
|
-
for fmtkey in fmtkeys
|
|
635
|
-
for implkey in self._formats[fmtkey]._registry.keys() # pylint: disable=protected-access # noqa: E501
|
|
636
|
-
}
|
|
658
|
+
fmtkeys = [self._correct_format_name(format_name)]
|
|
659
|
+
best_match = self._implementation_matches(
|
|
660
|
+
format_names=fmtkeys, capabilities=capabilities,
|
|
661
|
+
empty_is_ok=empty_is_ok)
|
|
662
|
+
implkeys = self._implementation_names(best_match, alphabetical)
|
|
637
663
|
ret: list[str] = []
|
|
638
|
-
for implkey in
|
|
664
|
+
for implkey in implkeys:
|
|
639
665
|
ret.append(implkey)
|
|
640
666
|
if lower and implkey != implkey.lower():
|
|
641
667
|
ret.append(implkey.lower())
|
|
@@ -643,6 +669,37 @@ class TableIOFactory:
|
|
|
643
669
|
ret.append(implkey.upper())
|
|
644
670
|
return ret
|
|
645
671
|
|
|
672
|
+
def _implementation_matches(
|
|
673
|
+
self, format_names: list[str],
|
|
674
|
+
capabilities: Optional[Capabilities],
|
|
675
|
+
empty_is_ok: bool) -> BestMatch:
|
|
676
|
+
"""Get matching implementations for the requested format names."""
|
|
677
|
+
implmatches = [
|
|
678
|
+
self._formats[format_name].best_match_names(
|
|
679
|
+
capabilities=capabilities, empty_is_ok=True)
|
|
680
|
+
for format_name in format_names
|
|
681
|
+
]
|
|
682
|
+
best_match = BestMatch.add_list(implmatches)
|
|
683
|
+
if len(best_match) == 0 and not empty_is_ok:
|
|
684
|
+
msg = 'No implementations match the capabilities.'
|
|
685
|
+
raise TableIOFactoryNoCapabilityMatch(msg)
|
|
686
|
+
return best_match
|
|
687
|
+
|
|
688
|
+
@staticmethod
|
|
689
|
+
def _implementation_names(best_match: BestMatch,
|
|
690
|
+
alphabetical: bool) -> list[str]:
|
|
691
|
+
"""Get implementation names from best matches without duplicates."""
|
|
692
|
+
names: list[str] = []
|
|
693
|
+
seen: set[str] = set()
|
|
694
|
+
for impl in best_match.combined():
|
|
695
|
+
if impl.implementation in seen:
|
|
696
|
+
continue
|
|
697
|
+
seen.add(impl.implementation)
|
|
698
|
+
names.append(impl.implementation)
|
|
699
|
+
if alphabetical:
|
|
700
|
+
return sorted(names)
|
|
701
|
+
return names
|
|
702
|
+
|
|
646
703
|
@staticmethod
|
|
647
704
|
def get_usage(format_name: str, implementation: str) -> Descriptor:
|
|
648
705
|
"""Get the usage information for a registered format.
|
|
@@ -664,28 +721,7 @@ class TableIOFactory:
|
|
|
664
721
|
|
|
665
722
|
def i_get_usage(self, format_name: str, implementation: str) -> Descriptor:
|
|
666
723
|
"""Internally get the usage information for a registered format."""
|
|
667
|
-
|
|
668
|
-
if format_name in self._formats:
|
|
669
|
-
format_info = self._formats[format_name]
|
|
670
|
-
elif format_name.lower() not in self._lower2correct:
|
|
671
|
-
msg1 = f'Format "{format_name}" is not registered. '
|
|
672
|
-
msg1 += 'Available formats: '
|
|
673
|
-
msg1 += f'{", ".join(sorted(list(self._formats.keys())))}'
|
|
674
|
-
raise TableIOFactoryNoSuchError(msg1)
|
|
675
|
-
else:
|
|
676
|
-
format_info = self._formats[self._lower2correct[format_name.lower()]] # pylint: disable=protected-access # noqa: E501
|
|
677
|
-
assert format_info is not None
|
|
678
|
-
fusage = format_info._usage # pylint: disable=protected-access # noqa: E501
|
|
679
|
-
if implementation in fusage:
|
|
680
|
-
return fusage[implementation]
|
|
681
|
-
l2correct = format_info._lower2correct # pylint: disable=protected-access # noqa: E501
|
|
682
|
-
if implementation.lower() not in l2correct:
|
|
683
|
-
msg2 = f'Implementation "{implementation}" is not registered. '
|
|
684
|
-
msg2 += 'Available implementations: '
|
|
685
|
-
msg2 += f'{", ".join(list(format_info._registry.keys()))}' # pylint: disable=protected-access # noqa: E501
|
|
686
|
-
raise TableIOFactoryNoSuchError(msg2)
|
|
687
|
-
correct_name = l2correct[implementation.lower()]
|
|
688
|
-
return format_info._usage[correct_name] # pylint: disable=protected-access # noqa: E501
|
|
724
|
+
return self._format_info(format_name).get_usage(implementation)
|
|
689
725
|
|
|
690
726
|
|
|
691
727
|
def create_tableio(format_name: str, # pylint: disable=too-many-arguments, too-many-positional-arguments # noqa: E501
|
|
@@ -729,7 +765,9 @@ def create_tableio(format_name: str, # pylint: disable=too-many-arguments, too-
|
|
|
729
765
|
|
|
730
766
|
|
|
731
767
|
def filter_args_tableio(args: OptionalArgs, format_name: str,
|
|
732
|
-
implementation: str
|
|
768
|
+
implementation: Optional[str],
|
|
769
|
+
capabilities: Optional[Capabilities] = None) \
|
|
770
|
+
-> OptionalArgs:
|
|
733
771
|
"""Filter the arguments for a registered format.
|
|
734
772
|
|
|
735
773
|
This is a shortcut for TableIOFactory.filter_args().
|
|
@@ -743,15 +781,24 @@ def filter_args_tableio(args: OptionalArgs, format_name: str,
|
|
|
743
781
|
args: The arguments to filter.
|
|
744
782
|
format_name: The name identifier of the format class to filter
|
|
745
783
|
the arguments for.
|
|
746
|
-
implementation: The implementation name to use.
|
|
784
|
+
implementation: The implementation name to use. If implementation
|
|
785
|
+
is specified as None, filtering is done for the
|
|
786
|
+
implementation create() would use with a None value
|
|
787
|
+
for the implementation parameter.
|
|
788
|
+
capabilities: The capabilities to match. This is used to determine
|
|
789
|
+
the implementation to use if implementation is None.
|
|
747
790
|
Returns:
|
|
748
791
|
The filtered arguments.
|
|
749
792
|
Raises:
|
|
750
793
|
TableIOFactoryNoSuchError: If the format_name or implementation
|
|
751
794
|
name is not registered.
|
|
795
|
+
TableIOFactoryNoCapabilityMatch: If the capabilities cannot be
|
|
796
|
+
matched to the selected
|
|
797
|
+
implementation.
|
|
752
798
|
"""
|
|
753
799
|
return TableIOFactory.filter_args(args=args, format_name=format_name,
|
|
754
|
-
implementation=implementation
|
|
800
|
+
implementation=implementation,
|
|
801
|
+
capabilities=capabilities)
|
|
755
802
|
|
|
756
803
|
|
|
757
804
|
def list_registered_tableio(lower: bool = False,
|
|
@@ -787,11 +834,12 @@ def list_registered_tableio(lower: bool = False,
|
|
|
787
834
|
empty_is_ok=empty_is_ok)
|
|
788
835
|
|
|
789
836
|
|
|
837
|
+
# pylint: disable-next=too-many-arguments,too-many-positional-arguments
|
|
790
838
|
def list_implementations_tableio(format_name: Optional[str] = None,
|
|
791
|
-
lower: bool = False,
|
|
792
|
-
upper: bool = False,
|
|
839
|
+
lower: bool = False, upper: bool = False,
|
|
793
840
|
capabilities: Optional[Capabilities] = None,
|
|
794
|
-
empty_is_ok: bool = False
|
|
841
|
+
empty_is_ok: bool = False,
|
|
842
|
+
alphabetical: bool = True) -> list[str]:
|
|
795
843
|
"""Get a list of all registered implementation names.
|
|
796
844
|
|
|
797
845
|
This is a shortcut for TableIOFactory.get_registered_implementations().
|
|
@@ -810,6 +858,12 @@ def list_implementations_tableio(format_name: Optional[str] = None,
|
|
|
810
858
|
If False, a TableIOFactoryNoCapabilityMatch
|
|
811
859
|
error is raised if no implementations match the
|
|
812
860
|
capabilities.
|
|
861
|
+
alphabetical: If True, the implementations are returned in
|
|
862
|
+
alphabetical order. If False, the implementations
|
|
863
|
+
are returned with the strict matches first and the
|
|
864
|
+
nonstrict matches last. Both strict and nonstrict
|
|
865
|
+
matches are sorted by priority, from highest to
|
|
866
|
+
lowest.
|
|
813
867
|
Returns:
|
|
814
868
|
A list of registered implementation name strings optionally
|
|
815
869
|
filtered to only include implementations that offer the
|
|
@@ -821,7 +875,8 @@ def list_implementations_tableio(format_name: Optional[str] = None,
|
|
|
821
875
|
"""
|
|
822
876
|
return TableIOFactory.get_registered_implementations(
|
|
823
877
|
format_name=format_name, lower=lower, upper=upper,
|
|
824
|
-
capabilities=capabilities, empty_is_ok=empty_is_ok
|
|
878
|
+
capabilities=capabilities, empty_is_ok=empty_is_ok,
|
|
879
|
+
alphabetical=alphabetical)
|
|
825
880
|
|
|
826
881
|
|
|
827
882
|
def usage_tableio(format_name: str, implementation: str) -> Descriptor:
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Callable, Optional
|
|
9
9
|
from xml.etree import ElementTree as ET
|
|
10
|
+
from zipfile import ZipFile, ZipInfo
|
|
10
11
|
from openpyxl import Workbook, load_workbook
|
|
11
12
|
from openpyxl.styles import Border, Font, PatternFill, Side
|
|
12
13
|
from openpyxl.utils.cell import get_column_letter, range_boundaries
|
|
@@ -27,7 +28,25 @@ _HIGHLIGHT_RGB: dict[Color, str] = {
|
|
|
27
28
|
Color.GREEN: 'FFC6EFCE',
|
|
28
29
|
Color.YELLOW: 'FFFFFF00'
|
|
29
30
|
}
|
|
30
|
-
|
|
31
|
+
_SPREADSHEET_NS = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'
|
|
32
|
+
_REL_NS = 'http://schemas.openxmlformats.org/package/2006/relationships'
|
|
33
|
+
_CONTENT_TYPES_NS = (
|
|
34
|
+
'http://schemas.openxmlformats.org/package/2006/content-types'
|
|
35
|
+
)
|
|
36
|
+
_XML_NS = {'main': _SPREADSHEET_NS}
|
|
37
|
+
_REL_XML_NS = {'rel': _REL_NS}
|
|
38
|
+
_CONTENT_TYPES_XML_NS = {'ct': _CONTENT_TYPES_NS}
|
|
39
|
+
_SHARED_STRINGS_PATH = 'xl/sharedStrings.xml'
|
|
40
|
+
_WORKBOOK_RELS_PATH = 'xl/_rels/workbook.xml.rels'
|
|
41
|
+
_CONTENT_TYPES_PATH = '[Content_Types].xml'
|
|
42
|
+
_SHARED_STRINGS_REL_TYPE = (
|
|
43
|
+
'http://schemas.openxmlformats.org/officeDocument/2006/'
|
|
44
|
+
'relationships/sharedStrings'
|
|
45
|
+
)
|
|
46
|
+
_SHARED_STRINGS_CONTENT_TYPE = (
|
|
47
|
+
'application/vnd.openxmlformats-officedocument.'
|
|
48
|
+
'spreadsheetml.sharedStrings+xml'
|
|
49
|
+
)
|
|
31
50
|
_FONT_CHILD_ORDER = [
|
|
32
51
|
'b',
|
|
33
52
|
'i',
|
|
@@ -46,6 +65,11 @@ _FONT_CHILD_ORDER = [
|
|
|
46
65
|
]
|
|
47
66
|
|
|
48
67
|
|
|
68
|
+
def _xml_tag(namespace: str, tag_name: str) -> str:
|
|
69
|
+
"""Return one fully qualified XML tag name."""
|
|
70
|
+
return f'{{{namespace}}}{tag_name}'
|
|
71
|
+
|
|
72
|
+
|
|
49
73
|
def _font_child_sort_key(element: ET.Element) -> int:
|
|
50
74
|
"""Return the schema-order key for one Excel font child element."""
|
|
51
75
|
tag_name = element.tag.split('}', 1)[-1]
|
|
@@ -63,14 +87,159 @@ def _styles_xml_with_sorted_fonts(data: bytes) -> bytes:
|
|
|
63
87
|
xml_declaration=True))
|
|
64
88
|
|
|
65
89
|
|
|
90
|
+
def _new_shared_strings_root() -> ET.Element:
|
|
91
|
+
"""Create an empty shared strings XML root element."""
|
|
92
|
+
return ET.Element(_xml_tag(_SPREADSHEET_NS, 'sst'))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _read_shared_strings_root(file_name: Path) -> ET.Element:
|
|
96
|
+
"""Read the shared strings root, or create an empty one."""
|
|
97
|
+
try:
|
|
98
|
+
with ZipFile(file_name, 'r') as archive:
|
|
99
|
+
data = archive.read(_SHARED_STRINGS_PATH)
|
|
100
|
+
except KeyError:
|
|
101
|
+
return _new_shared_strings_root()
|
|
102
|
+
return ET.fromstring(data)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _shared_string_count(shared_strings_root: ET.Element) -> int:
|
|
106
|
+
"""Return the number of shared string items in the root."""
|
|
107
|
+
return len(shared_strings_root.findall('main:si', _XML_NS))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _shared_strings_xml(shared_strings_root: ET.Element) -> bytes:
|
|
111
|
+
"""Return finalized shared strings XML bytes."""
|
|
112
|
+
count_text = str(_shared_string_count(shared_strings_root))
|
|
113
|
+
shared_strings_root.set('count', count_text)
|
|
114
|
+
shared_strings_root.set('uniqueCount', count_text)
|
|
115
|
+
return bytes(ET.tostring(shared_strings_root, encoding='utf-8',
|
|
116
|
+
xml_declaration=True))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _inline_string_to_shared_string(
|
|
120
|
+
cell: ET.Element,
|
|
121
|
+
shared_strings_root: ET.Element) -> None:
|
|
122
|
+
"""Move one inline string cell value to the shared string table."""
|
|
123
|
+
inline_string = cell.find('main:is', _XML_NS)
|
|
124
|
+
if inline_string is None:
|
|
125
|
+
return
|
|
126
|
+
shared_index = _shared_string_count(shared_strings_root)
|
|
127
|
+
shared_item = ET.Element(_xml_tag(_SPREADSHEET_NS, 'si'))
|
|
128
|
+
for child in list(inline_string):
|
|
129
|
+
inline_string.remove(child)
|
|
130
|
+
shared_item.append(child)
|
|
131
|
+
shared_strings_root.append(shared_item)
|
|
132
|
+
cell.remove(inline_string)
|
|
133
|
+
cell.set('t', 's')
|
|
134
|
+
value = ET.Element(_xml_tag(_SPREADSHEET_NS, 'v'))
|
|
135
|
+
value.text = str(shared_index)
|
|
136
|
+
cell.append(value)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _sheet_xml_with_shared_strings(
|
|
140
|
+
data: bytes,
|
|
141
|
+
shared_strings_root: ET.Element) -> bytes:
|
|
142
|
+
"""Return sheet XML with inline strings converted to shared strings."""
|
|
143
|
+
root = ET.fromstring(data)
|
|
144
|
+
for cell in root.findall('.//main:c[@t="inlineStr"]', _XML_NS):
|
|
145
|
+
_inline_string_to_shared_string(cell, shared_strings_root)
|
|
146
|
+
return bytes(ET.tostring(root, encoding='utf-8',
|
|
147
|
+
xml_declaration=True))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _content_types_with_shared_strings(data: bytes) -> bytes:
|
|
151
|
+
"""Return content types XML with a shared strings override."""
|
|
152
|
+
root = ET.fromstring(data)
|
|
153
|
+
for override in root.findall('ct:Override', _CONTENT_TYPES_XML_NS):
|
|
154
|
+
if override.get('PartName') == '/' + _SHARED_STRINGS_PATH:
|
|
155
|
+
return data
|
|
156
|
+
override = ET.Element(_xml_tag(_CONTENT_TYPES_NS, 'Override'))
|
|
157
|
+
override.set('PartName', '/' + _SHARED_STRINGS_PATH)
|
|
158
|
+
override.set('ContentType', _SHARED_STRINGS_CONTENT_TYPE)
|
|
159
|
+
root.append(override)
|
|
160
|
+
return bytes(ET.tostring(root, encoding='utf-8',
|
|
161
|
+
xml_declaration=True))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _next_relationship_id(root: ET.Element) -> str:
|
|
165
|
+
"""Return the next workbook relationship id."""
|
|
166
|
+
max_id = 0
|
|
167
|
+
for relationship in root.findall('rel:Relationship', _REL_XML_NS):
|
|
168
|
+
rel_id = relationship.get('Id', '')
|
|
169
|
+
if not rel_id.startswith('rId'):
|
|
170
|
+
continue
|
|
171
|
+
try:
|
|
172
|
+
max_id = max(max_id, int(rel_id[3:]))
|
|
173
|
+
except ValueError:
|
|
174
|
+
continue
|
|
175
|
+
return f'rId{max_id + 1}'
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _workbook_rels_with_shared_strings(data: bytes) -> bytes:
|
|
179
|
+
"""Return workbook relationships XML with a shared strings relation."""
|
|
180
|
+
root = ET.fromstring(data)
|
|
181
|
+
for relationship in root.findall('rel:Relationship', _REL_XML_NS):
|
|
182
|
+
if relationship.get('Type') == _SHARED_STRINGS_REL_TYPE:
|
|
183
|
+
return data
|
|
184
|
+
relationship = ET.Element(_xml_tag(_REL_NS, 'Relationship'))
|
|
185
|
+
relationship.set('Id', _next_relationship_id(root))
|
|
186
|
+
relationship.set('Type', _SHARED_STRINGS_REL_TYPE)
|
|
187
|
+
relationship.set('Target', 'sharedStrings.xml')
|
|
188
|
+
root.append(relationship)
|
|
189
|
+
return bytes(ET.tostring(root, encoding='utf-8',
|
|
190
|
+
xml_declaration=True))
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _is_worksheet_xml(filename: str) -> bool:
|
|
194
|
+
"""Return True if an archive entry is a worksheet XML file."""
|
|
195
|
+
return filename.startswith('xl/worksheets/') and filename.endswith('.xml')
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _worksheet_rewrites_with_shared_strings(
|
|
199
|
+
file_name: Path,
|
|
200
|
+
shared_strings_root: ET.Element) -> dict[str, bytes]:
|
|
201
|
+
"""Return worksheet XML rewrites and update the shared string table."""
|
|
202
|
+
rewrites: dict[str, bytes] = {}
|
|
203
|
+
with ZipFile(file_name, 'r') as archive:
|
|
204
|
+
for filename in archive.namelist():
|
|
205
|
+
if not _is_worksheet_xml(filename):
|
|
206
|
+
continue
|
|
207
|
+
data = archive.read(filename)
|
|
208
|
+
if b'inlineStr' not in data:
|
|
209
|
+
continue
|
|
210
|
+
rewrites[filename] = _sheet_xml_with_shared_strings(
|
|
211
|
+
data, shared_strings_root)
|
|
212
|
+
return rewrites
|
|
213
|
+
|
|
214
|
+
|
|
66
215
|
def _rewrite_saved_workbook(file_name: Path) -> None:
|
|
67
216
|
"""Rewrite the saved workbook so styles XML follows validator order."""
|
|
68
|
-
|
|
217
|
+
shared_strings_root = _read_shared_strings_root(file_name)
|
|
218
|
+
worksheet_rewrites = _worksheet_rewrites_with_shared_strings(
|
|
219
|
+
file_name, shared_strings_root)
|
|
220
|
+
use_shared_strings = _shared_string_count(shared_strings_root) > 0
|
|
221
|
+
|
|
222
|
+
def rewrite_entry(item: ZipInfo, data: bytes) -> Optional[bytes]:
|
|
69
223
|
"""Rewrite one workbook archive entry when needed."""
|
|
70
|
-
|
|
224
|
+
filename = item.filename
|
|
225
|
+
if filename == 'xl/styles.xml':
|
|
71
226
|
return _styles_xml_with_sorted_fonts(data)
|
|
227
|
+
if use_shared_strings and filename == _SHARED_STRINGS_PATH:
|
|
228
|
+
return None
|
|
229
|
+
if use_shared_strings and filename == _CONTENT_TYPES_PATH:
|
|
230
|
+
return _content_types_with_shared_strings(data)
|
|
231
|
+
if use_shared_strings and filename == _WORKBOOK_RELS_PATH:
|
|
232
|
+
return _workbook_rels_with_shared_strings(data)
|
|
233
|
+
if filename in worksheet_rewrites:
|
|
234
|
+
return worksheet_rewrites[filename]
|
|
72
235
|
return data
|
|
73
|
-
|
|
236
|
+
extra_entries = None
|
|
237
|
+
if use_shared_strings:
|
|
238
|
+
extra_entries = {
|
|
239
|
+
_SHARED_STRINGS_PATH: _shared_strings_xml(shared_strings_root)
|
|
240
|
+
}
|
|
241
|
+
rewrite_zip_archive(file_name, rewrite_entry,
|
|
242
|
+
extra_entries=extra_entries)
|
|
74
243
|
|
|
75
244
|
|
|
76
245
|
class TableIOExcelOpenPyXL(TableIOExcelBased):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tableio
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7
|
|
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>
|
|
@@ -11,14 +11,14 @@ Classifier: Operating System :: OS Independent
|
|
|
11
11
|
Requires-Python: >=3.12
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE.txt
|
|
14
|
-
Requires-Dist: pip>=26.
|
|
14
|
+
Requires-Dist: pip>=26.1.1
|
|
15
15
|
Requires-Dist: setuptools>=82.0.1
|
|
16
|
-
Requires-Dist: build>=1.
|
|
17
|
-
Requires-Dist: wheel>=0.
|
|
16
|
+
Requires-Dist: build>=1.5.0
|
|
17
|
+
Requires-Dist: wheel>=0.47.0
|
|
18
18
|
Requires-Dist: mformat-ext>=0.6
|
|
19
19
|
Requires-Dist: openpyxl>=3.1.5
|
|
20
20
|
Requires-Dist: types-openpyxl>=3.1.5.20260402
|
|
21
|
-
Requires-Dist: odfdo>=3.22.
|
|
21
|
+
Requires-Dist: odfdo>=3.22.7
|
|
22
22
|
Requires-Dist: xlsxwriter>=3.2.9
|
|
23
23
|
Requires-Dist: pylightxl>=1.61
|
|
24
24
|
Dynamic: author
|
|
@@ -135,20 +135,10 @@ readers/writers to the factory.
|
|
|
135
135
|
Even though the API documentation exists, most users and programmers probably get
|
|
136
136
|
a better start by reading the examples.
|
|
137
137
|
|
|
138
|
-
## Version history
|
|
139
|
-
|
|
140
|
-
| Version | Date | Python version | Comment |
|
|
141
|
-
|---------|-------------|----------------|-----------------------------|
|
|
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 |
|
|
145
|
-
| 0.2 | 2026 Mar 28 | 3.12 or newer | More features, API changes |
|
|
146
|
-
| 0.1 | 2026 Mar 23 | 3.12 or newer | First released version |
|
|
147
|
-
|
|
148
138
|
## Test summary
|
|
149
139
|
|
|
150
|
-
- Test result:
|
|
140
|
+
- Test result: 1186 passed in 18s
|
|
151
141
|
- No flake8 warnings.
|
|
152
142
|
- No mypy errors found.
|
|
153
|
-
- Built version(s): 0.
|
|
154
|
-
- Build and test using Python 3.14.
|
|
143
|
+
- Built version(s): 0.7
|
|
144
|
+
- Build and test using Python 3.14.4
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|