TSVZ 4.7__tar.gz → 4.8__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TSVZ
3
- Version: 4.7
3
+ Version: 4.8
4
4
  Summary: Key-value store kept in a TSV / CSV / NSV / PSV file, with tsvz-spec-v1 support
5
5
  Home-page: https://github.com/yufei-pan/TSVZ
6
6
  Author: Yufei Pan
@@ -34,7 +34,7 @@ key-value store in a delimiter-separated text file**. The first column of each
34
34
  row is the key; the file behaves like an ordered dictionary that is persisted to
35
35
  disk as you change it.
36
36
 
37
- **Version 4.7**, Python **3.6+**, one self-contained module (`TSVZ.py`): drop it
37
+ **Version 4.8**, Python **3.6+**, one self-contained module (`TSVZ.py`): drop it
38
38
  next to your code and `import TSVZ`, or install it.
39
39
 
40
40
  ```bash
@@ -258,7 +258,7 @@ and `-d=,`, except the ones C5 and C9 list.
258
258
  | C3 | `read` and `scrub` of a missing store exit 1 with a message (3.39: exit 0, and `read` printed "File not found" on stdout). |
259
259
  | C4 | `delete` deletes every KEY it is given (3.39: only the first). |
260
260
  | C5 | `append` or `delete` without a KEY, or with an empty KEY, is a usage error with exit 2 (3.39 wrote an empty line). |
261
- | C6 | A `clear` or `scrub` that writes nothing (one part of a multi-part store, a store that kept changing) exits 1. |
261
+ | C6 | A `clear` or `scrub` that writes nothing (one part of a multi-part store, a store that kept changing, a part that declares a spec version newer than 1, per spec §19.4 and §19.8) exits 1. |
262
262
  | C7 | `-c/--header`, `--defaults`, `-s/--strict` and `-f/--force` are undocumented aliases of the `--x-` options. `--x-header` and `--x-defaults` keep non-ASCII text. |
263
263
  | C8 | Usage errors print tsvz's own message, exit 2; the `-h` text is new. |
264
264
  | C9 | Arguments after STORE for `read`, `clear` and `scrub` are a usage error, exit 2 (3.39 ignored them, so `clear junk` cleared the file). |
@@ -7,7 +7,7 @@ key-value store in a delimiter-separated text file**. The first column of each
7
7
  row is the key; the file behaves like an ordered dictionary that is persisted to
8
8
  disk as you change it.
9
9
 
10
- **Version 4.7**, Python **3.6+**, one self-contained module (`TSVZ.py`): drop it
10
+ **Version 4.8**, Python **3.6+**, one self-contained module (`TSVZ.py`): drop it
11
11
  next to your code and `import TSVZ`, or install it.
12
12
 
13
13
  ```bash
@@ -231,7 +231,7 @@ and `-d=,`, except the ones C5 and C9 list.
231
231
  | C3 | `read` and `scrub` of a missing store exit 1 with a message (3.39: exit 0, and `read` printed "File not found" on stdout). |
232
232
  | C4 | `delete` deletes every KEY it is given (3.39: only the first). |
233
233
  | C5 | `append` or `delete` without a KEY, or with an empty KEY, is a usage error with exit 2 (3.39 wrote an empty line). |
234
- | C6 | A `clear` or `scrub` that writes nothing (one part of a multi-part store, a store that kept changing) exits 1. |
234
+ | C6 | A `clear` or `scrub` that writes nothing (one part of a multi-part store, a store that kept changing, a part that declares a spec version newer than 1, per spec §19.4 and §19.8) exits 1. |
235
235
  | C7 | `-c/--header`, `--defaults`, `-s/--strict` and `-f/--force` are undocumented aliases of the `--x-` options. `--x-header` and `--x-defaults` keep non-ASCII text. |
236
236
  | C8 | Usage errors print tsvz's own message, exit 2; the `-h` text is new. |
237
237
  | C9 | Arguments after STORE for `read`, `clear` and `scrub` are a usage error, exit 2 (3.39 ignored them, so `clear junk` cleared the file). |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TSVZ
3
- Version: 4.7
3
+ Version: 4.8
4
4
  Summary: Key-value store kept in a TSV / CSV / NSV / PSV file, with tsvz-spec-v1 support
5
5
  Home-page: https://github.com/yufei-pan/TSVZ
6
6
  Author: Yufei Pan
@@ -34,7 +34,7 @@ key-value store in a delimiter-separated text file**. The first column of each
34
34
  row is the key; the file behaves like an ordered dictionary that is persisted to
35
35
  disk as you change it.
36
36
 
37
- **Version 4.7**, Python **3.6+**, one self-contained module (`TSVZ.py`): drop it
37
+ **Version 4.8**, Python **3.6+**, one self-contained module (`TSVZ.py`): drop it
38
38
  next to your code and `import TSVZ`, or install it.
39
39
 
40
40
  ```bash
@@ -258,7 +258,7 @@ and `-d=,`, except the ones C5 and C9 list.
258
258
  | C3 | `read` and `scrub` of a missing store exit 1 with a message (3.39: exit 0, and `read` printed "File not found" on stdout). |
259
259
  | C4 | `delete` deletes every KEY it is given (3.39: only the first). |
260
260
  | C5 | `append` or `delete` without a KEY, or with an empty KEY, is a usage error with exit 2 (3.39 wrote an empty line). |
261
- | C6 | A `clear` or `scrub` that writes nothing (one part of a multi-part store, a store that kept changing) exits 1. |
261
+ | C6 | A `clear` or `scrub` that writes nothing (one part of a multi-part store, a store that kept changing, a part that declares a spec version newer than 1, per spec §19.4 and §19.8) exits 1. |
262
262
  | C7 | `-c/--header`, `--defaults`, `-s/--strict` and `-f/--force` are undocumented aliases of the `--x-` options. `--x-header` and `--x-defaults` keep non-ASCII text. |
263
263
  | C8 | Usage errors print tsvz's own message, exit 2; the `-h` text is new. |
264
264
  | C9 | Arguments after STORE for `read`, `clear` and `scrub` are a usage error, exit 2 (3.39 ignored them, so `clear junk` cleared the file). |
@@ -40,10 +40,10 @@ if os.name == 'nt':
40
40
  elif os.name == 'posix':
41
41
  import fcntl
42
42
 
43
- version = '4.7'
43
+ version = '4.8'
44
44
  __version__ = version
45
45
  author = 'pan@zopyr.us'
46
- COMMIT_DATE = '2026-10-13'
46
+ COMMIT_DATE = '2026-10-14'
47
47
 
48
48
  DEFAULT_DELIMITER = '\t'
49
49
  DEFAULTS_INDICATOR_KEY = '#_defaults_#'
@@ -1608,7 +1608,7 @@ _MARKER_RE = re.compile(r'^#_[A-Za-z0-9_-]+_#$')
1608
1608
  #: What a lookup gets for a key that is not live (spec §20.2.7: then the stripped key is tried).
1609
1609
  _NO_ROW = object()
1610
1610
  _CHECKSUM_RE = re.compile(r'^#_checksum_([A-Za-z0-9_-]+)_#$', re.IGNORECASE)
1611
- _ASCII_DIGITS_RE = re.compile(r'^[0-9]+$')
1611
+ _ASCII_DIGITS_RE = re.compile(r'^[0-9]+\Z') # \Z: '$' would also match before a final newline
1612
1612
  _SPEC_DECODE_RE = re.compile(r'<(?:sep|LF|CR|NUL|lt|#)>')
1613
1613
  _SPEC_TOKENS = {'<LF>': '\n', '<CR>': '\r', '<NUL>': '\0', '<lt>': '<', '<#>': '#'}
1614
1614
  _RowState = namedtuple('_RowState', 'defaults strip fillEmpty')
@@ -1742,6 +1742,7 @@ class _SpecState(object):
1742
1742
 
1743
1743
  def __init__(self, defaults=None):
1744
1744
  self.version = 1
1745
+ self.newestVersion = 1 # the highest version a #_version_# line declared (spec §19.4, §19.8)
1745
1746
  self.defaults = list(defaults) if defaults and len(defaults) > 1 else [DEFAULTS_INDICATOR_KEY]
1746
1747
  self.strip = True
1747
1748
  self.fillEmpty = False
@@ -1786,16 +1787,17 @@ def _specApplyMarker(state, keyLower, values, reporter, where):
1786
1787
  else:
1787
1788
  number = 0
1788
1789
  if _ASCII_DIGITS_RE.match(value):
1789
- try:
1790
- number = int(value)
1791
- except ValueError: # more digits than int() converts (Python 3.11+ limit)
1792
- pass
1790
+ digits = value.lstrip('0') or '0'
1791
+ # int() refuses very long values (Python 3.11+); any such value is above every implemented version.
1792
+ number = int(digits) if len(digits) <= 64 else MAX_SPEC_VERSION + 1
1793
1793
  if number < 1:
1794
1794
  reporter.note('bad-marker', where, 'ignored #_version_# with invalid value {!r}'.format(value))
1795
1795
  return False
1796
1796
  if number > MAX_SPEC_VERSION:
1797
- reporter.note('newer-version', where, 'declares spec version {}; read as version {}'.format(value, MAX_SPEC_VERSION))
1797
+ shown = value if len(value) <= 20 else '{}… ({} digits)'.format(value[:20], len(value))
1798
+ reporter.note('newer-version', where, 'declares spec version {}; read as version {}'.format(shown, MAX_SPEC_VERSION))
1798
1799
  state.version = number
1800
+ state.newestVersion = max(state.newestVersion, number)
1799
1801
  elif keyLower in _BOOL_MARKERS:
1800
1802
  attr, default = _BOOL_MARKERS[keyLower]
1801
1803
  if not value:
@@ -3040,7 +3042,8 @@ def _specClearTabularFile(fileName, teeLogger=None, header='', verifyHeader=Fals
3040
3042
  A single file is read without a lock and rewritten in place only if no
3041
3043
  other writer changed it since the read (else it is read again, up to
3042
3044
  ``_SPEC_REWRITE_ATTEMPTS`` times). A path naming one part of a multi-part
3043
- store is refused.
3045
+ store is refused, and so is a single file declaring a spec version newer
3046
+ than ``MAX_SPEC_VERSION``.
3044
3047
  """
3045
3048
  reporter = _Reporter(fileName, teeLogger)
3046
3049
  attempt = None
@@ -3074,6 +3077,10 @@ def _specClearTabularFile(fileName, teeLogger=None, header='', verifyHeader=Fals
3074
3077
  break
3075
3078
  if stamp is None or load.parts != parts:
3076
3079
  continue # the set of parts changed under the read
3080
+ if load.state.newestVersion > MAX_SPEC_VERSION:
3081
+ attempt.note('refused', None, 'clear skipped: the part declares a spec version newer than {} (#_version_#), whose '
3082
+ 'markers this TSVZ cannot keep (spec §19.8); nothing was written'.format(MAX_SPEC_VERSION))
3083
+ return False
3077
3084
  if any(header) and verifyHeader and load.headerLine is not None and not headerChecked:
3078
3085
  headerChecked = True
3079
3086
  if not _lineContainHeader(header, load.headerLine[1:], verbose=verbose, teeLogger=teeLogger, strict=strict, delimiter=delimiter):
@@ -3156,8 +3163,8 @@ def _specScrub(fileName, teeLogger=None, header='', createIfNotExist=False, veri
3156
3163
  writer changed it since the read (else it is read again, up to
3157
3164
  ``_SPEC_REWRITE_ATTEMPTS`` times), so a record committed meanwhile is
3158
3165
  never compacted away. ``done`` is False when nothing was written because
3159
- the store is missing, has several parts (or the path names one part), or
3160
- kept changing. It is None when the read stopped at damaged data (spec
3166
+ the store is missing, has several parts (or the path names one part),
3167
+ declares a spec version newer than ``MAX_SPEC_VERSION``, or kept changing. It is None when the read stopped at damaged data (spec
3161
3168
  §4.10): the part was rewritten from what was read, the original kept.
3162
3169
  """
3163
3170
  if taskDic is None:
@@ -3193,6 +3200,11 @@ def _specScrub(fileName, teeLogger=None, header='', createIfNotExist=False, veri
3193
3200
  return taskDic, False
3194
3201
  if stamp is None or load.parts != parts:
3195
3202
  continue # the set of parts changed under the read
3203
+ if load.state.newestVersion > MAX_SPEC_VERSION:
3204
+ attempt.note('refused', None, 'scrub skipped: the store declares a spec version newer than {0} (#_version_#), which '
3205
+ 'this TSVZ reads as version {0} only (spec §6.3); a rewrite would make that reading permanent; '
3206
+ 'nothing was written'.format(MAX_SPEC_VERSION))
3207
+ return taskDic, False
3196
3208
  info = load.infos[0]
3197
3209
  if (info.lost or info.damaged) and backedUp != stamp:
3198
3210
  # Damaged data is not in what is written, or only unverified (spec §4.10, §16.6): keep the original
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes