tableio 1.1__tar.gz → 1.2__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 (43) hide show
  1. {tableio-1.1 → tableio-1.2}/PKG-INFO +45 -6
  2. {tableio-1.1 → tableio-1.2}/README.md +4 -3
  3. {tableio-1.1 → tableio-1.2}/README_pypi.md +42 -3
  4. {tableio-1.1 → tableio-1.2}/pyproject.toml +1 -2
  5. {tableio-1.1 → tableio-1.2}/setup.py +3 -3
  6. {tableio-1.1 → tableio-1.2}/src/tableio/__init__.py +6 -1
  7. {tableio-1.1 → tableio-1.2}/src/tableio/config_data.py +8 -1
  8. {tableio-1.1 → tableio-1.2}/src/tableio/config_data_apply.py +30 -6
  9. {tableio-1.1 → tableio-1.2}/src/tableio/config_data_describe.py +13 -1
  10. {tableio-1.1 → tableio-1.2}/src/tableio/config_data_validate.py +5 -1
  11. {tableio-1.1 → tableio-1.2}/src/tableio/factory.py +28 -12
  12. tableio-1.2/src/tableio/optional_args.py +143 -0
  13. {tableio-1.1 → tableio-1.2}/src/tableio/tableio.py +17 -2
  14. {tableio-1.1 → tableio-1.2}/src/tableio/tableio_csv.py +106 -23
  15. {tableio-1.1 → tableio-1.2}/src/tableio/tableio_excel_openpyxl.py +20 -5
  16. {tableio-1.1 → tableio-1.2}/src/tableio/tableio_excel_pylightxl.py +96 -23
  17. {tableio-1.1 → tableio-1.2}/src/tableio/tableio_excel_xlsxwriter.py +18 -13
  18. {tableio-1.1 → tableio-1.2}/src/tableio/tableio_excelbased.py +11 -3
  19. {tableio-1.1 → tableio-1.2}/src/tableio/tableio_mformat.py +73 -27
  20. {tableio-1.1 → tableio-1.2}/src/tableio/tableio_mformatbased.py +30 -6
  21. {tableio-1.1 → tableio-1.2}/src/tableio/tableio_ods_odfdo.py +114 -11
  22. {tableio-1.1 → tableio-1.2}/src/tableio/tableio_spreadsheetbased.py +20 -7
  23. {tableio-1.1 → tableio-1.2}/src/tableio/tableio_textbased.py +15 -8
  24. tableio-1.2/src/tableio/timedelta_helpers.py +179 -0
  25. {tableio-1.1 → tableio-1.2}/src/tableio/value_type.py +2 -2
  26. {tableio-1.1 → tableio-1.2}/src/tableio/valueconversion.py +70 -35
  27. {tableio-1.1 → tableio-1.2}/tableio.egg-info/PKG-INFO +45 -6
  28. {tableio-1.1 → tableio-1.2}/tableio.egg-info/SOURCES.txt +1 -0
  29. {tableio-1.1 → tableio-1.2}/tableio.egg-info/requires.txt +2 -2
  30. tableio-1.1/src/tableio/optional_args.py +0 -77
  31. {tableio-1.1 → tableio-1.2}/LICENSE.txt +0 -0
  32. {tableio-1.1 → tableio-1.2}/setup.cfg +0 -0
  33. {tableio-1.1 → tableio-1.2}/src/tableio/_archive_rewrite.py +0 -0
  34. {tableio-1.1 → tableio-1.2}/src/tableio/access_capability.py +0 -0
  35. {tableio-1.1 → tableio-1.2}/src/tableio/border_helper.py +0 -0
  36. {tableio-1.1 → tableio-1.2}/src/tableio/capability.py +0 -0
  37. {tableio-1.1 → tableio-1.2}/src/tableio/color.py +0 -0
  38. {tableio-1.1 → tableio-1.2}/src/tableio/config_data_error.py +0 -0
  39. {tableio-1.1 → tableio-1.2}/src/tableio/py.typed +0 -0
  40. {tableio-1.1 → tableio-1.2}/src/tableio/reg_pkg_formats.py +0 -0
  41. {tableio-1.1 → tableio-1.2}/src/tableio/tableio_types.py +0 -0
  42. {tableio-1.1 → tableio-1.2}/tableio.egg-info/dependency_links.txt +0 -0
  43. {tableio-1.1 → tableio-1.2}/tableio.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tableio
3
- Version: 1.1
3
+ Version: 1.2
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>
@@ -19,8 +19,8 @@ Description-Content-Type: text/markdown
19
19
  License-File: LICENSE.txt
20
20
  Requires-Dist: mformat-ext>=0.7
21
21
  Requires-Dist: openpyxl>=3.1.5
22
- Requires-Dist: types-openpyxl>=3.1.5.20260518
23
- Requires-Dist: odfdo>=3.22.10
22
+ Requires-Dist: types-openpyxl>=3.1.5.20260827
23
+ Requires-Dist: odfdo>=3.26.3
24
24
  Requires-Dist: xlsxwriter>=3.2.9
25
25
  Requires-Dist: pylightxl>=1.61
26
26
  Dynamic: author
@@ -111,6 +111,7 @@ The main features are:
111
111
  - int
112
112
  - float
113
113
  - datetime
114
+ - timedelta (see below)
114
115
  - writing a table as a filtered data range
115
116
  - writing a table to a specific location in a spreadsheet (specified by a box)
116
117
  - reading table data from a specific location in a spreadsheet (specified by a box)
@@ -119,6 +120,43 @@ The main features are:
119
120
  at that position or at positions relative to that position.
120
121
  - several border styles for tables
121
122
 
123
+ ## Timedelta (duration) values
124
+
125
+ Excel (OpenPyXL, XlsxWriter, pylightxl) and ODS (odfdo) store `timedelta`
126
+ values natively, as duration cells, and read them back as `timedelta`.
127
+ (OpenPyXL reads durations with millisecond precision.)
128
+ The cells are displayed as `[hh]:mm:ss` (hours do not wrap at 24, seconds
129
+ are displayed rounded to whole seconds, the stored value is exact).
130
+ Microsoft Excel has no duration type, it stores a duration as a number of
131
+ days with a time number format. So by design Excel shows the cell as for
132
+ instance `72:00:00`, but shows the value as a date and time (like
133
+ `1/3/1900 12:00:00 AM`) in the formula bar when the cell is selected. Excel
134
+ does the same for durations typed in by hand, and LibreOffice shows
135
+ `72:00:00` in both places for the same file.
136
+ Notice also that Microsoft Excel cannot display negative durations (it
137
+ shows `####`). The stored value is still correct, it is read back
138
+ correctly and LibreOffice displays it as for instance `-00:03:58`. If
139
+ negative durations must be readable in Microsoft Excel, consider storing
140
+ the magnitude and the sign in separate columns.
141
+
142
+ Formats without native duration support (CSV and all write-only document
143
+ formats) write a fallback representation, selected with the optional
144
+ argument `timedelta_fallback` (a `TimeDeltaFallback` value):
145
+
146
+ | TimeDeltaFallback | Two days, three hours and 1.5 seconds |
147
+ |-----------------------|---------------------------------------|
148
+ | `HMS_STRING` (default)| `51:00:01.5` |
149
+ | `DHMS_STRING` | `2 d 03:00:01.5` |
150
+ | `DHMS_STRING_LONG` | `2 days 03:00:01.5` |
151
+ | `WDHMS_STRING` | `2 d 03:00:01.5` (with `W w` if a week or more) |
152
+ | `WDHMS_STRING_LONG` | `2 days 03:00:01.5` (with `W weeks` if a week or more) |
153
+ | `FLOATSECONDS` | `183601.5` |
154
+
155
+ Negative durations get a leading `-`. When reading such a file the values
156
+ are strings (CSV). Convert them with `parse_timedelta()`, that accepts all
157
+ fallback formats (and `timedelta` values, so it works for all formats).
158
+ `format_timedelta()` formats a `timedelta` in any of the fallback formats.
159
+
122
160
  ## Example programs
123
161
 
124
162
  The best way to learn to use this package is to use the provided
@@ -139,9 +177,10 @@ a better start by reading the examples.
139
177
 
140
178
  ## Test summary
141
179
 
142
- - Test result: 1347 passed in 20s
180
+ - Test result: 1567 passed in 9s
143
181
  - No flake8 warnings.
144
182
  - No mypy errors found.
183
+ - No pylint warnings.
145
184
  - No python layout warnings.
146
- - Built version(s): 1.1
147
- - Build and test using Python 3.14.6
185
+ - Built version(s): 1.2
186
+ - Build and test using Python 3.14.7
@@ -93,9 +93,10 @@ information about the build system. This README can also be viewed at
93
93
 
94
94
  ## Test summary
95
95
 
96
- - Test result: 1347 passed in 20s
96
+ - Test result: 1567 passed in 9s
97
97
  - No flake8 warnings.
98
98
  - No mypy errors found.
99
+ - No pylint warnings.
99
100
  - No python layout warnings.
100
- - Built version(s): 1.1
101
- - Build and test using Python 3.14.6
101
+ - Built version(s): 1.2
102
+ - Build and test using Python 3.14.7
@@ -81,6 +81,7 @@ The main features are:
81
81
  - int
82
82
  - float
83
83
  - datetime
84
+ - timedelta (see below)
84
85
  - writing a table as a filtered data range
85
86
  - writing a table to a specific location in a spreadsheet (specified by a box)
86
87
  - reading table data from a specific location in a spreadsheet (specified by a box)
@@ -89,6 +90,43 @@ The main features are:
89
90
  at that position or at positions relative to that position.
90
91
  - several border styles for tables
91
92
 
93
+ ## Timedelta (duration) values
94
+
95
+ Excel (OpenPyXL, XlsxWriter, pylightxl) and ODS (odfdo) store `timedelta`
96
+ values natively, as duration cells, and read them back as `timedelta`.
97
+ (OpenPyXL reads durations with millisecond precision.)
98
+ The cells are displayed as `[hh]:mm:ss` (hours do not wrap at 24, seconds
99
+ are displayed rounded to whole seconds, the stored value is exact).
100
+ Microsoft Excel has no duration type, it stores a duration as a number of
101
+ days with a time number format. So by design Excel shows the cell as for
102
+ instance `72:00:00`, but shows the value as a date and time (like
103
+ `1/3/1900 12:00:00 AM`) in the formula bar when the cell is selected. Excel
104
+ does the same for durations typed in by hand, and LibreOffice shows
105
+ `72:00:00` in both places for the same file.
106
+ Notice also that Microsoft Excel cannot display negative durations (it
107
+ shows `####`). The stored value is still correct, it is read back
108
+ correctly and LibreOffice displays it as for instance `-00:03:58`. If
109
+ negative durations must be readable in Microsoft Excel, consider storing
110
+ the magnitude and the sign in separate columns.
111
+
112
+ Formats without native duration support (CSV and all write-only document
113
+ formats) write a fallback representation, selected with the optional
114
+ argument `timedelta_fallback` (a `TimeDeltaFallback` value):
115
+
116
+ | TimeDeltaFallback | Two days, three hours and 1.5 seconds |
117
+ |-----------------------|---------------------------------------|
118
+ | `HMS_STRING` (default)| `51:00:01.5` |
119
+ | `DHMS_STRING` | `2 d 03:00:01.5` |
120
+ | `DHMS_STRING_LONG` | `2 days 03:00:01.5` |
121
+ | `WDHMS_STRING` | `2 d 03:00:01.5` (with `W w` if a week or more) |
122
+ | `WDHMS_STRING_LONG` | `2 days 03:00:01.5` (with `W weeks` if a week or more) |
123
+ | `FLOATSECONDS` | `183601.5` |
124
+
125
+ Negative durations get a leading `-`. When reading such a file the values
126
+ are strings (CSV). Convert them with `parse_timedelta()`, that accepts all
127
+ fallback formats (and `timedelta` values, so it works for all formats).
128
+ `format_timedelta()` formats a `timedelta` in any of the fallback formats.
129
+
92
130
  ## Example programs
93
131
 
94
132
  The best way to learn to use this package is to use the provided
@@ -109,9 +147,10 @@ a better start by reading the examples.
109
147
 
110
148
  ## Test summary
111
149
 
112
- - Test result: 1347 passed in 20s
150
+ - Test result: 1567 passed in 9s
113
151
  - No flake8 warnings.
114
152
  - No mypy errors found.
153
+ - No pylint warnings.
115
154
  - No python layout warnings.
116
- - Built version(s): 1.1
117
- - Build and test using Python 3.14.6
155
+ - Built version(s): 1.2
156
+ - Build and test using Python 3.14.7
@@ -4,7 +4,6 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tableio"
7
- version = "1.1"
8
7
  authors = [
9
8
  { name="Tom Björkholm", email="klausuler_linnet0q@icloud.com" },
10
9
  ]
@@ -22,7 +21,7 @@ classifiers = [
22
21
  "Intended Audience :: Developers",
23
22
  "Typing :: Typed"
24
23
  ]
25
- dynamic = ["dependencies"]
24
+ dynamic = ["dependencies", "version"]
26
25
 
27
26
  [project.urls]
28
27
  "Source code" = "https://github.com/tom-bjorkholm/table-io"
@@ -5,7 +5,7 @@ from setuptools import setup
5
5
 
6
6
  setup(
7
7
  name='tableio',
8
- version='1.1',
8
+ version='1.2',
9
9
  description='Uniform way to write table data to and read from different ' \
10
10
  'file formats',
11
11
  author='Tom Björkholm',
@@ -17,8 +17,8 @@ setup(
17
17
  install_requires=[
18
18
  'mformat-ext >= 0.7',
19
19
  'openpyxl >= 3.1.5',
20
- 'types-openpyxl >= 3.1.5.20260518',
21
- 'odfdo >= 3.22.10',
20
+ 'types-openpyxl >= 3.1.5.20260827',
21
+ 'odfdo >= 3.26.3',
22
22
  'xlsxwriter >= 3.2.9',
23
23
  'pylightxl >= 1.61'
24
24
  ]
@@ -21,9 +21,11 @@ from tableio.config_data_error import ConfigError, ConfigIssue
21
21
  from tableio.config_data_validate import tio_config_validate
22
22
  from tableio.factory import create_tableio, filter_args_tableio, \
23
23
  list_implementations_tableio, list_registered_tableio
24
- from tableio.optional_args import CsvDialect, OptionalArgs, OptionalArgsDict
24
+ from tableio.optional_args import CsvDialect, OptionalArgs, \
25
+ OptionalArgsDict, TimeDeltaFallback
25
26
  from tableio.tableio import TableIO
26
27
  from tableio.tableio_types import TableBorderStyle, Box, FileAccess, Position
28
+ from tableio.timedelta_helpers import format_timedelta, parse_timedelta
27
29
  from tableio.value_type import CellT, DictData, DictDataMap, Fmt, \
28
30
  FmtDictData, FmtDictRow, FmtListData, FmtListRow, ListData, \
29
31
  ListDataSeq, ReadResult, Value, ValueFmt
@@ -43,6 +45,8 @@ __all__ = ['create_tableio',
43
45
  'DictDataMap',
44
46
  'ReadResult',
45
47
  'Fmt',
48
+ 'format_timedelta',
49
+ 'parse_timedelta',
46
50
  'ValueFmt',
47
51
  'FmtListRow',
48
52
  'FmtDictRow',
@@ -63,6 +67,7 @@ __all__ = ['create_tableio',
63
67
  'list_registered_tableio',
64
68
  'list_implementations_tableio',
65
69
  'CsvDialect',
70
+ 'TimeDeltaFallback',
66
71
  'ConfigData', 'ConfigError', 'ConfigIssue', 'ConfigSpec',
67
72
  'CsvConfigData', 'HtmlConfigData', 'LatexConfigData',
68
73
  'tio_config_create', 'tio_config_default',
@@ -6,7 +6,7 @@
6
6
 
7
7
  from dataclasses import dataclass
8
8
  from typing import Optional
9
- from tableio.optional_args import CsvDialect
9
+ from tableio.optional_args import CsvDialect, TimeDeltaFallback
10
10
 
11
11
 
12
12
  @dataclass
@@ -115,3 +115,10 @@ class ConfigData: # pylint: disable=too-many-instance-attributes
115
115
 
116
116
  latex: Optional[LatexConfigData] = None
117
117
  """LaTeX-specific configuration values, or ``None`` when unset."""
118
+
119
+ timedelta_fallback: Optional[TimeDeltaFallback] = None
120
+ """How formats without native timedelta write it, or ``None``.
121
+
122
+ ``None`` means backend default (``TimeDeltaFallback.HMS_STRING``).
123
+ Formats with native timedelta support ignore this value.
124
+ """
@@ -7,6 +7,8 @@
7
7
  from typing import Callable, NoReturn, Optional, TypeVar, cast
8
8
 
9
9
  from mformat.mformat import PathLike
10
+ from mformat.paper_size import PaperSize
11
+ from mformat.plain_text_table import TableAlignment
10
12
  from tableio.access_capability import add_access_capabilities
11
13
  from tableio.capability import Capabilities, capability_match
12
14
  from tableio.config_data import ConfigData, CsvConfigData, HtmlConfigData, \
@@ -15,7 +17,8 @@ from tableio.config_data_error import ConfigError, ConfigIssue
15
17
  from tableio.config_data_validate import tio_config_validate
16
18
  from tableio.factory import create_tableio, filter_args_tableio, \
17
19
  list_implementations_tableio, list_registered_tableio, usage_tableio
18
- from tableio.optional_args import CsvDialect, OptionalArgs, OptionalArgsDict
20
+ from tableio.optional_args import CsvDialect, OptionalArgs, \
21
+ OptionalArgsDict, TimeDeltaFallback
19
22
  from tableio.tableio import TableIO
20
23
  from tableio.tableio_types import FileAccess
21
24
 
@@ -111,6 +114,16 @@ def _best_default_names(capabilities: Capabilities, format_name: Optional[str],
111
114
  _config_error(name, 'does not match registered TableIO backends.')
112
115
 
113
116
 
117
+ def _paper_size(value: Optional[str]) -> Optional[PaperSize]:
118
+ """Return a validated paper size text as the backend enum value."""
119
+ return None if value is None else PaperSize.from_str(value)
120
+
121
+
122
+ def _table_alignment(value: Optional[str]) -> Optional[TableAlignment]:
123
+ """Return a validated table alignment text as the backend enum value."""
124
+ return None if value is None else TableAlignment[value.upper()]
125
+
126
+
114
127
  def _base_arg_items(config: ConfigData) -> list[tuple[str, str, object]]:
115
128
  """Return configured top-level optional argument values."""
116
129
  return [
@@ -118,11 +131,14 @@ def _base_arg_items(config: ConfigData) -> list[tuple[str, str, object]]:
118
131
  config.character_encoding),
119
132
  ('language', 'lang', config.language),
120
133
  ('title', 'title', config.title),
121
- ('paper_size', 'paper_size', config.paper_size),
134
+ ('paper_size', 'paper_size', _paper_size(config.paper_size)),
122
135
  ('line_length', 'line_length', config.line_length),
123
136
  ('table_max_line_length', 'table_max_line_length',
124
137
  config.table_max_line_length),
125
- ('table_alignment', 'table_alignment', config.table_alignment)
138
+ ('table_alignment', 'table_alignment',
139
+ _table_alignment(config.table_alignment)),
140
+ ('timedelta_fallback', 'timedelta_fallback',
141
+ config.timedelta_fallback)
126
142
  ]
127
143
 
128
144
 
@@ -201,7 +217,8 @@ def _all_option_config(format_name: str,
201
217
  title='HTML file', paper_size='A4', line_length=79,
202
218
  table_max_line_length=140,
203
219
  table_alignment='CENTER_BUT_DIGITS_RIGHT', csv=csv,
204
- html=html, latex=latex)
220
+ html=html, latex=latex,
221
+ timedelta_fallback=TimeDeltaFallback.HMS_STRING)
205
222
 
206
223
 
207
224
  def _include_default_impl(implementation: Optional[str],
@@ -306,6 +323,8 @@ def tio_config_create(
306
323
  TableIO:
307
324
  """Create a TableIO object from configuration and runtime values.
308
325
 
326
+ The implementation is selected using the capabilities together with the
327
+ capabilities implied by file_access, as done by the validation.
309
328
  Args:
310
329
  config: Durable configuration data.
311
330
  file_name: Runtime file name to open.
@@ -317,16 +336,18 @@ def tio_config_create(
317
336
  """
318
337
  tio_config_validate(config, capabilities=capabilities,
319
338
  file_access=file_access)
339
+ match_caps = add_access_capabilities(file_access,
340
+ capabilities or Capabilities())
320
341
  extra_args = None
321
342
  if file_exists_callback is not None:
322
343
  extra_args = cast(OptionalArgsDict, {
323
344
  'file_exists_callback': file_exists_callback
324
345
  })
325
- args = _filtered_args(config, capabilities, extra_args)
346
+ args = _filtered_args(config, match_caps, extra_args)
326
347
  return create_tableio(format_name=config.format_name, file_name=file_name,
327
348
  file_access=file_access, args=args,
328
349
  implementation=config.implementation,
329
- capabilities=capabilities)
350
+ capabilities=match_caps)
330
351
 
331
352
 
332
353
  def tio_config_ignored_names(config: ConfigData,
@@ -386,6 +407,9 @@ def tio_config_trim(config: ConfigData,
386
407
  filtered_names),
387
408
  table_alignment=_kept(config.table_alignment,
388
409
  'table_alignment', filtered_names),
410
+ timedelta_fallback=_kept(config.timedelta_fallback,
411
+ 'timedelta_fallback',
412
+ filtered_names),
389
413
  csv=csv, html=html, latex=latex)
390
414
 
391
415
 
@@ -13,7 +13,7 @@ from mformat.plain_text_table import TableAlignment
13
13
 
14
14
  from tableio.factory import list_implementations_tableio, \
15
15
  list_registered_tableio, usage_tableio
16
- from tableio.optional_args import CsvDialect
16
+ from tableio.optional_args import CsvDialect, TimeDeltaFallback
17
17
 
18
18
 
19
19
  @dataclass
@@ -58,6 +58,11 @@ def _csv_dialect_choices() -> tuple[str, ...]:
58
58
  return tuple(member.name for member in CsvDialect)
59
59
 
60
60
 
61
+ def _timedelta_fallback_choices() -> tuple[str, ...]:
62
+ """Return advertised timedelta fallback choices."""
63
+ return tuple(member.name for member in TimeDeltaFallback)
64
+
65
+
61
66
  def _table_alignment_choices() -> tuple[str, ...]:
62
67
  """Return advertised plain text table alignment choices."""
63
68
  return tuple(member.name for member in TableAlignment)
@@ -147,6 +152,13 @@ def tio_config_specs() -> dict[str, ConfigSpec]:
147
152
  default_text='None means backend default.',
148
153
  choices=_table_alignment_choices(),
149
154
  optional_arg='table_alignment')),
155
+ _arg_spec(ConfigSpec(name='timedelta_fallback',
156
+ description='How timedelta values are written '
157
+ 'by formats without native timedelta support.',
158
+ value_type='Optional[TimeDeltaFallback]',
159
+ default_text='None means backend default.',
160
+ choices=_timedelta_fallback_choices(),
161
+ optional_arg='timedelta_fallback')),
150
162
  _arg_spec(ConfigSpec(name='csv.dialect',
151
163
  description='The CSV dialect template.',
152
164
  value_type='Optional[CsvDialect]',
@@ -17,7 +17,7 @@ from tableio.config_data_error import ConfigError, ConfigIssue
17
17
  from tableio.factory import TableIOFactoryNoCapabilityMatch, \
18
18
  TableIOFactoryNoSuchError, filter_args_tableio, \
19
19
  list_implementations_tableio
20
- from tableio.optional_args import CsvDialect
20
+ from tableio.optional_args import CsvDialect, TimeDeltaFallback
21
21
  from tableio.tableio_types import FileAccess
22
22
 
23
23
 
@@ -156,6 +156,10 @@ def _validate_top_values(config: ConfigData, specs: dict[str, ConfigSpec],
156
156
  _validate_int_min(issues, 'table_max_line_length',
157
157
  config.table_max_line_length, 10, True)
158
158
  _valid_choice(config.table_alignment, specs['table_alignment'], issues)
159
+ if config.timedelta_fallback is not None and \
160
+ not isinstance(config.timedelta_fallback, TimeDeltaFallback):
161
+ _add_issue(issues, 'timedelta_fallback',
162
+ 'must be a TimeDeltaFallback value or None.')
159
163
 
160
164
 
161
165
  def _validate_csv(config: ConfigData, specs: dict[str, ConfigSpec],
@@ -6,7 +6,6 @@
6
6
  #
7
7
 
8
8
  from typing import Optional, NamedTuple
9
- from functools import total_ordering
10
9
  import warnings
11
10
  from mformat.mformat import PathLike
12
11
  from tableio.access_capability import check_access_capabilities
@@ -53,7 +52,6 @@ class TableIOFactoryNoCapabilityMatch(ValueError):
53
52
  """
54
53
 
55
54
 
56
- @total_ordering
57
55
  class ImplPrio(NamedTuple):
58
56
  """Priority of an implementation."""
59
57
 
@@ -66,25 +64,43 @@ class ImplPrio(NamedTuple):
66
64
  priority: int
67
65
  """The priority of the implementation."""
68
66
 
67
+ def sort_key(self) -> tuple[int, str, str]:
68
+ """Return the ordering key: priority, format name, implementation.
69
+
70
+ All comparison operators are defined from this key, as the
71
+ inherited tuple comparisons use the field order instead.
72
+ """
73
+ return (self.priority, self.format_name, self.implementation)
74
+
69
75
  def __lt__(self, other: object) -> bool:
70
76
  """Compare two implementation priorities."""
71
77
  if not isinstance(other, ImplPrio):
72
78
  return NotImplemented
73
- if self.priority < other.priority:
74
- return True
75
- if self.priority > other.priority:
76
- return False
77
- if self.format_name < other.format_name:
78
- return True
79
- if self.format_name > other.format_name:
80
- return False
81
- return self.implementation < other.implementation
79
+ return self.sort_key() < other.sort_key()
80
+
81
+ def __le__(self, other: object) -> bool:
82
+ """Compare two implementation priorities."""
83
+ if not isinstance(other, ImplPrio):
84
+ return NotImplemented
85
+ return self.sort_key() <= other.sort_key()
86
+
87
+ def __gt__(self, other: object) -> bool:
88
+ """Compare two implementation priorities."""
89
+ if not isinstance(other, ImplPrio):
90
+ return NotImplemented
91
+ return self.sort_key() > other.sort_key()
92
+
93
+ def __ge__(self, other: object) -> bool:
94
+ """Compare two implementation priorities."""
95
+ if not isinstance(other, ImplPrio):
96
+ return NotImplemented
97
+ return self.sort_key() >= other.sort_key()
82
98
 
83
99
  def __eq__(self, other: object) -> bool:
84
100
  """Compare two implementation priorities."""
85
101
  if not isinstance(other, ImplPrio):
86
102
  return NotImplemented
87
- return not self < other and not other < self
103
+ return self.sort_key() == other.sort_key()
88
104
 
89
105
 
90
106
  class BestMatch(NamedTuple):
@@ -0,0 +1,143 @@
1
+ #! /usr/bin/env python3
2
+ """Optional arguments for the tableio package."""
3
+
4
+ # Copyright (c) 2026 Tom Björkholm
5
+ # MIT License
6
+
7
+ from typing import Optional, cast
8
+ from enum import IntEnum, auto
9
+ from mformat.factory import OptArgsDict
10
+
11
+
12
+ class CsvDialect(IntEnum):
13
+ """The type of CSV file to write."""
14
+
15
+ EXCEL = auto()
16
+ """Excel CSV file type/dialect."""
17
+ UNIX = auto()
18
+ """Unix CSV file type/dialect."""
19
+
20
+
21
+ class TimeDeltaFallback(IntEnum):
22
+ """The fallback behavior for timedelta values.
23
+
24
+ How a timedelta value is written when an implementation does not support
25
+ writing timedelta values. If an implementation does support timedelta
26
+ values, the fallback behavior specified by this enum will be ignored.
27
+
28
+ For all string formats: A negative value gets a leading '-' that applies
29
+ to the whole duration (for example '-01:00:00' is minus one hour).
30
+ Fractional seconds are written with up to six decimals, and only when
31
+ non-zero (for example '00:00:01.5'). The 'HH' part is always at least two
32
+ digits. Use ``tableio.parse_timedelta`` to read the values back.
33
+ """
34
+
35
+ FLOATSECONDS = auto()
36
+ """Convert the timedelta to a float of the total number of seconds.
37
+
38
+ Microsecond precision is lost for very large durations (roughly
39
+ above 100 years), as a float has limited precision.
40
+ """
41
+
42
+ HMS_STRING = auto()
43
+ """Convert the timedelta to a string in the format 'HH:MM:SS'.
44
+
45
+ Days are included in the hours, so 'HH' may exceed 24 (for example
46
+ '26:03:04').
47
+ This is the default fallback.
48
+ """
49
+
50
+ DHMS_STRING = auto()
51
+ """Convert the timedelta to a string in the format 'D d HH:MM:SS'.
52
+
53
+ The 'D d' part will only be included if the number of days is non-zero.
54
+ """
55
+
56
+ DHMS_STRING_LONG = auto()
57
+ """Convert the timedelta to a string in the format 'D days HH:MM:SS'.
58
+
59
+ The 'D days' part will only be included if the number of days is non-zero.
60
+ If the number of days is one, it will be written as '1 day'.
61
+ """
62
+
63
+ WDHMS_STRING = auto()
64
+ """Convert the timedelta to a string in the format 'W w D d HH:MM:SS'.
65
+
66
+ The 'W w' part will only be included if the number of weeks is non-zero.
67
+ The 'D d' part will only be included if the number of days is non-zero.
68
+ """
69
+
70
+ WDHMS_STRING_LONG = auto()
71
+ """Convert the timedelta to a string as 'W weeks D days HH:MM:SS'.
72
+
73
+ The 'W weeks' part will only be included if the number of weeks is
74
+ non-zero. If the number of weeks is one, it will be written as '1 week'.
75
+ The 'D days' part will only be included if the number of days is non-zero.
76
+ If the number of days is one, it will be written as '1 day'.
77
+ """
78
+
79
+
80
+ class OptionalArgsDict(OptArgsDict, total=False):
81
+ """Optional arguments for the tableio package.
82
+
83
+ This is a TypedDict that describes the optional arguments that can be
84
+ passed to the factory in the tableio package.
85
+ For description of the arguments, see the class derived from TableIO
86
+ that uses the arguments.
87
+ The possible optional arguments includ the arguments in
88
+ mformat.factory.OptArgsDict plus the arguments specific to the tableio
89
+ package.
90
+ """
91
+
92
+ csv_dialect: Optional[CsvDialect]
93
+ """The type/dialect of CSV file to write. None for default type."""
94
+
95
+ csv_delimiter: Optional[str]
96
+ """The delimiter to use for CSV files. None for default delimiter."""
97
+
98
+ csv_quoting: Optional[str]
99
+ """The quoting style to use for CSV files.
100
+
101
+ Allowed values (case-insensitive): 'all', 'minimal',
102
+ 'nonnumeric', 'none', 'strings', 'notnull'.
103
+ None for default quoting."""
104
+
105
+ csv_quotechar: Optional[str]
106
+ """The quote character to use for CSV files. None for default."""
107
+
108
+ csv_lineterminator: Optional[str]
109
+ """The line terminator to use for CSV files. None for default."""
110
+
111
+ csv_escapechar: Optional[str]
112
+ """The escape character to use for CSV files. None for default."""
113
+
114
+ timedelta_fallback: Optional[TimeDeltaFallback]
115
+ """The fallback format to use for timedelta, when no native support.
116
+
117
+ Silently ignored if the implementation provides native support for
118
+ timedelta values. None for default (TimeDeltaFallback.HMS_STRING).
119
+ """
120
+
121
+
122
+ type OptionalArgs = Optional[OptionalArgsDict]
123
+ """Optional arguments for the factory in the tableio package."""
124
+
125
+ _MFORMAT_OPTARG_NAMES: set[str] = set(OptArgsDict.__annotations__)
126
+
127
+
128
+ def mformat_optargs_from_optionalargs(optional_args: OptionalArgs) \
129
+ -> Optional[OptArgsDict]:
130
+ """Convert the optional arguments to a dictionary of arguments for mformat.
131
+
132
+ Args:
133
+ optional_args: The optional arguments to convert.
134
+ Returns:
135
+ A dictionary of arguments for mformat.
136
+ """
137
+ if optional_args is None:
138
+ return None
139
+ ret = {
140
+ key: value for key, value in optional_args.items()
141
+ if key in _MFORMAT_OPTARG_NAMES and value is not None
142
+ }
143
+ return cast(OptArgsDict, ret)