TSVZ 4.5__tar.gz → 4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TSVZ
3
- Version: 4.5
3
+ Version: 4.7
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.5**, Python **3.6+**, one self-contained module (`TSVZ.py`): drop it
37
+ **Version 4.7**, 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
@@ -224,6 +224,7 @@ delimiter or encoding argument that conflicts with the extension.
224
224
  | L4 | New warnings on stderr (invalid encoding, lines dropped by `strict`); data unchanged. |
225
225
  | L5 | A damaged compressed file is read up to the damage instead of raising. |
226
226
  | L6 | `mapToFile` no longer glues a row it adds at the end of the file to the next one. |
227
+ | L7 | `TSVZed`'s `in` and `get` find a key as `t[key]` does: the live key equal to it, else the key right-stripped. 3.39 matched them exactly, so `'x ' in t` was false while `t['x ']` read `x`, and `t.setdefault('x ', …)` overwrote `x`. |
227
228
 
228
229
  ### `.tsvz`, `.csvz`, `.nsvz`, `.psvz`: now tsvz-spec-v1
229
230
 
@@ -243,6 +244,7 @@ delimiter or encoding argument that conflicts with the extension.
243
244
  | S12 | `TSVZedLite.clear()` and `clear_file()` raise `OSError` when the clear is refused (a part could not be read, the path names one part of a store, or the store kept changing). Memory is reloaded from the unchanged store first. |
244
245
  | S13 | Compressed parts are written in members of at most 1 MiB of records (`#_member_size_#`), Zstandard frames with a checksum; an append never reads or repairs the part. |
245
246
  | S14 | A zero byte in a value is written `<NUL>`, and a raw zero byte in a record is read as zero-fill damage (spec §4.9): under the default `recover` only the bytes after it are read. **A value that 4.2 or 4.3 wrote with a raw zero byte therefore reads differently in 4.4.** Such values are rare; before upgrading, `grep -c -P '\x00' store.tsvz` (any `.tsvz`, `.csvz` or `.psvz` part) finds them. Stop 4.2 and 4.3 handlers before upgrading: they read `<NUL>` as text, as they read `<CR>`. |
247
+ | S15 | A lookup (`t[key]`, `in`, `get`, `del`, `pop`, `setdefault`) takes the live key equal to the one given before the right-stripped one, so a key ending in whitespace (written while `#_strip_trailing_whites_#` was false) can be read and deleted by name (spec §20.2.7). Setting a key still right-strips it. |
246
248
 
247
249
  ### The `tsvz` command
248
250
 
@@ -262,6 +264,7 @@ and `-d=,`, except the ones C5 and C9 list.
262
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). |
263
265
  | C10 | The table (a terminal, or `--format table`) of `get`, `keys`, `pop`, `popitem`, `setdefault`, and of `read` on a `.tsvz`-family store, shows the rows only: no row is drawn as a header, and no row is cut to the first row's or the terminal's width. `read` of a `.tsv`-family file keeps 3.39's table, whose first row is the file's header line. |
264
266
  | C11 | The table shows control characters, bidi controls and line separators as escapes (`\r`, `\n`, `\t`, `\xNN`, `\uNNNN`), so no value can move the cursor, reorder the line or restyle the terminal. It sizes columns in terminal columns, so CJK text (two columns a character) lines up. The table is for reading: a backslash in a value is shown as is, so use records for exact values. |
267
+ | C12 | While stripping is on, `get`, `has`, `pop`, `delete` and `setdefault` take the live key equal to a KEY ending in a space or tab before the stripped one (spec §20.2.7), so `tsvz keys s \| tsvz delete s -` deletes every key. `delete` of such a KEY reads the store first, and exits 1 without writing when the store cannot be read whole. |
265
268
 
266
269
  ## Spec deviations and interpretations
267
270
 
@@ -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.5**, Python **3.6+**, one self-contained module (`TSVZ.py`): drop it
10
+ **Version 4.7**, 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
@@ -197,6 +197,7 @@ delimiter or encoding argument that conflicts with the extension.
197
197
  | L4 | New warnings on stderr (invalid encoding, lines dropped by `strict`); data unchanged. |
198
198
  | L5 | A damaged compressed file is read up to the damage instead of raising. |
199
199
  | L6 | `mapToFile` no longer glues a row it adds at the end of the file to the next one. |
200
+ | L7 | `TSVZed`'s `in` and `get` find a key as `t[key]` does: the live key equal to it, else the key right-stripped. 3.39 matched them exactly, so `'x ' in t` was false while `t['x ']` read `x`, and `t.setdefault('x ', …)` overwrote `x`. |
200
201
 
201
202
  ### `.tsvz`, `.csvz`, `.nsvz`, `.psvz`: now tsvz-spec-v1
202
203
 
@@ -216,6 +217,7 @@ delimiter or encoding argument that conflicts with the extension.
216
217
  | S12 | `TSVZedLite.clear()` and `clear_file()` raise `OSError` when the clear is refused (a part could not be read, the path names one part of a store, or the store kept changing). Memory is reloaded from the unchanged store first. |
217
218
  | S13 | Compressed parts are written in members of at most 1 MiB of records (`#_member_size_#`), Zstandard frames with a checksum; an append never reads or repairs the part. |
218
219
  | S14 | A zero byte in a value is written `<NUL>`, and a raw zero byte in a record is read as zero-fill damage (spec §4.9): under the default `recover` only the bytes after it are read. **A value that 4.2 or 4.3 wrote with a raw zero byte therefore reads differently in 4.4.** Such values are rare; before upgrading, `grep -c -P '\x00' store.tsvz` (any `.tsvz`, `.csvz` or `.psvz` part) finds them. Stop 4.2 and 4.3 handlers before upgrading: they read `<NUL>` as text, as they read `<CR>`. |
220
+ | S15 | A lookup (`t[key]`, `in`, `get`, `del`, `pop`, `setdefault`) takes the live key equal to the one given before the right-stripped one, so a key ending in whitespace (written while `#_strip_trailing_whites_#` was false) can be read and deleted by name (spec §20.2.7). Setting a key still right-strips it. |
219
221
 
220
222
  ### The `tsvz` command
221
223
 
@@ -235,6 +237,7 @@ and `-d=,`, except the ones C5 and C9 list.
235
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). |
236
238
  | C10 | The table (a terminal, or `--format table`) of `get`, `keys`, `pop`, `popitem`, `setdefault`, and of `read` on a `.tsvz`-family store, shows the rows only: no row is drawn as a header, and no row is cut to the first row's or the terminal's width. `read` of a `.tsv`-family file keeps 3.39's table, whose first row is the file's header line. |
237
239
  | C11 | The table shows control characters, bidi controls and line separators as escapes (`\r`, `\n`, `\t`, `\xNN`, `\uNNNN`), so no value can move the cursor, reorder the line or restyle the terminal. It sizes columns in terminal columns, so CJK text (two columns a character) lines up. The table is for reading: a backslash in a value is shown as is, so use records for exact values. |
240
+ | C12 | While stripping is on, `get`, `has`, `pop`, `delete` and `setdefault` take the live key equal to a KEY ending in a space or tab before the stripped one (spec §20.2.7), so `tsvz keys s \| tsvz delete s -` deletes every key. `delete` of such a KEY reads the store first, and exits 1 without writing when the store cannot be read whole. |
238
241
 
239
242
  ## Spec deviations and interpretations
240
243
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TSVZ
3
- Version: 4.5
3
+ Version: 4.7
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.5**, Python **3.6+**, one self-contained module (`TSVZ.py`): drop it
37
+ **Version 4.7**, 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
@@ -224,6 +224,7 @@ delimiter or encoding argument that conflicts with the extension.
224
224
  | L4 | New warnings on stderr (invalid encoding, lines dropped by `strict`); data unchanged. |
225
225
  | L5 | A damaged compressed file is read up to the damage instead of raising. |
226
226
  | L6 | `mapToFile` no longer glues a row it adds at the end of the file to the next one. |
227
+ | L7 | `TSVZed`'s `in` and `get` find a key as `t[key]` does: the live key equal to it, else the key right-stripped. 3.39 matched them exactly, so `'x ' in t` was false while `t['x ']` read `x`, and `t.setdefault('x ', …)` overwrote `x`. |
227
228
 
228
229
  ### `.tsvz`, `.csvz`, `.nsvz`, `.psvz`: now tsvz-spec-v1
229
230
 
@@ -243,6 +244,7 @@ delimiter or encoding argument that conflicts with the extension.
243
244
  | S12 | `TSVZedLite.clear()` and `clear_file()` raise `OSError` when the clear is refused (a part could not be read, the path names one part of a store, or the store kept changing). Memory is reloaded from the unchanged store first. |
244
245
  | S13 | Compressed parts are written in members of at most 1 MiB of records (`#_member_size_#`), Zstandard frames with a checksum; an append never reads or repairs the part. |
245
246
  | S14 | A zero byte in a value is written `<NUL>`, and a raw zero byte in a record is read as zero-fill damage (spec §4.9): under the default `recover` only the bytes after it are read. **A value that 4.2 or 4.3 wrote with a raw zero byte therefore reads differently in 4.4.** Such values are rare; before upgrading, `grep -c -P '\x00' store.tsvz` (any `.tsvz`, `.csvz` or `.psvz` part) finds them. Stop 4.2 and 4.3 handlers before upgrading: they read `<NUL>` as text, as they read `<CR>`. |
247
+ | S15 | A lookup (`t[key]`, `in`, `get`, `del`, `pop`, `setdefault`) takes the live key equal to the one given before the right-stripped one, so a key ending in whitespace (written while `#_strip_trailing_whites_#` was false) can be read and deleted by name (spec §20.2.7). Setting a key still right-strips it. |
246
248
 
247
249
  ### The `tsvz` command
248
250
 
@@ -262,6 +264,7 @@ and `-d=,`, except the ones C5 and C9 list.
262
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). |
263
265
  | C10 | The table (a terminal, or `--format table`) of `get`, `keys`, `pop`, `popitem`, `setdefault`, and of `read` on a `.tsvz`-family store, shows the rows only: no row is drawn as a header, and no row is cut to the first row's or the terminal's width. `read` of a `.tsv`-family file keeps 3.39's table, whose first row is the file's header line. |
264
266
  | C11 | The table shows control characters, bidi controls and line separators as escapes (`\r`, `\n`, `\t`, `\xNN`, `\uNNNN`), so no value can move the cursor, reorder the line or restyle the terminal. It sizes columns in terminal columns, so CJK text (two columns a character) lines up. The table is for reading: a backslash in a value is shown as is, so use records for exact values. |
267
+ | C12 | While stripping is on, `get`, `has`, `pop`, `delete` and `setdefault` take the live key equal to a KEY ending in a space or tab before the stripped one (spec §20.2.7), so `tsvz keys s \| tsvz delete s -` deletes every key. `delete` of such a KEY reads the store first, and exits 1 without writing when the store cannot be read whole. |
265
268
 
266
269
  ## Spec deviations and interpretations
267
270
 
@@ -40,10 +40,10 @@ if os.name == 'nt':
40
40
  elif os.name == 'posix':
41
41
  import fcntl
42
42
 
43
- version = '4.5'
43
+ version = '4.7'
44
44
  __version__ = version
45
45
  author = 'pan@zopyr.us'
46
- COMMIT_DATE = '2026-10-12'
46
+ COMMIT_DATE = '2026-10-13'
47
47
 
48
48
  DEFAULT_DELIMITER = '\t'
49
49
  DEFAULTS_INDICATOR_KEY = '#_defaults_#'
@@ -179,13 +179,18 @@ def get_terminal_size():
179
179
  try:
180
180
  import os
181
181
  _tsize = os.get_terminal_size()
182
+ if not (_tsize[0] and _tsize[1]):
183
+ raise OSError('the terminal has no size set')
182
184
  except Exception:
183
185
  try:
184
186
  import fcntl
185
187
  import struct
186
188
  import termios
187
189
  packed = fcntl.ioctl(0, termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))
188
- _tsize = struct.unpack('HHHH', packed)[:2]
190
+ rows, columns = struct.unpack('HHHH', packed)[:2]
191
+ if not (rows and columns):
192
+ raise OSError('the terminal has no size set')
193
+ _tsize = (columns, rows)
189
194
  except Exception:
190
195
  import shutil
191
196
  _tsize = shutil.get_terminal_size(fallback=(240, 50))
@@ -1600,6 +1605,8 @@ def scrubTabularFile(fileName,teeLogger = None,header = '',createIfNotExist = Fa
1600
1605
  # ===========================================================================
1601
1606
  MAX_SPEC_VERSION = 1
1602
1607
  _MARKER_RE = re.compile(r'^#_[A-Za-z0-9_-]+_#$')
1608
+ #: What a lookup gets for a key that is not live (spec §20.2.7: then the stripped key is tried).
1609
+ _NO_ROW = object()
1603
1610
  _CHECKSUM_RE = re.compile(r'^#_checksum_([A-Za-z0-9_-]+)_#$', re.IGNORECASE)
1604
1611
  _ASCII_DIGITS_RE = re.compile(r'^[0-9]+$')
1605
1612
  _SPEC_DECODE_RE = re.compile(r'<(?:sep|LF|CR|NUL|lt|#)>')
@@ -1654,6 +1661,82 @@ def _specFormatRecord(cells, delimiter, marker=False):
1654
1661
  return delimiter.join([first] + [_specEncodeField(cell, delimiter) for cell in cells[1:]])
1655
1662
 
1656
1663
 
1664
+ def _specTombstones(keys, delimiter, strip):
1665
+ """``(cells, marker)`` records that delete ``keys`` (spec §9.2) from a store whose stripping is ``strip``.
1666
+
1667
+ While stripping is on a reader strips a tombstone's key too (spec §9.1): the
1668
+ tombstone of a key whose encoded form ends in a space or tab, stored while
1669
+ stripping was off, would delete the stripped key instead. A batch holding
1670
+ one is written with stripping off, and stripping is turned back on after it
1671
+ (spec §9.6).
1672
+ """
1673
+ rows = [([key], False) for key in keys]
1674
+ if strip and any(_specStrippable(key, delimiter) for key in keys):
1675
+ rows.insert(0, (['#_strip_trailing_whites_#', 'false'], True))
1676
+ rows.append((['#_strip_trailing_whites_#', 'true'], True))
1677
+ return rows
1678
+
1679
+
1680
+ def _specFormatRecords(rows, delimiter):
1681
+ """``rows`` (``[(cells, marker), ...]``) as record lines, each with its '\\n'."""
1682
+ return ''.join(_specFormatRecord(cells, delimiter, marker=marker) + '\n' for cells, marker in rows)
1683
+
1684
+
1685
+ def _specTombstoneLines(key, delimiter, strip):
1686
+ """The tombstone of ``key`` (spec §9.6) as lines joined by '\\n', without a last '\\n'."""
1687
+ return '\n'.join(_specFormatRecord(cells, delimiter, marker=marker)
1688
+ for cells, marker in _specTombstones([key], delimiter, strip))
1689
+
1690
+
1691
+ def _specStrippable(key, delimiter):
1692
+ """True when a reader that strips (spec §7.7) would change ``key``: its encoded form ends in a space or tab."""
1693
+ return key.endswith((' ', '\t')) and _specEncodeField(key, delimiter, isKey=True).endswith((' ', '\t'))
1694
+
1695
+
1696
+ def _specNameKey(key, delimiter, strip, live):
1697
+ """The key that a requested ``key`` names (spec §20.2.7) in a store whose stripping is ``strip``.
1698
+
1699
+ While stripping is on, a key a reader would strip names the live key equal
1700
+ to it (``live`` holds the live keys), else the key a reader resolves it to.
1701
+ """
1702
+ if not strip or key in live or not _specStrippable(key, delimiter):
1703
+ return key
1704
+ return _specDecodeField(_specEncodeField(key, delimiter, isKey=True).rstrip(' \t'), delimiter)
1705
+
1706
+
1707
+ def _specNamesKeys(rows, delimiter):
1708
+ """True when naming the keys of ``delete`` rows (``[(cells, marker), ...]``) needs the store (spec §20.2.7)."""
1709
+ return any(not marker and _specStrippable(cells[0], delimiter) for cells, marker in rows)
1710
+
1711
+
1712
+ def _specDeleteRows(rows, delimiter, strip, live):
1713
+ """The batch ``delete`` appends for ``rows`` (``[(cells, marker), ...]``, spec §20.2, §20.3).
1714
+
1715
+ Every key names a key of the store as it was before the batch, whose
1716
+ stripping was ``strip`` and whose live keys are ``live`` (§20.2.7). The
1717
+ tombstone of a key a reader would strip is written with stripping off
1718
+ (§9.6). A reset of ``#_strip_trailing_whites_#`` in the batch turns
1719
+ stripping back on (§10.3, §12.1).
1720
+ """
1721
+ batch = []
1722
+ named = strip
1723
+ guarded = False
1724
+ for cells, marker in rows:
1725
+ if marker:
1726
+ if cells[0].lower() == '#_strip_trailing_whites_#':
1727
+ strip, guarded = True, False
1728
+ batch.append((cells, marker))
1729
+ continue
1730
+ key = _specNameKey(cells[0], delimiter, named, live)
1731
+ if strip and not guarded and _specStrippable(key, delimiter):
1732
+ batch.append((['#_strip_trailing_whites_#', 'false'], True))
1733
+ guarded = True
1734
+ batch.append(([key], False))
1735
+ if guarded:
1736
+ batch.append((['#_strip_trailing_whites_#', 'true'], True))
1737
+ return batch
1738
+
1739
+
1657
1740
  class _SpecState(object):
1658
1741
  """Reader state of spec §12, carried across records and parts."""
1659
1742
 
@@ -2604,16 +2687,23 @@ def _specBackupPart(path, reporter):
2604
2687
  _lockFile(f)
2605
2688
  try:
2606
2689
  first = backup = '{}.damaged-{}'.format(path, time.strftime('%Y%m%dT%H%M%S'))
2690
+ # Readable by whoever could read the part, writable by its owner only, never setuid,
2691
+ # setgid, sticky or executable (the umask still applies on top).
2692
+ st = os.fstat(f.fileno())
2693
+ mode = st.st_mode & 0o644
2607
2694
  suffix = 0
2608
2695
  while True:
2609
2696
  try:
2610
- copy = open(backup, 'xb') # never overwrite an earlier backup
2697
+ # never overwrite an earlier backup; O_BINARY: no newline translation on Windows
2698
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, 'O_BINARY', 0)
2699
+ copy = os.fdopen(os.open(backup, flags, mode), 'wb')
2611
2700
  break
2612
2701
  except FileExistsError:
2613
2702
  suffix += 1
2614
2703
  backup = '{}.{}'.format(first, suffix)
2615
2704
  try:
2616
2705
  with copy:
2706
+ _matchOwner(copy.fileno(), st)
2617
2707
  shutil.copyfileobj(f, copy)
2618
2708
  copy.flush()
2619
2709
  os.fsync(copy.fileno())
@@ -2672,6 +2762,22 @@ def _specSnapshot(fileName):
2672
2762
  return parts, None
2673
2763
 
2674
2764
 
2765
+ def _matchOwner(fd, st):
2766
+ """Give a new copy the owner and group of the file ``st`` describes. Where that is not allowed,
2767
+ a copy left in another group loses its group bits, so no one reads it who could not read the file."""
2768
+ if not hasattr(os, 'fchown'):
2769
+ return
2770
+ for uid in (st.st_uid, -1):
2771
+ try:
2772
+ os.fchown(fd, uid, st.st_gid)
2773
+ return
2774
+ except OSError:
2775
+ pass
2776
+ created = os.fstat(fd)
2777
+ if created.st_gid != st.st_gid:
2778
+ os.fchmod(fd, created.st_mode & 0o7707) # what the umask left, without the group bits
2779
+
2780
+
2675
2781
  def _specRewriteInPlace(path, content, expect=None, memberSize=None):
2676
2782
  """Replace a part's bytes in place (same inode, spec §18.7 / §19.8).
2677
2783
 
@@ -2691,18 +2797,73 @@ def _specRewriteInPlace(path, content, expect=None, memberSize=None):
2691
2797
  st = os.fstat(f.fileno())
2692
2798
  if expect is not None and _specStamp(st) != expect:
2693
2799
  return None
2694
- if st.st_size == len(raw) and f.read() == raw:
2800
+ fd = f.fileno()
2801
+ # The whole part, for a failed rewrite to put back whenever it fails. Read unbuffered, as
2802
+ # the file is written below: a read-ahead in f would leave it a stale position to close at.
2803
+ old = _readFrom0(fd, st.st_size)
2804
+ if old == raw:
2695
2805
  return False
2696
- f.seek(0)
2697
- f.write(raw)
2698
- f.truncate()
2699
- f.flush()
2700
- os.fsync(f.fileno())
2806
+ try:
2807
+ _reserveSpace(fd, len(raw)) # spec §19.8: a full disk refuses the rewrite before any byte changes
2808
+ _writeFrom0(fd, raw)
2809
+ except BaseException as e:
2810
+ # Put the old bytes back rather than leave a half-rewritten part (or one a failed
2811
+ # reservation lengthened).
2812
+ try:
2813
+ _writeFrom0(fd, old)
2814
+ os.fsync(fd)
2815
+ except OSError as undo:
2816
+ damaged = '{}: the rewrite failed ({!r}) and the old bytes could not be put back ({}): the part is damaged'.format(
2817
+ path, e, undo)
2818
+ if isinstance(e, OSError):
2819
+ raise OSError(e.errno, damaged)
2820
+ _warn('TSVZ warning: ' + damaged) # an interrupt stays an interrupt
2821
+ raise
2822
+ os.fsync(fd)
2701
2823
  return True
2702
2824
  finally:
2825
+ try:
2826
+ _seekRawStart(f) # msvcrt unlocks from the raw position: the range it locked
2827
+ except OSError:
2828
+ pass
2703
2829
  _unlockFile(f)
2704
2830
 
2705
2831
 
2832
+ def _reserveSpace(fd, length):
2833
+ """Reserve ``length`` bytes from the start of the file before it is rewritten in place (spec
2834
+ §19.8). Where the platform or the file system cannot reserve space, nothing is reserved."""
2835
+ fallocate = getattr(os, 'posix_fallocate', None)
2836
+ if fallocate is None or length <= 0:
2837
+ return
2838
+ try:
2839
+ fallocate(fd, 0, length)
2840
+ except OSError as e:
2841
+ if e.errno not in (errno.EOPNOTSUPP, getattr(errno, 'ENOTSUP', errno.EOPNOTSUPP), errno.ENOSYS, errno.EINVAL):
2842
+ raise
2843
+
2844
+
2845
+ def _readFrom0(fd, length):
2846
+ """Up to ``length`` bytes from the start of the file (unbuffered)."""
2847
+ os.lseek(fd, 0, os.SEEK_SET)
2848
+ chunks = []
2849
+ while length > 0:
2850
+ chunk = os.read(fd, length)
2851
+ if not chunk:
2852
+ break
2853
+ chunks.append(chunk)
2854
+ length -= len(chunk)
2855
+ return b''.join(chunks)
2856
+
2857
+
2858
+ def _writeFrom0(fd, data):
2859
+ """Write ``data`` over the file from offset 0 and cut the file there (unbuffered)."""
2860
+ os.lseek(fd, 0, os.SEEK_SET)
2861
+ view = memoryview(data)
2862
+ while view:
2863
+ view = view[os.write(fd, view):]
2864
+ os.ftruncate(fd, len(data))
2865
+
2866
+
2706
2867
  def _specPreamble(state, delimiter, compensate=False, version=True):
2707
2868
  """Marker lines that re-create ``state`` (spec §19.2.3a): version, non-default markers, defaults last.
2708
2869
 
@@ -2905,7 +3066,7 @@ def _specClearTabularFile(fileName, teeLogger=None, header='', verifyHeader=Fals
2905
3066
  attempt.note('refused', None, 'clear skipped: a part of the store could not be read; nothing was written')
2906
3067
  return False
2907
3068
  if len(load.parts) > 1:
2908
- tombstones = ''.join(_specFormatRecord([key], delimiter) + '\n' for key in load.data)
3069
+ tombstones = _specFormatRecords(_specTombstones(load.data, delimiter, load.state.strip), delimiter)
2909
3070
  if tombstones:
2910
3071
  _specAppendPayload(load.active, tombstones.encode('utf-8'), attempt, memberSize=load.state.memberSize)
2911
3072
  attempt.note('multipart', None, 'cleared a multi-part store by appending tombstones; older parts are not compacted')
@@ -3517,14 +3678,37 @@ class TSVZed(OrderedDict):
3517
3678
  # else:
3518
3679
  # self.appendEvent.set()
3519
3680
 
3681
+ def _nameKey(self, key):
3682
+ """The key a requested ``key`` names: the live key equal to it, else ``key`` right-stripped (spec §20.2.7).
3683
+
3684
+ The fallback strips every trailing whitespace whatever the stripping
3685
+ marker says, as ``__setitem__`` strips every key it writes.
3686
+ """
3687
+ key = str(key)
3688
+ return key if OrderedDict.__contains__(self, key) else key.rstrip()
3689
+
3520
3690
  def __getitem__(self, key):
3521
- return super().__getitem__(str(key).rstrip())
3522
-
3691
+ key = str(key)
3692
+ row = dict.get(self, key, _NO_ROW) # one probe for the usual hit
3693
+ return row if row is not _NO_ROW else OrderedDict.__getitem__(self, key.rstrip())
3694
+
3695
+ def __contains__(self, key):
3696
+ # OrderedDict.setdefault asks this before it reads or writes, so it names keys as t[key] does.
3697
+ key = str(key)
3698
+ return dict.__contains__(self, key) or dict.__contains__(self, key.rstrip())
3699
+
3700
+ def get(self, key, default=None):
3701
+ key = str(key)
3702
+ row = dict.get(self, key, _NO_ROW)
3703
+ if row is _NO_ROW:
3704
+ row = dict.get(self, key.rstrip(), _NO_ROW)
3705
+ return default if row is _NO_ROW else row
3706
+
3523
3707
 
3524
3708
  def __delitem__(self,key):
3525
3709
  if self._spec:
3526
3710
  return self._specDelItem(key)
3527
- key = str(key).rstrip()
3711
+ key = self._nameKey(key)
3528
3712
  if key == DEFAULTS_INDICATOR_KEY:
3529
3713
  self.defaults = [DEFAULTS_INDICATOR_KEY]
3530
3714
  if self.verbose:
@@ -3558,8 +3742,8 @@ class TSVZed(OrderedDict):
3558
3742
  # the mapping by the caller, so self[key] must not be accessed.
3559
3743
  self.dirty = True
3560
3744
  if self._spec:
3561
- # Spec §9.2: a tombstone is the lone key, never padded.
3562
- self.appendQueue.append(_specFormatRecord([key], self.delimiter))
3745
+ # Spec §9.2: a tombstone is the lone key, never padded. One queue item, so it is committed whole.
3746
+ self.appendQueue.append(_specTombstoneLines(key, self.delimiter, self._specState.strip))
3563
3747
  return self
3564
3748
  if self.correctColumnNum > 1:
3565
3749
  emptyLine = [key]+['']*(self.correctColumnNum-1)
@@ -3628,7 +3812,7 @@ class TSVZed(OrderedDict):
3628
3812
  self.lastUpdateTime = get_time_ns()
3629
3813
 
3630
3814
  def _specDelItem(self, key):
3631
- key = str(key).rstrip()
3815
+ key = self._nameKey(key)
3632
3816
  if _MARKER_RE.match(key):
3633
3817
  # A lone marker key resets that marker (spec §12.1).
3634
3818
  self._specSetMarker(key, [key])
@@ -3781,7 +3965,15 @@ memoryOnly:{self.memoryOnly}
3781
3965
  return self.close()
3782
3966
 
3783
3967
  def popitem(self, last=True):
3784
- key, value = super().popitem(last)
3968
+ if sys.version_info >= (3, 11):
3969
+ key, value = OrderedDict.popitem(self, last)
3970
+ else:
3971
+ # Before Python 3.11 OrderedDict.popitem goes through self[key] and del self[key], which strip the key.
3972
+ if not OrderedDict.__len__(self):
3973
+ raise KeyError('dictionary is empty')
3974
+ key = next(OrderedDict.__reversed__(self)) if last else next(OrderedDict.__iter__(self))
3975
+ value = OrderedDict.__getitem__(self, key)
3976
+ OrderedDict.__delitem__(self, key)
3785
3977
  if not self.memoryOnly:
3786
3978
  self.__appendEmptyLine(key)
3787
3979
  self.lastUpdateTime = get_time_ns()
@@ -3795,12 +3987,14 @@ memoryOnly:{self.memoryOnly}
3795
3987
  is raised.
3796
3988
 
3797
3989
  '''
3798
- key = str(key).rstrip()
3799
- if key not in self:
3990
+ key = self._nameKey(key)
3991
+ if not OrderedDict.__contains__(self, key):
3800
3992
  if default is self.__marker:
3801
3993
  raise KeyError(key)
3802
3994
  return default
3803
- value = super().pop(key)
3995
+ # Not OrderedDict.pop: before Python 3.11 it goes through del self[key], which writes a tombstone too.
3996
+ value = OrderedDict.__getitem__(self, key)
3997
+ OrderedDict.__delitem__(self, key)
3804
3998
  if not self.memoryOnly:
3805
3999
  self.__appendEmptyLine(key)
3806
4000
  self.lastUpdateTime = get_time_ns()
@@ -3810,7 +4004,7 @@ memoryOnly:{self.memoryOnly}
3810
4004
  '''Move an existing element to the end (or beginning if last is false).
3811
4005
  Raise KeyError if the element does not exist.
3812
4006
  '''
3813
- key = str(key).rstrip()
4007
+ key = self._nameKey(key)
3814
4008
  super().move_to_end(key, last)
3815
4009
  if self._spec:
3816
4010
  _warnOnce(self, 'move_to_end', self._fileName,
@@ -4333,9 +4527,18 @@ class TSVZedLite(MutableMapping):
4333
4527
  """'tsvz' for tsvz-spec-v1 files (.tsvz/.csvz/.nsvz/.psvz), 'tsv' for 3.39-format files."""
4334
4528
  return 'tsvz' if self._spec else 'tsv'
4335
4529
 
4530
+ def _nameKey(self, key):
4531
+ """The key a requested ``key`` names: the live key equal to it, else ``key`` right-stripped (spec §20.2.7).
4532
+
4533
+ The fallback strips every trailing whitespace whatever the stripping
4534
+ marker says, as ``__setitem__`` strips every key it writes.
4535
+ """
4536
+ key = str(key)
4537
+ return key if key in self.indexes else key.rstrip()
4538
+
4336
4539
  def __contains__(self, key):
4337
4540
  if self._spec:
4338
- return str(key).rstrip() in self.indexes
4541
+ return self._nameKey(key) in self.indexes
4339
4542
  return MutableMapping.__contains__(self, key)
4340
4543
 
4341
4544
  def get(self, key, default=None):
@@ -4486,7 +4689,7 @@ class TSVZedLite(MutableMapping):
4486
4689
  self._specWrite(_specFormatRecord(value, self.delimiter, marker=True))
4487
4690
 
4488
4691
  def _specWrite(self, line):
4489
- """Append one record to the active part; return its offset, or None for a compressed part."""
4692
+ """Append one record (or lines written together) to the active part; return its offset, or None for a compressed part."""
4490
4693
  data = (line + '\n').encode('utf-8', errors='replace')
4491
4694
  if self.fileObj is None:
4492
4695
  reporter = _Reporter(self._activePath)
@@ -4659,7 +4862,7 @@ class TSVZedLite(MutableMapping):
4659
4862
 
4660
4863
  def __mapDeleteToFile(self,key):
4661
4864
  if self._spec:
4662
- self._specWrite(_specFormatRecord([key], self.delimiter))
4865
+ self._specWrite(_specTombstoneLines(key, self.delimiter, self._specState.strip))
4663
4866
  return
4664
4867
  # Persist a deletion by appending a tombstone (a lone key with no
4665
4868
  # values); on read such a row deletes the key (see _processLine). The
@@ -4713,7 +4916,7 @@ class TSVZedLite(MutableMapping):
4713
4916
 
4714
4917
  # Implement basic __getitem__, __setitem__, __delitem__, __iter__, and __len__. needed for MutableMapping
4715
4918
  def __getitem__(self,key):
4716
- key = str(key).rstrip()
4919
+ key = self._nameKey(key)
4717
4920
  if key not in self.indexes:
4718
4921
  if key == DEFAULTS_INDICATOR_KEY:
4719
4922
  return self.defaults
@@ -4792,7 +4995,7 @@ class TSVZedLite(MutableMapping):
4792
4995
  self.indexes[key] = self.__writeValues(value)
4793
4996
 
4794
4997
  def __delitem__(self,key):
4795
- key = str(key).rstrip()
4998
+ key = self._nameKey(key)
4796
4999
  if self._spec:
4797
5000
  if _MARKER_RE.match(key):
4798
5001
  # A lone marker key resets that marker (spec §12.1).
@@ -4803,7 +5006,7 @@ class TSVZedLite(MutableMapping):
4803
5006
  eprint(f"Key {key} not found")
4804
5007
  return
4805
5008
  self.indexes.pop(key, None)
4806
- self._specWrite(_specFormatRecord([key], self.delimiter))
5009
+ self.__mapDeleteToFile(key)
4807
5010
  return
4808
5011
  if key == DEFAULTS_INDICATOR_KEY:
4809
5012
  self.__mapDeleteToFile(key)
@@ -4850,7 +5053,7 @@ class TSVZedLite(MutableMapping):
4850
5053
 
4851
5054
  __marker = object()
4852
5055
  def pop(self, key, default=__marker, return_pos = False):
4853
- key = str(key).rstrip()
5056
+ key = self._nameKey(key)
4854
5057
  try:
4855
5058
  pos = self.indexes.pop(key)
4856
5059
  except KeyError:
@@ -5311,9 +5514,9 @@ class _ServeStore(object):
5311
5514
  return not self.parts
5312
5515
 
5313
5516
  def resolve(self, key):
5314
- """A requested key as a reader resolves it (spec §7.7; 3.39 rstrip for loose files)."""
5517
+ """The key a requested key names (spec §20.2.7; 3.39 rstrip for loose files)."""
5315
5518
  if self.spec:
5316
- return key.rstrip(' \t') if self.state.strip else key
5519
+ return _specNameKey(key, self.delimiter, self.state.strip, self.data)
5317
5520
  return key.rstrip()
5318
5521
 
5319
5522
  def read(self, log):
@@ -5380,11 +5583,14 @@ class _ServeStore(object):
5380
5583
  def _payload(self, rows, log):
5381
5584
  """Bytes for ``rows`` (``[(cells, marker), ...]``), formatted as the direct writers format them."""
5382
5585
  if self.spec:
5383
- return ''.join(_specFormatRecord(cells, self.delimiter, marker=marker) + '\n'
5384
- for cells, marker in rows).encode('utf-8')
5586
+ return _specFormatRecords(rows, self.delimiter).encode('utf-8')
5385
5587
  return _legacyFormatPayload(self.path, [cells for cells, marker in rows], log, [''], True, self.verbose,
5386
5588
  'utf8', self.strict, self.delimiter)[0]
5387
5589
 
5590
+ def _tombstone(self, key):
5591
+ """Rows that delete ``key``: written with stripping off when it would strip ``key`` (spec §9.6)."""
5592
+ return _specTombstones([key], self.delimiter, self.state.strip) if self.spec else [([key], False)]
5593
+
5388
5594
  def _durable(self, sync):
5389
5595
  return sync or (self.spec and self.state.writeAck == 'disk')
5390
5596
 
@@ -5406,10 +5612,27 @@ class _ServeStore(object):
5406
5612
  self.writer.wait(queued[0])
5407
5613
 
5408
5614
  def write(self, rows, sync, log, written=False):
5409
- """Append ``rows`` (``[(cells, marker), ...]``) as one batch: ``set``, ``delete`` and bulk input."""
5615
+ """Append ``rows`` (``[(cells, marker), ...]``) as one batch: ``set`` and its bulk input."""
5616
+ with self.lock:
5617
+ queued = self._submit(rows, sync, log)
5618
+ self._ack(queued, written)
5619
+
5620
+ def delete(self, rows, sync, log, written=False):
5621
+ """Append the tombstones of ``delete`` (``[(cells, marker), ...]``) as one batch (spec §20.2.7).
5622
+
5623
+ Naming a key a reader would strip needs the store as it is now, read
5624
+ under the lock, so that delete is atomic (spec §21.8.2). Returns False,
5625
+ having written nothing, when that read could not be completed.
5626
+ """
5410
5627
  with self.lock:
5628
+ if self.spec and _specNamesKeys(rows, self.delimiter):
5629
+ self._fresh(log)
5630
+ if self.unreadable:
5631
+ return False
5632
+ rows = _specDeleteRows(rows, self.delimiter, self.state.strip, self.data)
5411
5633
  queued = self._submit(rows, sync, log)
5412
5634
  self._ack(queued, written)
5635
+ return True
5413
5636
 
5414
5637
  def pop(self, key, sync, log):
5415
5638
  """Atomically return the row of ``key`` and append its tombstone; None when it is missing."""
@@ -5422,7 +5645,7 @@ class _ServeStore(object):
5422
5645
  if row is None:
5423
5646
  return None
5424
5647
  row = list(row)
5425
- queued = self._submit([([key], False)], sync, log)
5648
+ queued = self._submit(self._tombstone(key), sync, log)
5426
5649
  self._ack(queued, written=True) # the row is only popped once its tombstone is written
5427
5650
  return row
5428
5651
 
@@ -5436,7 +5659,7 @@ class _ServeStore(object):
5436
5659
  return None
5437
5660
  key = next(reversed(self.data)) if last else next(iter(self.data))
5438
5661
  row = list(self.data[key])
5439
- queued = self._submit([([key], False)], sync, log)
5662
+ queued = self._submit(self._tombstone(key), sync, log)
5440
5663
  self._ack(queued, written=True)
5441
5664
  return row
5442
5665
 
@@ -5621,21 +5844,32 @@ def _serveDispatch(store, line, bulk, log):
5621
5844
  if operation in _SERVE_NEED_STORE and store.missing(log):
5622
5845
  return 1, [], 'no such store' # spec §20.2.2
5623
5846
  unreadable = (1, _SERVE_UNREADABLE)
5847
+ # Each read takes its status under the store lock it read under: another request's reload may
5848
+ # change ``unreadable`` once the lock is free.
5624
5849
  if operation == 'read':
5625
- rows = store.read(log)
5626
- status = unreadable if store.unreadable else (0, '')
5850
+ with store.lock:
5851
+ rows = store.read(log)
5852
+ status = unreadable if store.unreadable else (0, '')
5627
5853
  return status[0], [_specFormatRecord(row, '\t') for row in rows], status[1]
5628
5854
  if operation == 'get':
5629
- rows, missing = store.get(args, log)
5630
- status = unreadable if store.unreadable else (3 if missing else 0, '')
5855
+ with store.lock:
5856
+ rows, missing = store.get(args, log)
5857
+ status = unreadable if store.unreadable else (3 if missing else 0, '')
5631
5858
  return status[0], [_specFormatRecord(row, '\t') for row in rows], status[1]
5632
5859
  if operation == 'has':
5633
- found = store.has(args[0], log)
5634
- return (1, [], _SERVE_UNREADABLE) if store.unreadable else (0 if found else 3, [], '')
5860
+ with store.lock:
5861
+ found = store.has(args[0], log)
5862
+ return (1, [], _SERVE_UNREADABLE) if store.unreadable else (0 if found else 3, [], '')
5635
5863
  if operation == 'len':
5636
- return 0, [str(store.length(log))], ''
5864
+ with store.lock:
5865
+ count = store.length(log)
5866
+ status = unreadable if store.unreadable else (0, '') # spec §4.10, §20.6: a stopped read failed
5867
+ return status[0], [str(count)], status[1]
5637
5868
  if operation == 'keys':
5638
- return 0, [_specEncodeField(key, '\t', isKey=True) for key in store.keys(log)], ''
5869
+ with store.lock:
5870
+ keys = store.keys(log)
5871
+ status = unreadable if store.unreadable else (0, '')
5872
+ return status[0], [_specEncodeField(key, '\t', isKey=True) for key in keys], status[1]
5639
5873
  if operation in ('set', 'append', 'delete'):
5640
5874
  keysOnly = operation == 'delete'
5641
5875
  if isBulk:
@@ -5648,7 +5882,10 @@ def _serveDispatch(store, line, bulk, log):
5648
5882
  rows = [([key], bool(_MARKER_RE.match(field))) for key, field in zip(args, raw)]
5649
5883
  else:
5650
5884
  rows = [(args, bool(_MARKER_RE.match(raw[0])))]
5651
- store.write(rows, sync, log, written)
5885
+ if not keysOnly:
5886
+ store.write(rows, sync, log, written)
5887
+ elif not store.delete(rows, sync, log, written):
5888
+ return 1, [], _SERVE_UNREADABLE + '; nothing was written'
5652
5889
  return 0, [], ''
5653
5890
  if operation in ('pop', 'popitem'):
5654
5891
  if operation == 'pop':
@@ -6349,7 +6586,10 @@ class TSVZClient(MutableMapping):
6349
6586
 
6350
6587
  def __delitem__(self, key):
6351
6588
  """Delete ``key``; a missing key is not an error (as in ``TSVZed``)."""
6352
- self._request(['delete', _serveEncode(str(key))], write=True)
6589
+ key = str(key)
6590
+ # Resending the delete of a key ending in whitespace could name, and delete, the stripped key (spec §20.2.7).
6591
+ retry = not (self._spec and _specStrippable(key, self.delimiter))
6592
+ self._request(['delete', _serveEncode(key)], write=True, retry=retry)
6353
6593
 
6354
6594
  _marker = object()
6355
6595
 
@@ -6375,6 +6615,11 @@ class TSVZClient(MutableMapping):
6375
6615
  cells = self._cells(key, default)
6376
6616
  if len(cells) < 2:
6377
6617
  return self.get(key)
6618
+ if str(key) != cells[0]:
6619
+ # cells[0] is stripped as set strips it; a live key equal to ``key`` wins first (spec §20.2.7).
6620
+ row = self.get(key)
6621
+ if row is not None:
6622
+ return row
6378
6623
  lines = self._request(['setdefault'] + self._fields(cells), write=True, retry=False)[0]
6379
6624
  return _serveDecodeLine(lines[0]) if lines else None
6380
6625
 
@@ -6820,7 +7065,7 @@ def _cliRead(args, delimiter, logger, stdin, stdout):
6820
7065
  def _cliGet(args, delimiter, logger, stdin, stdout):
6821
7066
  """``get STORE KEY ...`` (spec §20.2): each KEY's row in argument order; exit 3 when one is missing.
6822
7067
 
6823
- Keys resolve as a reader resolves them (§7.7). For a missing key of a
7068
+ Each KEY names a key as §20.2.7 says. For a missing key of a
6824
7069
  spec store this prints what ``TSVZed`` returns for it (§14.5): the key and
6825
7070
  the active defaults while ``#_return_defaults_when_missing_#`` is true,
6826
7071
  else nothing. A missing key of a loose file prints nothing.
@@ -6839,7 +7084,7 @@ def _cliGet(args, delimiter, logger, stdin, stdout):
6839
7084
  missing = False
6840
7085
  for key in args.args:
6841
7086
  if spec:
6842
- key = key.rstrip(' \t') if state.strip else key
7087
+ key = _specNameKey(key, delimiter, state.strip, data)
6843
7088
  else:
6844
7089
  key = key.rstrip()
6845
7090
  row = data.get(key)
@@ -6885,15 +7130,20 @@ def _cliStdinBatch(args, delimiter, logger, stdin, keysOnly):
6885
7130
  so is a line with an empty key (reported). ``keysOnly`` keeps the first
6886
7131
  field only: a tombstone, or a marker reset.
6887
7132
  """
6888
- spec = _isSpecPath(args.store)
7133
+ rows = _cliStdinRows(args, delimiter, logger, stdin, keysOnly)
7134
+ if not _isSpecPath(args.store):
7135
+ return [cells for cells, marker in rows]
7136
+ return [_specFormatRecord(cells, delimiter, marker=marker) for cells, marker in rows]
7137
+
7138
+
7139
+ def _cliStdinRows(args, delimiter, logger, stdin, keysOnly):
7140
+ """Standard input for ``set STORE -`` / ``delete STORE -`` as ``[(cells, marker), ...]`` (spec §20.3)."""
6889
7141
  source = _Reporter('<stdin>', logger)
6890
7142
  try:
6891
- rows = _parseRecordLines(_cliStdinLines(stdin, source), delimiter, spec, keysOnly, source)
7143
+ return _parseRecordLines(_cliStdinLines(stdin, source), delimiter, _isSpecPath(args.store), keysOnly,
7144
+ source)
6892
7145
  finally:
6893
7146
  source.flush()
6894
- if not spec:
6895
- return [cells for cells, marker in rows]
6896
- return [_specFormatRecord(cells, delimiter, marker=marker) for cells, marker in rows]
6897
7147
 
6898
7148
 
6899
7149
  def _parseRecordLines(texts, delimiter, spec, keysOnly, reporter):
@@ -6958,12 +7208,33 @@ def _cliDelete(args, delimiter, logger, stdin, stdout):
6958
7208
  """``delete STORE KEY [KEY ...]`` (spec §20.2): one tombstone per KEY, appended in one write.
6959
7209
 
6960
7210
  ``delete STORE -`` deletes the first field of every line on standard input (§20.3).
7211
+ A key that a reader would strip names a key of the store (§20.2.7), so the
7212
+ store is read first; when it cannot be read whole nothing is written and
7213
+ the exit status is 1.
6961
7214
  """
6962
- if args.args == ['-']:
6963
- _cliAppendBatch(args, delimiter, logger, _cliStdinBatch(args, delimiter, logger, stdin, keysOnly=True))
7215
+ if not _isSpecPath(args.store):
7216
+ if args.args == ['-']:
7217
+ _cliAppendBatch(args, delimiter, logger, _cliStdinBatch(args, delimiter, logger, stdin, keysOnly=True))
7218
+ else:
7219
+ appendLinesTabularFile(args.store, [[key] for key in args.args], teeLogger=logger, header=args.header,
7220
+ createIfNotExist=True, verbose=args.verbose, strict=args.strict,
7221
+ delimiter=delimiter)
6964
7222
  return _cliWritten(args, logger)
6965
- appendLinesTabularFile(args.store, [[key] for key in args.args], teeLogger=logger, header=args.header,
6966
- createIfNotExist=True, verbose=args.verbose, strict=args.strict, delimiter=delimiter)
7223
+ if args.args == ['-']:
7224
+ rows = _cliStdinRows(args, delimiter, logger, stdin, keysOnly=True)
7225
+ else:
7226
+ rows = [([key], bool(_MARKER_RE.match(key))) for key in args.args]
7227
+ if _specNamesKeys(rows, delimiter):
7228
+ strip, live = True, {} # a store delete creates starts with stripping on (spec §10.3)
7229
+ if _cliStoreExists(args.store):
7230
+ load, readable = _cliSpecLoad(args, delimiter, logger)
7231
+ if not readable:
7232
+ logger.teelog('tsvz: {}: could not read every part of the store; nothing was written'.format(
7233
+ args.store), 'error')
7234
+ return 1
7235
+ strip, live = load.state.strip, load.data
7236
+ rows = _specDeleteRows(rows, delimiter, strip, live)
7237
+ _cliAppendBatch(args, delimiter, logger, [_specFormatRecord(cells, delimiter, marker=marker) for cells, marker in rows])
6967
7238
  return _cliWritten(args, logger)
6968
7239
 
6969
7240
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes