tableio 0.5__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.
Files changed (35) hide show
  1. {tableio-0.5/tableio.egg-info → tableio-0.7}/PKG-INFO +9 -19
  2. {tableio-0.5 → tableio-0.7}/README.md +3 -3
  3. {tableio-0.5 → tableio-0.7}/README_pypi.md +4 -14
  4. {tableio-0.5 → tableio-0.7}/pyproject.toml +1 -1
  5. {tableio-0.5 → tableio-0.7}/setup.py +5 -5
  6. {tableio-0.5 → tableio-0.7}/src/tableio/factory.py +150 -95
  7. {tableio-0.5 → tableio-0.7}/src/tableio/optional_args.py +4 -4
  8. {tableio-0.5 → tableio-0.7}/src/tableio/tableio_excel_openpyxl.py +173 -4
  9. {tableio-0.5 → tableio-0.7/tableio.egg-info}/PKG-INFO +9 -19
  10. {tableio-0.5 → tableio-0.7}/tableio.egg-info/requires.txt +4 -4
  11. {tableio-0.5 → tableio-0.7}/LICENSE.txt +0 -0
  12. {tableio-0.5 → tableio-0.7}/setup.cfg +0 -0
  13. {tableio-0.5 → tableio-0.7}/src/tableio/__init__.py +0 -0
  14. {tableio-0.5 → tableio-0.7}/src/tableio/_archive_rewrite.py +0 -0
  15. {tableio-0.5 → tableio-0.7}/src/tableio/border_helper.py +0 -0
  16. {tableio-0.5 → tableio-0.7}/src/tableio/capability.py +0 -0
  17. {tableio-0.5 → tableio-0.7}/src/tableio/color.py +0 -0
  18. {tableio-0.5 → tableio-0.7}/src/tableio/py.typed +0 -0
  19. {tableio-0.5 → tableio-0.7}/src/tableio/reg_pkg_formats.py +0 -0
  20. {tableio-0.5 → tableio-0.7}/src/tableio/tableio.py +0 -0
  21. {tableio-0.5 → tableio-0.7}/src/tableio/tableio_csv.py +0 -0
  22. {tableio-0.5 → tableio-0.7}/src/tableio/tableio_excel_pylightxl.py +0 -0
  23. {tableio-0.5 → tableio-0.7}/src/tableio/tableio_excel_xlsxwriter.py +0 -0
  24. {tableio-0.5 → tableio-0.7}/src/tableio/tableio_excelbased.py +0 -0
  25. {tableio-0.5 → tableio-0.7}/src/tableio/tableio_mformat.py +0 -0
  26. {tableio-0.5 → tableio-0.7}/src/tableio/tableio_mformatbased.py +0 -0
  27. {tableio-0.5 → tableio-0.7}/src/tableio/tableio_ods_odfdo.py +0 -0
  28. {tableio-0.5 → tableio-0.7}/src/tableio/tableio_spreadsheetbased.py +0 -0
  29. {tableio-0.5 → tableio-0.7}/src/tableio/tableio_textbased.py +0 -0
  30. {tableio-0.5 → tableio-0.7}/src/tableio/tableio_types.py +0 -0
  31. {tableio-0.5 → tableio-0.7}/src/tableio/value_type.py +0 -0
  32. {tableio-0.5 → tableio-0.7}/src/tableio/valueconversion.py +0 -0
  33. {tableio-0.5 → tableio-0.7}/tableio.egg-info/SOURCES.txt +0 -0
  34. {tableio-0.5 → tableio-0.7}/tableio.egg-info/dependency_links.txt +0 -0
  35. {tableio-0.5 → 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.5
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.0.1
14
+ Requires-Dist: pip>=26.1.1
15
15
  Requires-Dist: setuptools>=82.0.1
16
- Requires-Dist: build>=1.4.2
17
- Requires-Dist: wheel>=0.46.3
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.4
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
@@ -129,26 +129,16 @@ You can find the public API documentation at [https://bitbucket.org/tom-bjorkhol
129
129
 
130
130
  You can find the protected API documentation at [https://bitbucket.org/tom-bjorkholm/table-io/src/master/doc/protected_api.md](https://bitbucket.org/tom-bjorkholm/table-io/src/master/doc/protected_api.md)
131
131
  The protected API documentation is only for developers that want to
132
- extend the framework with by adding their own classes as registered
132
+ extend the framework by adding their own classes as registered
133
133
  readers/writers to the factory.
134
134
 
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: 1173 passed in 18s
140
+ - Test result: 1186 passed in 18s
151
141
  - No flake8 warnings.
152
142
  - No mypy errors found.
153
- - Built version(s): 0.5
154
- - Build and test using Python 3.14.3
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: 1173 passed in 18s
96
+ - Test result: 1186 passed in 18s
97
97
  - No flake8 warnings.
98
98
  - No mypy errors found.
99
- - Built version(s): 0.5
100
- - Build and test using Python 3.14.3
99
+ - Built version(s): 0.7
100
+ - Build and test using Python 3.14.4
@@ -101,26 +101,16 @@ You can find the public API documentation at [https://bitbucket.org/tom-bjorkhol
101
101
 
102
102
  You can find the protected API documentation at [https://bitbucket.org/tom-bjorkholm/table-io/src/master/doc/protected_api.md](https://bitbucket.org/tom-bjorkholm/table-io/src/master/doc/protected_api.md)
103
103
  The protected API documentation is only for developers that want to
104
- extend the framework with by adding their own classes as registered
104
+ extend the framework by adding their own classes as registered
105
105
  readers/writers to the factory.
106
106
 
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: 1173 passed in 18s
112
+ - Test result: 1186 passed in 18s
123
113
  - No flake8 warnings.
124
114
  - No mypy errors found.
125
- - Built version(s): 0.5
126
- - Build and test using Python 3.14.3
115
+ - Built version(s): 0.7
116
+ - Build and test using Python 3.14.4
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tableio"
7
- version = "0.5"
7
+ version = "0.7"
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.5',
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.0.1',
18
+ 'pip >= 26.1.1',
19
19
  'setuptools >= 82.0.1',
20
- 'build >= 1.4.2',
21
- 'wheel >= 0.46.3',
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.4',
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
- correct_name: Optional[str] = None
415
- if format_name in self._formats:
416
- correct_name = format_name
417
- elif format_name.lower() in self._lower2correct:
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) -> OptionalArgs:
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) -> OptionalArgs:
506
+ implementation: Optional[str],
507
+ capabilities: Optional[Capabilities] = None
508
+ ) -> OptionalArgs:
479
509
  """Internally filter the arguments for a registered format."""
480
- format_usage = self.i_get_usage(format_name=format_name,
481
- implementation=implementation)
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(format_name: Optional[str] = None,
561
- lower: bool = False,
562
- upper: bool = False,
563
- capabilities: Optional[Capabilities]
564
- = None,
565
- empty_is_ok: bool = False) -> list[str]:
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
- def i_get_registered_implementations( # pylint: disable=too-many-arguments,too-many-positional-arguments # noqa: E501
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) -> list[str]:
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
- implkeys: set[str] = set()
617
- if capabilities is not None:
618
- implmatches: list[BestMatch] = []
619
- for fmtkey in fmtkeys:
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 sorted(implkeys): # order was lost using a set
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
- format_info: Optional[FactoryFormatInfo] = None
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) -> OptionalArgs:
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) -> list[str]:
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:
@@ -13,9 +13,9 @@ class CsvDialect(IntEnum):
13
13
  """The type of CSV file to write."""
14
14
 
15
15
  EXCEL = auto()
16
- """Excel CSV file type."""
16
+ """Excel CSV file type/dialect."""
17
17
  UNIX = auto()
18
- """Unix CSV file type."""
18
+ """Unix CSV file type/dialect."""
19
19
 
20
20
 
21
21
  class OptionalArgsDict(OptArgsDict, total=False):
@@ -30,8 +30,8 @@ class OptionalArgsDict(OptArgsDict, total=False):
30
30
  package.
31
31
  """
32
32
 
33
- csv_type: Optional[CsvDialect]
34
- """The type of CSV file to write. None for default type."""
33
+ csv_dialect: Optional[CsvDialect]
34
+ """The type/dialect of CSV file to write. None for default type."""
35
35
 
36
36
  csv_delimiter: Optional[str]
37
37
  """The delimiter to use for CSV files. None for default delimiter."""
@@ -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
- _XML_NS = {'main': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'}
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
- def rewrite_entry(item: object, data: bytes) -> bytes:
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
- if getattr(item, 'filename') == 'xl/styles.xml':
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
- rewrite_zip_archive(file_name, rewrite_entry)
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.5
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.0.1
14
+ Requires-Dist: pip>=26.1.1
15
15
  Requires-Dist: setuptools>=82.0.1
16
- Requires-Dist: build>=1.4.2
17
- Requires-Dist: wheel>=0.46.3
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.4
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
@@ -129,26 +129,16 @@ You can find the public API documentation at [https://bitbucket.org/tom-bjorkhol
129
129
 
130
130
  You can find the protected API documentation at [https://bitbucket.org/tom-bjorkholm/table-io/src/master/doc/protected_api.md](https://bitbucket.org/tom-bjorkholm/table-io/src/master/doc/protected_api.md)
131
131
  The protected API documentation is only for developers that want to
132
- extend the framework with by adding their own classes as registered
132
+ extend the framework by adding their own classes as registered
133
133
  readers/writers to the factory.
134
134
 
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: 1173 passed in 18s
140
+ - Test result: 1186 passed in 18s
151
141
  - No flake8 warnings.
152
142
  - No mypy errors found.
153
- - Built version(s): 0.5
154
- - Build and test using Python 3.14.3
143
+ - Built version(s): 0.7
144
+ - Build and test using Python 3.14.4
@@ -1,10 +1,10 @@
1
- pip>=26.0.1
1
+ pip>=26.1.1
2
2
  setuptools>=82.0.1
3
- build>=1.4.2
4
- wheel>=0.46.3
3
+ build>=1.5.0
4
+ wheel>=0.47.0
5
5
  mformat-ext>=0.6
6
6
  openpyxl>=3.1.5
7
7
  types-openpyxl>=3.1.5.20260402
8
- odfdo>=3.22.4
8
+ odfdo>=3.22.7
9
9
  xlsxwriter>=3.2.9
10
10
  pylightxl>=1.61
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