TSVZ 5.1__tar.gz → 5.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TSVZ
3
- Version: 5.1
3
+ Version: 5.2
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/tsvz-org/TSVZ
6
6
  Author: Yufei Pan
@@ -163,6 +163,20 @@ named like an operation then needs a path prefix (`./read`).
163
163
  with a warning.
164
164
  - **TSVZ extensions:**
165
165
  - `--x-header H` and `--x-defaults KEY<d>D1<d>...` decode backslash escapes such as `\t`;
166
+ - `--x-header H` makes line 1 of a `.tsv`-family file its header for every operation
167
+ when line 1 is H: the same number of cells, each equal to H's once the whitespace
168
+ around it is taken off (a trailing empty cell counts: `id\tv\t` is not `id\tv`; a UTF-8
169
+ byte order mark before line 1, as Excel writes one, is skipped). It
170
+ is then never a key, row or count, `scrub` and `clear` keep it, and
171
+ `read --format table` shows it above the rows. When line 1 differs, the file has no
172
+ header and line 1 is a row, with 3.39's "Header mismatch" warning (`--x-strict`: an
173
+ error, exit 1, nothing written). A missing or empty file gets H first. Without
174
+ `--x-header`, line 1 is a row, as in 3.39. (The library's own header check, a
175
+ `header=` given to `readTabularFile`, `appendTabularFile`, `clearTabularFile`,
176
+ `scrubTabularFile` or `TSVZed`, is 3.39's, looser one: each cell of H within the
177
+ cell of line 1 under it, so `idaho\tvalue` passes for `id\tv`.) A rewrite without
178
+ `--x-header` can put a row equal to H on line 1
179
+ ([limitations](docs/limitations.md#a-rewrite-without---x-header-can-put-a-row-equal-to-h-on-line-1)).
166
180
  - `--x-strict` / `--x-force` turn 3.39's column and header checks on or off.
167
181
 
168
182
  ## Serving a store
@@ -193,7 +207,13 @@ status stay the same.
193
207
  and `fsync`s everything and removes `STORE.serve`.
194
208
  - **Bypassing it.** `--x-direct` makes a `tsvz` command use the files. Command
195
209
  lines with `--x-header`, `--x-defaults` or `--x-strict`, or another delimiter
196
- than the server's, use the files too.
210
+ than the server's, use the files too. A server started with `--x-header H`
211
+ answers every request by the `--x-header` rule above, and one started with
212
+ `--x-strict` too answers as a direct `--x-strict` command does: a line 1 that is
213
+ not H fails the request (status 1, nothing written), and reads and scrubs drop
214
+ the rows of another width. It does not start when line 1 is not H then. The
215
+ table of a routed `read --format table` shows no H line: its first row is set
216
+ as the table's header, as with no `--x-header`.
197
217
 
198
218
  ```python
199
219
  import TSVZ
@@ -261,7 +281,9 @@ microseconds as a write starts is followed as a rotation instead); while that re
261
281
  fails or is refused, the rows are appended to a new file there, and memory keeps all of
262
282
  them for the rewrite that gets through. A compressed file another writer left damaged
263
283
  is made again too (L5: every read stops at the damage, so rows
264
- appended after it would be read by no one); the bytes after the damage go. While that
284
+ appended after it would be read by no one): what the read did not get goes, the bytes after
285
+ the damage and also the lines of the damaged member before it that the codec never handed
286
+ over (Python's gzip gives none of a first member cut short, where `zcat` shows them). While that
265
287
  rewrite fails, the rows are appended after the damage and memory keeps them for the
266
288
  next rewrite that gets through; while it is refused (a row changed in place to text
267
289
  the encoding cannot hold), they are appended after it as in 4.9, and the next read
@@ -301,6 +323,106 @@ empty; the keys it sets or deletes then take its values, and the keys it left ou
301
323
  memory's. An explicit `reload()` while the file is damaged replaces memory with the
302
324
  damaged file, as asked, and warns.
303
325
 
326
+ ## Upgrading to 5.2 from 5.1
327
+
328
+ The file formats are unchanged, and a 3.39 host still reads every `.tsv`-family file
329
+ 5.2 writes.
330
+
331
+ - **A scrub of a `.tsv` whose rows were all deleted shrinks it.** `scrubTabularFile`,
332
+ `tsvz scrub` (also through a `tsvz serve` handler) of a `.tsv`-family file (`.tsv`,
333
+ `.csv`, `.nsv`, `.psv` and their compressed forms) in which every key has been deleted
334
+ leaves the file as an empty store: the header line it has, and its `#_defaults_#`
335
+ lines as a scrub of a file with rows keeps them (an empty one after the header, the
336
+ last non-empty one after it, both as wide as the header, or as the column count the
337
+ read took, which caller defaults set when given). A file with no header line keeps its
338
+ empty first line, or becomes 0 bytes when its first line was a row; a first line that
339
+ is no row (a comment, say) becomes an empty line. A defaults line it keeps follows an
340
+ empty first line, which the scrub then writes. Comment lines go, as a scrub of a file
341
+ with rows already drops them. A scrub that is given
342
+ a header (and checks it, the default) keeps the file's first line as its header, an
343
+ empty one included, with a "Header mismatch" warning when it differs; the header it is
344
+ given is written only with `verifyHeader=False`, or when the file's first line cannot be
345
+ decoded (plain text named `.gz`); `tsvz scrub --x-header` follows the `--x-header` rule
346
+ below instead. A scrub of a path that is not there
347
+ makes no file and returns `{}`, as in 5.1, unless `createIfNotExist=True`. A compressed
348
+ file whose read decodes no row (plain text named `.gz`, a first member cut short) is
349
+ made again as an empty store, as a scrub of a damaged file with rows already was in
350
+ 5.1: what the read did not get goes, the bytes after the damage and also the lines of the
351
+ damaged member before it that the codec never handed over (Python's gzip gives none of
352
+ a first member cut short, where `zcat` shows them) (see the damaged-stream notes above), and
353
+ `tsvz clear --x-header H --x-strict` (`clearTabularFile(verifyHeader=True)`) now clears
354
+ such a file, where 5.1 exited 1 with the codec's error.
355
+ 5.1 and 5.0 left such a file as it was, deletion lines included, so a writer that
356
+ appended keys, deleted them all and scrubbed, over and over, saw the file grow without
357
+ limit (205, 405, 605 bytes, ... for 20 keys a cycle); now it is the same size after
358
+ every scrub. A file with no byte (or a compressed one with only empty streams) is
359
+ still left alone, and so is a file whose strict scrub (`strict=True`, `--x-strict`) read
360
+ dropped a line for its width and found no row: that line is not a deletion, as 5.1 left
361
+ it. A scrub that finds the file changed since its read still reads
362
+ again and writes nothing if it keeps changing, as for a file with rows.
363
+ - **`get_file_obj('ab')` on a compressed store writes its trailer before the lock is let
364
+ go.** A program that calls it on a `.tsv.gz`, `.bz2`, `.xz` or `.zst` store and writes
365
+ through the file now has all of it, the gzip trailer and the bz2, xz and zstd data included,
366
+ in the file when `release_file_obj()` unlocks it. 5.1 and earlier wrote them after the
367
+ unlock, where another writer could append in between and corrupt the stream.
368
+ - **A `TSVZed` no longer lets a file grow with every session of sets and deletes.** It
369
+ rewrites a `.tsv`-family file at open when the file holds a key's row and the deletion
370
+ of it, as 3.39 did only for a key with two values, so a program that sets keys and
371
+ deletes them in each session (default settings, or `rewrite_interval`) keeps the file
372
+ the same size (205, 405, 605 bytes, ... for 20 keys a session before). Every session
373
+ that opens such a file now rewrites it once. With `rewrite_on_load=False` the open
374
+ does not, and the next exit or periodic rewrite does it (`rewrite_on_exit=True`,
375
+ `rewrite_interval`). What such a rewrite does, it now does at a plain open:
376
+ - it cuts a row wider than the header, and the cut stays
377
+ ([limitations](docs/limitations.md#scrubs-and-rewrites-read-rows-at-one-column-width-deferred-to-the-width-design)):
378
+ `id\tv\na\t1\t2\nb\t1\nb\t\n` becomes `id\tv\na\t1 \n`, and
379
+ `id\tv\na\t\t7\nb\t1\nb\t\n` becomes `id\tv\na\t \n`, the 7 lost;
380
+ - it writes over the file in place and pads with spaces (`a\t1 ` above): TSVZ readers
381
+ strip them, tools that read the raw bytes (`awk`, `cut`) see them;
382
+ - on a file it cannot write (read-only), it prints `Failed to open file … Permission
383
+ denied` twice and leaves the file as it is, as 5.1 did for a key with two values.
384
+ - **`--x-header H` makes line 1 the header for every `tsvz` operation.** On a
385
+ `.tsv`-family file whose line 1 is H (an exact match: the same cells, each without the
386
+ whitespace around it, a trailing empty cell included), line 1 is the header: `keys`,
387
+ `len`, `read`, `get`, `has`, `pop`, `popitem` and `delete` never list, count, print,
388
+ pop or delete it, and `scrub` and `clear` keep it. When line 1 differs, the file has no
389
+ header and line 1 is a row, with 3.39's "Header mismatch" warning, for the reads and
390
+ the scrub alike, so a scrub keeps the rows a read shows (but for the column-width cases
391
+ below), and in such a file it never writes a row that is H as line 1: it puts an empty
392
+ line first (`a\t1\nid\tv\na\t\n` becomes `\nid\tv\n`), so the reads still take that row
393
+ for a row. With `--x-strict` a line 1 that is not H is an error (exit 1, nothing
394
+ written) for every operation, through a handler too. A missing or empty file gets H
395
+ first at its first write, as in 5.1. A `tsvz serve STORE --x-header H` handler applies
396
+ the rule to every request it answers, routed `tsvz` commands included; with
397
+ `--x-strict` it is strict for them as above (5.1 ignored `--x-strict` at `serve`): such
398
+ a handler refuses to start on a file whose line 1 is not H, its routed `.tsv` reads and
399
+ scrubs drop lines of another column count, and its `.tsvz` reads are strict, as for a
400
+ direct `--x-strict` command. The library's own header check (`header=` to
401
+ `readTabularFile`, `appendTabularFile`, `clearTabularFile`, `scrubTabularFile`,
402
+ `TSVZed`) is unchanged, 3.39's containment check. The CLI's writes check line 1 more
403
+ strictly than 5.1's did: 5.1's `set`, `append`, `delete` and `clear` with `--x-header`
404
+ silently took a line 1 that merely held H's cells (`id\tvalue` for `id\tv`) for the
405
+ header and fitted rows to H's width; now they warn and write the row at its own width,
406
+ and with `--x-strict` exit 1. A rewrite without `--x-header` (a scrub without it,
407
+ `scrubTabularFile`, a `TSVZed` rewrite) can still put a row equal to H on line 1 of a
408
+ header-less file
409
+ ([limitations](docs/limitations.md#a-rewrite-without---x-header-can-put-a-row-equal-to-h-on-line-1)).
410
+ In 5.1 only `set`, `append`, `delete` and `clear` used `--x-header`: the reads and
411
+ `scrub` took line 1 as a row (`tsvz keys`
412
+ printed `id a b` for two keys, `len` said 3, `popitem first` popped the header), and
413
+ so did every request to a handler. Without `--x-header` nothing changed: line 1 is a
414
+ row, as in 5.1 and 3.39. In a `.tsvz` store `pop`, `popitem` and `setdefault` now check
415
+ `--x-header` as `set` does; nothing else changed there.
416
+ - **Known, mostly as in 5.1: a scrub, a rewrite or a read takes one column count for every row,
417
+ and can lose cells or keys.** A scrub given defaults narrower than the rows, a row wider
418
+ than the header, and a `.tsv` that begins with a bare `#_defaults_#` line (read without
419
+ defaults) are examples; the entry lists each, and which is new in 5.2
420
+ ([limitations](docs/limitations.md#scrubs-and-rewrites-read-rows-at-one-column-width-deferred-to-the-width-design)).
421
+ A strict scrub of a file whose read dropped a line for its width and found no row leaves it,
422
+ so it grows again over append, delete-all and scrub cycles
423
+ ([limitations](docs/limitations.md#a-strict-scrub-leaves-a-file-whose-read-dropped-a-line-for-its-width)).
424
+ Both wait for the column-width design after 5.2.
425
+
304
426
  ## Upgrading to 5.1 from 5.0
305
427
 
306
428
  The file formats are unchanged, and a 3.39 host still reads every `.tsv`-family file
@@ -357,12 +479,11 @@ The file formats are unchanged, and a 3.39 host still reads every `.tsv`-family
357
479
  file's own header line.)
358
480
  `TSVZed`'s message for a refused rewrite lists `hardMapToFile()` among the rewrites
359
481
  a program asks for.
360
- - **Known: a scrub of a `.tsv` whose rows were all deleted does not compact it** when
361
- the scrub is given the file's header (`scrubTabularFile(path, header=H)`) or the file
362
- has no header line: it leaves the file as it is, deletion lines included; reads are
363
- right, as in 3.39 and 5.0. (`tsvz scrub` of a headed file reads the header line as a
364
- row, so it does compact it.)
365
- ([limitations](docs/limitations.md#a-scrub-of-a-tsv-whose-rows-were-all-deleted-does-not-compact-it)).
482
+ - **Known in 5.1, fixed in 5.2: a scrub of a `.tsv` whose rows were all deleted did not
483
+ compact it** when the scrub was given the file's header (`scrubTabularFile(path, header=H)`)
484
+ or the file had no header line: it left the file as it was, deletion lines included;
485
+ reads were right, as in 3.39 and 5.0. (`tsvz scrub` of a headed file read the header
486
+ line as a row, so it did compact it.)
366
487
  - **Known: a scrub run with other defaults than the store's program can change how
367
488
  early rows read.** In a `.tsv` with an empty `#_defaults_#` line and rows with empty
368
489
  cells before any defaults line, the reset line the scrub writes before the rows keeps
@@ -131,6 +131,20 @@ named like an operation then needs a path prefix (`./read`).
131
131
  with a warning.
132
132
  - **TSVZ extensions:**
133
133
  - `--x-header H` and `--x-defaults KEY<d>D1<d>...` decode backslash escapes such as `\t`;
134
+ - `--x-header H` makes line 1 of a `.tsv`-family file its header for every operation
135
+ when line 1 is H: the same number of cells, each equal to H's once the whitespace
136
+ around it is taken off (a trailing empty cell counts: `id\tv\t` is not `id\tv`; a UTF-8
137
+ byte order mark before line 1, as Excel writes one, is skipped). It
138
+ is then never a key, row or count, `scrub` and `clear` keep it, and
139
+ `read --format table` shows it above the rows. When line 1 differs, the file has no
140
+ header and line 1 is a row, with 3.39's "Header mismatch" warning (`--x-strict`: an
141
+ error, exit 1, nothing written). A missing or empty file gets H first. Without
142
+ `--x-header`, line 1 is a row, as in 3.39. (The library's own header check, a
143
+ `header=` given to `readTabularFile`, `appendTabularFile`, `clearTabularFile`,
144
+ `scrubTabularFile` or `TSVZed`, is 3.39's, looser one: each cell of H within the
145
+ cell of line 1 under it, so `idaho\tvalue` passes for `id\tv`.) A rewrite without
146
+ `--x-header` can put a row equal to H on line 1
147
+ ([limitations](docs/limitations.md#a-rewrite-without---x-header-can-put-a-row-equal-to-h-on-line-1)).
134
148
  - `--x-strict` / `--x-force` turn 3.39's column and header checks on or off.
135
149
 
136
150
  ## Serving a store
@@ -161,7 +175,13 @@ status stay the same.
161
175
  and `fsync`s everything and removes `STORE.serve`.
162
176
  - **Bypassing it.** `--x-direct` makes a `tsvz` command use the files. Command
163
177
  lines with `--x-header`, `--x-defaults` or `--x-strict`, or another delimiter
164
- than the server's, use the files too.
178
+ than the server's, use the files too. A server started with `--x-header H`
179
+ answers every request by the `--x-header` rule above, and one started with
180
+ `--x-strict` too answers as a direct `--x-strict` command does: a line 1 that is
181
+ not H fails the request (status 1, nothing written), and reads and scrubs drop
182
+ the rows of another width. It does not start when line 1 is not H then. The
183
+ table of a routed `read --format table` shows no H line: its first row is set
184
+ as the table's header, as with no `--x-header`.
165
185
 
166
186
  ```python
167
187
  import TSVZ
@@ -229,7 +249,9 @@ microseconds as a write starts is followed as a rotation instead); while that re
229
249
  fails or is refused, the rows are appended to a new file there, and memory keeps all of
230
250
  them for the rewrite that gets through. A compressed file another writer left damaged
231
251
  is made again too (L5: every read stops at the damage, so rows
232
- appended after it would be read by no one); the bytes after the damage go. While that
252
+ appended after it would be read by no one): what the read did not get goes, the bytes after
253
+ the damage and also the lines of the damaged member before it that the codec never handed
254
+ over (Python's gzip gives none of a first member cut short, where `zcat` shows them). While that
233
255
  rewrite fails, the rows are appended after the damage and memory keeps them for the
234
256
  next rewrite that gets through; while it is refused (a row changed in place to text
235
257
  the encoding cannot hold), they are appended after it as in 4.9, and the next read
@@ -269,6 +291,106 @@ empty; the keys it sets or deletes then take its values, and the keys it left ou
269
291
  memory's. An explicit `reload()` while the file is damaged replaces memory with the
270
292
  damaged file, as asked, and warns.
271
293
 
294
+ ## Upgrading to 5.2 from 5.1
295
+
296
+ The file formats are unchanged, and a 3.39 host still reads every `.tsv`-family file
297
+ 5.2 writes.
298
+
299
+ - **A scrub of a `.tsv` whose rows were all deleted shrinks it.** `scrubTabularFile`,
300
+ `tsvz scrub` (also through a `tsvz serve` handler) of a `.tsv`-family file (`.tsv`,
301
+ `.csv`, `.nsv`, `.psv` and their compressed forms) in which every key has been deleted
302
+ leaves the file as an empty store: the header line it has, and its `#_defaults_#`
303
+ lines as a scrub of a file with rows keeps them (an empty one after the header, the
304
+ last non-empty one after it, both as wide as the header, or as the column count the
305
+ read took, which caller defaults set when given). A file with no header line keeps its
306
+ empty first line, or becomes 0 bytes when its first line was a row; a first line that
307
+ is no row (a comment, say) becomes an empty line. A defaults line it keeps follows an
308
+ empty first line, which the scrub then writes. Comment lines go, as a scrub of a file
309
+ with rows already drops them. A scrub that is given
310
+ a header (and checks it, the default) keeps the file's first line as its header, an
311
+ empty one included, with a "Header mismatch" warning when it differs; the header it is
312
+ given is written only with `verifyHeader=False`, or when the file's first line cannot be
313
+ decoded (plain text named `.gz`); `tsvz scrub --x-header` follows the `--x-header` rule
314
+ below instead. A scrub of a path that is not there
315
+ makes no file and returns `{}`, as in 5.1, unless `createIfNotExist=True`. A compressed
316
+ file whose read decodes no row (plain text named `.gz`, a first member cut short) is
317
+ made again as an empty store, as a scrub of a damaged file with rows already was in
318
+ 5.1: what the read did not get goes, the bytes after the damage and also the lines of the
319
+ damaged member before it that the codec never handed over (Python's gzip gives none of
320
+ a first member cut short, where `zcat` shows them) (see the damaged-stream notes above), and
321
+ `tsvz clear --x-header H --x-strict` (`clearTabularFile(verifyHeader=True)`) now clears
322
+ such a file, where 5.1 exited 1 with the codec's error.
323
+ 5.1 and 5.0 left such a file as it was, deletion lines included, so a writer that
324
+ appended keys, deleted them all and scrubbed, over and over, saw the file grow without
325
+ limit (205, 405, 605 bytes, ... for 20 keys a cycle); now it is the same size after
326
+ every scrub. A file with no byte (or a compressed one with only empty streams) is
327
+ still left alone, and so is a file whose strict scrub (`strict=True`, `--x-strict`) read
328
+ dropped a line for its width and found no row: that line is not a deletion, as 5.1 left
329
+ it. A scrub that finds the file changed since its read still reads
330
+ again and writes nothing if it keeps changing, as for a file with rows.
331
+ - **`get_file_obj('ab')` on a compressed store writes its trailer before the lock is let
332
+ go.** A program that calls it on a `.tsv.gz`, `.bz2`, `.xz` or `.zst` store and writes
333
+ through the file now has all of it, the gzip trailer and the bz2, xz and zstd data included,
334
+ in the file when `release_file_obj()` unlocks it. 5.1 and earlier wrote them after the
335
+ unlock, where another writer could append in between and corrupt the stream.
336
+ - **A `TSVZed` no longer lets a file grow with every session of sets and deletes.** It
337
+ rewrites a `.tsv`-family file at open when the file holds a key's row and the deletion
338
+ of it, as 3.39 did only for a key with two values, so a program that sets keys and
339
+ deletes them in each session (default settings, or `rewrite_interval`) keeps the file
340
+ the same size (205, 405, 605 bytes, ... for 20 keys a session before). Every session
341
+ that opens such a file now rewrites it once. With `rewrite_on_load=False` the open
342
+ does not, and the next exit or periodic rewrite does it (`rewrite_on_exit=True`,
343
+ `rewrite_interval`). What such a rewrite does, it now does at a plain open:
344
+ - it cuts a row wider than the header, and the cut stays
345
+ ([limitations](docs/limitations.md#scrubs-and-rewrites-read-rows-at-one-column-width-deferred-to-the-width-design)):
346
+ `id\tv\na\t1\t2\nb\t1\nb\t\n` becomes `id\tv\na\t1 \n`, and
347
+ `id\tv\na\t\t7\nb\t1\nb\t\n` becomes `id\tv\na\t \n`, the 7 lost;
348
+ - it writes over the file in place and pads with spaces (`a\t1 ` above): TSVZ readers
349
+ strip them, tools that read the raw bytes (`awk`, `cut`) see them;
350
+ - on a file it cannot write (read-only), it prints `Failed to open file … Permission
351
+ denied` twice and leaves the file as it is, as 5.1 did for a key with two values.
352
+ - **`--x-header H` makes line 1 the header for every `tsvz` operation.** On a
353
+ `.tsv`-family file whose line 1 is H (an exact match: the same cells, each without the
354
+ whitespace around it, a trailing empty cell included), line 1 is the header: `keys`,
355
+ `len`, `read`, `get`, `has`, `pop`, `popitem` and `delete` never list, count, print,
356
+ pop or delete it, and `scrub` and `clear` keep it. When line 1 differs, the file has no
357
+ header and line 1 is a row, with 3.39's "Header mismatch" warning, for the reads and
358
+ the scrub alike, so a scrub keeps the rows a read shows (but for the column-width cases
359
+ below), and in such a file it never writes a row that is H as line 1: it puts an empty
360
+ line first (`a\t1\nid\tv\na\t\n` becomes `\nid\tv\n`), so the reads still take that row
361
+ for a row. With `--x-strict` a line 1 that is not H is an error (exit 1, nothing
362
+ written) for every operation, through a handler too. A missing or empty file gets H
363
+ first at its first write, as in 5.1. A `tsvz serve STORE --x-header H` handler applies
364
+ the rule to every request it answers, routed `tsvz` commands included; with
365
+ `--x-strict` it is strict for them as above (5.1 ignored `--x-strict` at `serve`): such
366
+ a handler refuses to start on a file whose line 1 is not H, its routed `.tsv` reads and
367
+ scrubs drop lines of another column count, and its `.tsvz` reads are strict, as for a
368
+ direct `--x-strict` command. The library's own header check (`header=` to
369
+ `readTabularFile`, `appendTabularFile`, `clearTabularFile`, `scrubTabularFile`,
370
+ `TSVZed`) is unchanged, 3.39's containment check. The CLI's writes check line 1 more
371
+ strictly than 5.1's did: 5.1's `set`, `append`, `delete` and `clear` with `--x-header`
372
+ silently took a line 1 that merely held H's cells (`id\tvalue` for `id\tv`) for the
373
+ header and fitted rows to H's width; now they warn and write the row at its own width,
374
+ and with `--x-strict` exit 1. A rewrite without `--x-header` (a scrub without it,
375
+ `scrubTabularFile`, a `TSVZed` rewrite) can still put a row equal to H on line 1 of a
376
+ header-less file
377
+ ([limitations](docs/limitations.md#a-rewrite-without---x-header-can-put-a-row-equal-to-h-on-line-1)).
378
+ In 5.1 only `set`, `append`, `delete` and `clear` used `--x-header`: the reads and
379
+ `scrub` took line 1 as a row (`tsvz keys`
380
+ printed `id a b` for two keys, `len` said 3, `popitem first` popped the header), and
381
+ so did every request to a handler. Without `--x-header` nothing changed: line 1 is a
382
+ row, as in 5.1 and 3.39. In a `.tsvz` store `pop`, `popitem` and `setdefault` now check
383
+ `--x-header` as `set` does; nothing else changed there.
384
+ - **Known, mostly as in 5.1: a scrub, a rewrite or a read takes one column count for every row,
385
+ and can lose cells or keys.** A scrub given defaults narrower than the rows, a row wider
386
+ than the header, and a `.tsv` that begins with a bare `#_defaults_#` line (read without
387
+ defaults) are examples; the entry lists each, and which is new in 5.2
388
+ ([limitations](docs/limitations.md#scrubs-and-rewrites-read-rows-at-one-column-width-deferred-to-the-width-design)).
389
+ A strict scrub of a file whose read dropped a line for its width and found no row leaves it,
390
+ so it grows again over append, delete-all and scrub cycles
391
+ ([limitations](docs/limitations.md#a-strict-scrub-leaves-a-file-whose-read-dropped-a-line-for-its-width)).
392
+ Both wait for the column-width design after 5.2.
393
+
272
394
  ## Upgrading to 5.1 from 5.0
273
395
 
274
396
  The file formats are unchanged, and a 3.39 host still reads every `.tsv`-family file
@@ -325,12 +447,11 @@ The file formats are unchanged, and a 3.39 host still reads every `.tsv`-family
325
447
  file's own header line.)
326
448
  `TSVZed`'s message for a refused rewrite lists `hardMapToFile()` among the rewrites
327
449
  a program asks for.
328
- - **Known: a scrub of a `.tsv` whose rows were all deleted does not compact it** when
329
- the scrub is given the file's header (`scrubTabularFile(path, header=H)`) or the file
330
- has no header line: it leaves the file as it is, deletion lines included; reads are
331
- right, as in 3.39 and 5.0. (`tsvz scrub` of a headed file reads the header line as a
332
- row, so it does compact it.)
333
- ([limitations](docs/limitations.md#a-scrub-of-a-tsv-whose-rows-were-all-deleted-does-not-compact-it)).
450
+ - **Known in 5.1, fixed in 5.2: a scrub of a `.tsv` whose rows were all deleted did not
451
+ compact it** when the scrub was given the file's header (`scrubTabularFile(path, header=H)`)
452
+ or the file had no header line: it left the file as it was, deletion lines included;
453
+ reads were right, as in 3.39 and 5.0. (`tsvz scrub` of a headed file read the header
454
+ line as a row, so it did compact it.)
334
455
  - **Known: a scrub run with other defaults than the store's program can change how
335
456
  early rows read.** In a `.tsv` with an empty `#_defaults_#` line and rows with empty
336
457
  cells before any defaults line, the reset line the scrub writes before the rows keeps
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TSVZ
3
- Version: 5.1
3
+ Version: 5.2
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/tsvz-org/TSVZ
6
6
  Author: Yufei Pan
@@ -163,6 +163,20 @@ named like an operation then needs a path prefix (`./read`).
163
163
  with a warning.
164
164
  - **TSVZ extensions:**
165
165
  - `--x-header H` and `--x-defaults KEY<d>D1<d>...` decode backslash escapes such as `\t`;
166
+ - `--x-header H` makes line 1 of a `.tsv`-family file its header for every operation
167
+ when line 1 is H: the same number of cells, each equal to H's once the whitespace
168
+ around it is taken off (a trailing empty cell counts: `id\tv\t` is not `id\tv`; a UTF-8
169
+ byte order mark before line 1, as Excel writes one, is skipped). It
170
+ is then never a key, row or count, `scrub` and `clear` keep it, and
171
+ `read --format table` shows it above the rows. When line 1 differs, the file has no
172
+ header and line 1 is a row, with 3.39's "Header mismatch" warning (`--x-strict`: an
173
+ error, exit 1, nothing written). A missing or empty file gets H first. Without
174
+ `--x-header`, line 1 is a row, as in 3.39. (The library's own header check, a
175
+ `header=` given to `readTabularFile`, `appendTabularFile`, `clearTabularFile`,
176
+ `scrubTabularFile` or `TSVZed`, is 3.39's, looser one: each cell of H within the
177
+ cell of line 1 under it, so `idaho\tvalue` passes for `id\tv`.) A rewrite without
178
+ `--x-header` can put a row equal to H on line 1
179
+ ([limitations](docs/limitations.md#a-rewrite-without---x-header-can-put-a-row-equal-to-h-on-line-1)).
166
180
  - `--x-strict` / `--x-force` turn 3.39's column and header checks on or off.
167
181
 
168
182
  ## Serving a store
@@ -193,7 +207,13 @@ status stay the same.
193
207
  and `fsync`s everything and removes `STORE.serve`.
194
208
  - **Bypassing it.** `--x-direct` makes a `tsvz` command use the files. Command
195
209
  lines with `--x-header`, `--x-defaults` or `--x-strict`, or another delimiter
196
- than the server's, use the files too.
210
+ than the server's, use the files too. A server started with `--x-header H`
211
+ answers every request by the `--x-header` rule above, and one started with
212
+ `--x-strict` too answers as a direct `--x-strict` command does: a line 1 that is
213
+ not H fails the request (status 1, nothing written), and reads and scrubs drop
214
+ the rows of another width. It does not start when line 1 is not H then. The
215
+ table of a routed `read --format table` shows no H line: its first row is set
216
+ as the table's header, as with no `--x-header`.
197
217
 
198
218
  ```python
199
219
  import TSVZ
@@ -261,7 +281,9 @@ microseconds as a write starts is followed as a rotation instead); while that re
261
281
  fails or is refused, the rows are appended to a new file there, and memory keeps all of
262
282
  them for the rewrite that gets through. A compressed file another writer left damaged
263
283
  is made again too (L5: every read stops at the damage, so rows
264
- appended after it would be read by no one); the bytes after the damage go. While that
284
+ appended after it would be read by no one): what the read did not get goes, the bytes after
285
+ the damage and also the lines of the damaged member before it that the codec never handed
286
+ over (Python's gzip gives none of a first member cut short, where `zcat` shows them). While that
265
287
  rewrite fails, the rows are appended after the damage and memory keeps them for the
266
288
  next rewrite that gets through; while it is refused (a row changed in place to text
267
289
  the encoding cannot hold), they are appended after it as in 4.9, and the next read
@@ -301,6 +323,106 @@ empty; the keys it sets or deletes then take its values, and the keys it left ou
301
323
  memory's. An explicit `reload()` while the file is damaged replaces memory with the
302
324
  damaged file, as asked, and warns.
303
325
 
326
+ ## Upgrading to 5.2 from 5.1
327
+
328
+ The file formats are unchanged, and a 3.39 host still reads every `.tsv`-family file
329
+ 5.2 writes.
330
+
331
+ - **A scrub of a `.tsv` whose rows were all deleted shrinks it.** `scrubTabularFile`,
332
+ `tsvz scrub` (also through a `tsvz serve` handler) of a `.tsv`-family file (`.tsv`,
333
+ `.csv`, `.nsv`, `.psv` and their compressed forms) in which every key has been deleted
334
+ leaves the file as an empty store: the header line it has, and its `#_defaults_#`
335
+ lines as a scrub of a file with rows keeps them (an empty one after the header, the
336
+ last non-empty one after it, both as wide as the header, or as the column count the
337
+ read took, which caller defaults set when given). A file with no header line keeps its
338
+ empty first line, or becomes 0 bytes when its first line was a row; a first line that
339
+ is no row (a comment, say) becomes an empty line. A defaults line it keeps follows an
340
+ empty first line, which the scrub then writes. Comment lines go, as a scrub of a file
341
+ with rows already drops them. A scrub that is given
342
+ a header (and checks it, the default) keeps the file's first line as its header, an
343
+ empty one included, with a "Header mismatch" warning when it differs; the header it is
344
+ given is written only with `verifyHeader=False`, or when the file's first line cannot be
345
+ decoded (plain text named `.gz`); `tsvz scrub --x-header` follows the `--x-header` rule
346
+ below instead. A scrub of a path that is not there
347
+ makes no file and returns `{}`, as in 5.1, unless `createIfNotExist=True`. A compressed
348
+ file whose read decodes no row (plain text named `.gz`, a first member cut short) is
349
+ made again as an empty store, as a scrub of a damaged file with rows already was in
350
+ 5.1: what the read did not get goes, the bytes after the damage and also the lines of the
351
+ damaged member before it that the codec never handed over (Python's gzip gives none of
352
+ a first member cut short, where `zcat` shows them) (see the damaged-stream notes above), and
353
+ `tsvz clear --x-header H --x-strict` (`clearTabularFile(verifyHeader=True)`) now clears
354
+ such a file, where 5.1 exited 1 with the codec's error.
355
+ 5.1 and 5.0 left such a file as it was, deletion lines included, so a writer that
356
+ appended keys, deleted them all and scrubbed, over and over, saw the file grow without
357
+ limit (205, 405, 605 bytes, ... for 20 keys a cycle); now it is the same size after
358
+ every scrub. A file with no byte (or a compressed one with only empty streams) is
359
+ still left alone, and so is a file whose strict scrub (`strict=True`, `--x-strict`) read
360
+ dropped a line for its width and found no row: that line is not a deletion, as 5.1 left
361
+ it. A scrub that finds the file changed since its read still reads
362
+ again and writes nothing if it keeps changing, as for a file with rows.
363
+ - **`get_file_obj('ab')` on a compressed store writes its trailer before the lock is let
364
+ go.** A program that calls it on a `.tsv.gz`, `.bz2`, `.xz` or `.zst` store and writes
365
+ through the file now has all of it, the gzip trailer and the bz2, xz and zstd data included,
366
+ in the file when `release_file_obj()` unlocks it. 5.1 and earlier wrote them after the
367
+ unlock, where another writer could append in between and corrupt the stream.
368
+ - **A `TSVZed` no longer lets a file grow with every session of sets and deletes.** It
369
+ rewrites a `.tsv`-family file at open when the file holds a key's row and the deletion
370
+ of it, as 3.39 did only for a key with two values, so a program that sets keys and
371
+ deletes them in each session (default settings, or `rewrite_interval`) keeps the file
372
+ the same size (205, 405, 605 bytes, ... for 20 keys a session before). Every session
373
+ that opens such a file now rewrites it once. With `rewrite_on_load=False` the open
374
+ does not, and the next exit or periodic rewrite does it (`rewrite_on_exit=True`,
375
+ `rewrite_interval`). What such a rewrite does, it now does at a plain open:
376
+ - it cuts a row wider than the header, and the cut stays
377
+ ([limitations](docs/limitations.md#scrubs-and-rewrites-read-rows-at-one-column-width-deferred-to-the-width-design)):
378
+ `id\tv\na\t1\t2\nb\t1\nb\t\n` becomes `id\tv\na\t1 \n`, and
379
+ `id\tv\na\t\t7\nb\t1\nb\t\n` becomes `id\tv\na\t \n`, the 7 lost;
380
+ - it writes over the file in place and pads with spaces (`a\t1 ` above): TSVZ readers
381
+ strip them, tools that read the raw bytes (`awk`, `cut`) see them;
382
+ - on a file it cannot write (read-only), it prints `Failed to open file … Permission
383
+ denied` twice and leaves the file as it is, as 5.1 did for a key with two values.
384
+ - **`--x-header H` makes line 1 the header for every `tsvz` operation.** On a
385
+ `.tsv`-family file whose line 1 is H (an exact match: the same cells, each without the
386
+ whitespace around it, a trailing empty cell included), line 1 is the header: `keys`,
387
+ `len`, `read`, `get`, `has`, `pop`, `popitem` and `delete` never list, count, print,
388
+ pop or delete it, and `scrub` and `clear` keep it. When line 1 differs, the file has no
389
+ header and line 1 is a row, with 3.39's "Header mismatch" warning, for the reads and
390
+ the scrub alike, so a scrub keeps the rows a read shows (but for the column-width cases
391
+ below), and in such a file it never writes a row that is H as line 1: it puts an empty
392
+ line first (`a\t1\nid\tv\na\t\n` becomes `\nid\tv\n`), so the reads still take that row
393
+ for a row. With `--x-strict` a line 1 that is not H is an error (exit 1, nothing
394
+ written) for every operation, through a handler too. A missing or empty file gets H
395
+ first at its first write, as in 5.1. A `tsvz serve STORE --x-header H` handler applies
396
+ the rule to every request it answers, routed `tsvz` commands included; with
397
+ `--x-strict` it is strict for them as above (5.1 ignored `--x-strict` at `serve`): such
398
+ a handler refuses to start on a file whose line 1 is not H, its routed `.tsv` reads and
399
+ scrubs drop lines of another column count, and its `.tsvz` reads are strict, as for a
400
+ direct `--x-strict` command. The library's own header check (`header=` to
401
+ `readTabularFile`, `appendTabularFile`, `clearTabularFile`, `scrubTabularFile`,
402
+ `TSVZed`) is unchanged, 3.39's containment check. The CLI's writes check line 1 more
403
+ strictly than 5.1's did: 5.1's `set`, `append`, `delete` and `clear` with `--x-header`
404
+ silently took a line 1 that merely held H's cells (`id\tvalue` for `id\tv`) for the
405
+ header and fitted rows to H's width; now they warn and write the row at its own width,
406
+ and with `--x-strict` exit 1. A rewrite without `--x-header` (a scrub without it,
407
+ `scrubTabularFile`, a `TSVZed` rewrite) can still put a row equal to H on line 1 of a
408
+ header-less file
409
+ ([limitations](docs/limitations.md#a-rewrite-without---x-header-can-put-a-row-equal-to-h-on-line-1)).
410
+ In 5.1 only `set`, `append`, `delete` and `clear` used `--x-header`: the reads and
411
+ `scrub` took line 1 as a row (`tsvz keys`
412
+ printed `id a b` for two keys, `len` said 3, `popitem first` popped the header), and
413
+ so did every request to a handler. Without `--x-header` nothing changed: line 1 is a
414
+ row, as in 5.1 and 3.39. In a `.tsvz` store `pop`, `popitem` and `setdefault` now check
415
+ `--x-header` as `set` does; nothing else changed there.
416
+ - **Known, mostly as in 5.1: a scrub, a rewrite or a read takes one column count for every row,
417
+ and can lose cells or keys.** A scrub given defaults narrower than the rows, a row wider
418
+ than the header, and a `.tsv` that begins with a bare `#_defaults_#` line (read without
419
+ defaults) are examples; the entry lists each, and which is new in 5.2
420
+ ([limitations](docs/limitations.md#scrubs-and-rewrites-read-rows-at-one-column-width-deferred-to-the-width-design)).
421
+ A strict scrub of a file whose read dropped a line for its width and found no row leaves it,
422
+ so it grows again over append, delete-all and scrub cycles
423
+ ([limitations](docs/limitations.md#a-strict-scrub-leaves-a-file-whose-read-dropped-a-line-for-its-width)).
424
+ Both wait for the column-width design after 5.2.
425
+
304
426
  ## Upgrading to 5.1 from 5.0
305
427
 
306
428
  The file formats are unchanged, and a 3.39 host still reads every `.tsv`-family file
@@ -357,12 +479,11 @@ The file formats are unchanged, and a 3.39 host still reads every `.tsv`-family
357
479
  file's own header line.)
358
480
  `TSVZed`'s message for a refused rewrite lists `hardMapToFile()` among the rewrites
359
481
  a program asks for.
360
- - **Known: a scrub of a `.tsv` whose rows were all deleted does not compact it** when
361
- the scrub is given the file's header (`scrubTabularFile(path, header=H)`) or the file
362
- has no header line: it leaves the file as it is, deletion lines included; reads are
363
- right, as in 3.39 and 5.0. (`tsvz scrub` of a headed file reads the header line as a
364
- row, so it does compact it.)
365
- ([limitations](docs/limitations.md#a-scrub-of-a-tsv-whose-rows-were-all-deleted-does-not-compact-it)).
482
+ - **Known in 5.1, fixed in 5.2: a scrub of a `.tsv` whose rows were all deleted did not
483
+ compact it** when the scrub was given the file's header (`scrubTabularFile(path, header=H)`)
484
+ or the file had no header line: it left the file as it was, deletion lines included;
485
+ reads were right, as in 3.39 and 5.0. (`tsvz scrub` of a headed file read the header
486
+ line as a row, so it did compact it.)
366
487
  - **Known: a scrub run with other defaults than the store's program can change how
367
488
  early rows read.** In a `.tsv` with an empty `#_defaults_#` line and rows with empty
368
489
  cells before any defaults line, the reset line the scrub writes before the rows keeps
@@ -42,10 +42,10 @@ if os.name == 'nt':
42
42
  elif os.name == 'posix':
43
43
  import fcntl
44
44
 
45
- version = '5.1'
45
+ version = '5.2'
46
46
  __version__ = version
47
47
  author = 'pan@zopyr.us'
48
- COMMIT_DATE = '2026-11-01'
48
+ COMMIT_DATE = '2026-11-03'
49
49
 
50
50
  DEFAULT_DELIMITER = '\t'
51
51
  DEFAULTS_INDICATOR_KEY = '#_defaults_#'
@@ -1345,6 +1345,8 @@ def _processLine(line,taskDic,correctColumnNum,strict = True,delimiter = ...,def
1345
1345
  correctColumnNum = len(lineCache)
1346
1346
  # if verbose:
1347
1347
  # __teePrintOrNot(f"detected correctColumnNum: {len(lineCache)}",teeLogger=teeLogger)
1348
+ if isinstance(defaults, _DefaultsList) and defaults.columns is None:
1349
+ defaults.columns = correctColumnNum
1348
1350
  if len(lineCache) == 1 or not any(lineCache[1:]):
1349
1351
  if correctColumnNum == 1:
1350
1352
  taskDic[lineCache[0]] = lineCache if not storeOffset else offset
@@ -1364,6 +1366,8 @@ def _processLine(line,taskDic,correctColumnNum,strict = True,delimiter = ...,def
1364
1366
  return correctColumnNum , []
1365
1367
  elif len(lineCache) != correctColumnNum:
1366
1368
  if strict and not any(defaults[1:]):
1369
+ if isinstance(defaults, _DefaultsList):
1370
+ defaults.dropped = True
1367
1371
  if reporter is not None:
1368
1372
  reporter.note('strict-drop', None, 'dropped a line whose column count is not {} (strict mode)'.format(correctColumnNum))
1369
1373
  return correctColumnNum , []
@@ -1605,6 +1609,21 @@ def _headerMatches(header, line, delimiter):
1605
1609
  return _headerCellsMatch(header, _formatHeader(line, delimiter=get_delimiter(delimiter)))
1606
1610
 
1607
1611
 
1612
+ def _isHeaderLine(header, line, delimiter):
1613
+ """Whether ``line`` is the header ``header`` to the CLI's ``--x-header`` rule: the same number of cells, each equal
1614
+ once the whitespace around it (and the line's end, and a UTF-8 byte order mark before it, as Excel writes one) is
1615
+ taken off. Stricter than 3.39's ``_lineContainHeader``."""
1616
+ cells = line.rstrip('\r\n').lstrip('\ufeff').split(delimiter)
1617
+ return len(cells) == len(header) and all(a.strip() == b.strip() for a, b in zip(cells, header))
1618
+
1619
+
1620
+ def _headerMismatch(header, line, teeLogger, strict, delimiter):
1621
+ """Print 3.39's "Header mismatch" (as ``_lineContainHeader`` prints it) for ``line``; raise as it does when strict."""
1622
+ __teePrintOrNot(f"Header mismatch: \n{_formatHeader(line, delimiter=delimiter)} \n!= \n{header}",teeLogger=teeLogger)
1623
+ if strict:
1624
+ raise ValueError("Data format error! Header mismatch")
1625
+
1626
+
1608
1627
  def _lineContainHeader(header,line,verbose = False,teeLogger = None,strict = False,delimiter = ...):
1609
1628
  """
1610
1629
  Verify if a line contains the header.
@@ -2063,7 +2082,10 @@ def _legacyKeptHeader(fileName, header, verifyHeader, verbose, teeLogger, encodi
2063
2082
  ``verifyHeader`` (the header given for a file that holds nothing), else ``header``; [] when it has no cell: the file
2064
2083
  is then left with no header line."""
2065
2084
  if any(header) and verifyHeader:
2066
- line = _legacyFirstLine(fileName, encoding, teeLogger)
2085
+ try:
2086
+ line = _legacyFirstLine(fileName, encoding, teeLogger)
2087
+ except Exception: # a first line the codec cannot decode (gzip: BadGzipFile, zlib.error; bz2, xz, zstd: their own errors): none to keep
2088
+ line = '' # (a scrub or clear makes the file again; an append, which calls _legacyFirstLine itself, raises)
2067
2089
  if line: # a file that holds nothing ('') is an empty store with any header, as the readers take it: the one given is kept
2068
2090
  if warn and not _lineContainHeader(header,line,verbose = verbose,teeLogger = teeLogger,strict = strict,delimiter = delimiter):
2069
2091
  __teePrintOrNot(f'Warning: Header mismatch in {fileName}. Keeping original header in file...','warning',teeLogger)
@@ -2287,6 +2309,8 @@ class _DefaultsList(list):
2287
2309
  replaces its cells in place), not the caller's defaults it began with."""
2288
2310
  found = False
2289
2311
  reset = False # an empty defaults line was read (it stays one whatever follows it)
2312
+ columns = None # the column count the read took (_processLine), None before it took one
2313
+ dropped = False # a strict read dropped a line for its width (_processLine): a read with no row may not be all deletions
2290
2314
  line = None # the cells of that line as the file has them (``defaults`` holds them filled and fitted to the columns)
2291
2315
 
2292
2316
  def __setitem__(self, index, value):
@@ -2295,7 +2319,8 @@ class _DefaultsList(list):
2295
2319
 
2296
2320
 
2297
2321
  def _legacyScrub(fileName,teeLogger = None,header = '',createIfNotExist = False,verifyHeader = True,verbose = False,
2298
- taskDic = None,encoding = 'utf8',strict = False,delimiter = ...,defaults = ...,correctColumnNum = -1,errors = 'strict'):
2322
+ taskDic = None,encoding = 'utf8',strict = False,delimiter = ...,defaults = ...,correctColumnNum = -1,errors = 'strict',
2323
+ notFirst = None):
2299
2324
  """Compact a legacy file in place; return ``(taskDic, done)``: the rows read, and whether the file was written (or
2300
2325
  had nothing to write). ``done`` is False when nothing was written because the file kept changing (the shape of
2301
2326
  ``_specScrub``): it is rewritten only if it is what the read saw (spec §19.8), so a row another process appends
@@ -2305,7 +2330,15 @@ def _legacyScrub(fileName,teeLogger = None,header = '',createIfNotExist = False,
2305
2330
  non-empty one after the rows (those before it keep their empty cells, those after it were filled when read), so
2306
2331
  that every row reads as it did and the header, if any, stays on the first line. An empty one that was read is
2307
2332
  written before the rows (_legacyScrub, ``reset``), as wide as they are. The defaults the caller passed are not
2308
- written. With no header the file begins with an empty line only if the first line of the file was not a row."""
2333
+ written. With no header the file begins with an empty line only if the first line of the file was not a row.
2334
+
2335
+ A file whose read finds no live row (all deleted) is written the same way, with no row: the header line, the defaults
2336
+ lines, and the empty line for a header-less file that began with one. A reset is as wide as the header then, or, with
2337
+ no header, as the lines the read took its column count from. A file that holds nothing (no byte, or only empty streams) is left as it is, and so is one whose
2338
+ strict read dropped a line for its width: that line is not a deletion, and writing the file empty would lose it.
2339
+
2340
+ ``notFirst``, a header list, for a file written with no header: when its first row would be that header
2341
+ (``_isHeaderLine``), the file begins with an empty line instead, so a reader given that header still reads the row."""
2309
2342
  given = list(taskDic.items()) if taskDic else []
2310
2343
  givenDefaults = list(defaults) if defaults not in (..., None) else []
2311
2344
  for tries in range(_SPEC_REWRITE_ATTEMPTS):
@@ -2319,8 +2352,8 @@ def _legacyScrub(fileName,teeLogger = None,header = '',createIfNotExist = False,
2319
2352
  file = readTabularFile(fileName,teeLogger = teeLogger,header = header,createIfNotExist = createIfNotExist,
2320
2353
  verifyHeader = verifyHeader,verbose = verbose,taskDic = taskDic,
2321
2354
  encoding = encoding,strict = strict,delimiter = delimiter,defaults=readDefaults,correctColumnNum = correctColumnNum,errors = errors)
2322
- if not file:
2323
- return file, True
2355
+ if not file and (readDefaults.dropped or not os.path.exists(fileName) or _fileHoldsNothing(fileName)):
2356
+ return file, True # no row, and no file (not made by a scrub), no byte (or only empty streams) to compact, or a line the strict read dropped (not a deletion; it stays): as it is
2324
2357
  dlm = get_delimiter(delimiter,file_name=fileName)
2325
2358
  formatted = _formatHeader(header,delimiter=dlm)
2326
2359
  if not _verifyFileExistence(fileName,createIfNotExist = True,teeLogger = teeLogger,header = formatted,encoding = encoding,strict = False,delimiter=dlm,errors = errors):
@@ -2331,6 +2364,17 @@ def _legacyScrub(fileName,teeLogger = None,header = '',createIfNotExist = False,
2331
2364
  payload, count, columns = _legacyFormatRows(fileName, file, teeLogger, formatted, verifyHeader, verbose, encoding,
2332
2365
  strict, dlm, errors, assume=_sanitize(kept,delimiter=dlm) if kept else [])
2333
2366
  blank = not kept and _legacyBeginsBlank(fileName, encoding, teeLogger, dlm)
2367
+ if not kept and not blank and notFirst and count and \
2368
+ _isHeaderLine(notFirst, payload.split(b'\n', 1)[0].decode(encoding or 'utf8', errors='replace'), dlm):
2369
+ blank = True
2370
+ if not count:
2371
+ # No row to take the width from: a reader takes it from the header line, else from the first line it reads,
2372
+ # which a defaults line written here would be, so it is as wide as the file's lines were.
2373
+ columns = len(kept) or readDefaults.columns or 0
2374
+ last = readDefaults.line if readDefaults.found else None
2375
+ last = last if last and len(last) > 1 else None
2376
+ if not count and not kept and ((readDefaults.reset and columns > 1) or last):
2377
+ blank = True # a defaults line is never the first line of a file with no header: a reader given one would take it for it
2334
2378
  if readDefaults.reset and columns > 1:
2335
2379
  # An empty defaults line was read: no defaults reach the rows that follow it, whoever reads them (the rows
2336
2380
  # were read with it, and any the caller of that read gave are not written). Written before the rows, so
@@ -2343,8 +2387,9 @@ def _legacyScrub(fileName,teeLogger = None,header = '',createIfNotExist = False,
2343
2387
  else:
2344
2388
  first, newline, rest = payload.partition(b'\n')
2345
2389
  payload = first + newline + reset + rest
2346
- last = readDefaults.line if readDefaults.found else None
2347
- if last and len(last) > 1:
2390
+ if last:
2391
+ if not count and columns > 1:
2392
+ last = (list(last) + [''] * columns)[:columns] # (the same defaults to a reader, fitted to the width of the file as it fits a line)
2348
2393
  payload += _encodeRow(_sanitize(last,delimiter=dlm),dlm,encoding,errors,what='defaults') + b'\n'
2349
2394
  if _legacyReplace(fileName, kept, blank, encoding, dlm, errors, payload, expect=stamp) is None:
2350
2395
  continue # another writer changed the file after the read
@@ -4299,7 +4344,7 @@ class _TSVZedLoad(OrderedDict):
4299
4344
 
4300
4345
  Its rows are shaped as ``TSVZed.__setitem__`` shapes them in memory only (keys and cells right-stripped,
4301
4346
  the column count and the defaults applied), with a column count and defaults of its own. ``dirty`` tells
4302
- that the file holds a key twice.
4347
+ that the file holds a key twice (two values, or a row and its deletion).
4303
4348
  """
4304
4349
 
4305
4350
  def __init__(self, owner, columns, defaults):
@@ -4349,6 +4394,7 @@ class _TSVZedLoad(OrderedDict):
4349
4394
  def __delitem__(self, key):
4350
4395
  key = str(key)
4351
4396
  OrderedDict.__delitem__(self, key if OrderedDict.__contains__(self, key) else key.rstrip())
4397
+ self.dirty = True # the file holds the key twice: its row, and this deletion
4352
4398
 
4353
4399
  def replay(self, rows):
4354
4400
  """Apply rows queued for the file, as reading them back will: a row whose values are all empty deletes."""
@@ -6106,8 +6152,8 @@ memoryOnly:{self.memoryOnly}
6106
6152
 
6107
6153
  def get_file_obj(self,modes = 'ab'):
6108
6154
  """The store's file opened in ``modes`` and locked, with writeLock held until release_file_obj() (3.39 API; the
6109
- store's own writes do not use it). 'wb' empties it once locked, a compressed one then written through its
6110
- codec. None when it cannot be opened or locked (reported)."""
6155
+ store's own writes do not use it). 'wb' empties it once locked; for a compressed file 'w' and 'a' modes give
6156
+ a writer through its codec over the locked file (a new member at the end for 'a'). None when it cannot be opened or locked (reported)."""
6111
6157
  self.writeLock.acquire()
6112
6158
  file = None
6113
6159
  try:
@@ -6116,12 +6162,21 @@ memoryOnly:{self.memoryOnly}
6116
6162
  file = _openUntruncated(self._fileName)
6117
6163
  elif modes == 'a+b':
6118
6164
  file = open(self._fileName, 'a+b')
6165
+ elif 'a' in modes and '+' not in modes and _fileCodec(self._fileName):
6166
+ _requireCodec(_fileCodec(self._fileName))
6167
+ file = open(self._fileName, 'ab') # the codec writes into this locked file, a new member at its end
6119
6168
  else:
6120
6169
  file = openFileAsCompressed(self._fileName, mode=modes, encoding=self.encoding, teeLogger=self.teeLogger)
6121
6170
  _lockFile(file)
6122
6171
  if 'w' in modes:
6123
6172
  file.truncate(0)
6124
6173
  file = _compressedWriter(self._fileName, file) or file
6174
+ elif 'a' in modes and '+' not in modes and _fileCodec(self._fileName):
6175
+ raw = file
6176
+ file = _compressedWriter(self._fileName, raw) # (release_file_obj closes it, trailer and all, before the unlock)
6177
+ if 'b' not in modes:
6178
+ file = io.TextIOWrapper(file, encoding=self.encoding)
6179
+ file._tsvzRaw = raw
6125
6180
  if self.verbose:
6126
6181
  self.__teePrintOrNot(f"File {self._fileName} locked with mode {modes}")
6127
6182
  return file
@@ -7090,12 +7145,13 @@ class _ServeStore(object):
7090
7145
  takes ``self.lock``; ``log`` is the teeLogger its messages go to.
7091
7146
 
7092
7147
  ``header`` and ``createDefaults`` (default ``defaults``) are what a missing store is created with
7093
- (``create``). ``checkHeader``: writes and ``clear`` also check ``header`` as 3.39 did (the CLI's
7094
- ``--x-header``). ``lazy``: the store is read at the first request that needs it, not here.
7148
+ (``create``). ``header`` is also the CLI's ``--x-header``: a spec store's writes and ``clear`` check it as
7149
+ 3.39 did, and a loose file takes it by one rule for every request (``_legacyHeader``). ``lazy``: the store is
7150
+ read at the first request that needs it, not here.
7095
7151
  """
7096
7152
 
7097
7153
  def __init__(self, fileName, delimiter=..., header='', defaults=None, strict=False, teeLogger=None,
7098
- verbose=False, create=False, createDefaults=None, checkHeader=False, lazy=False):
7154
+ verbose=False, create=False, createDefaults=None, lazy=False):
7099
7155
  self.path = fileName
7100
7156
  self.spec = _isSpecPath(fileName)
7101
7157
  if self.spec:
@@ -7112,7 +7168,9 @@ class _ServeStore(object):
7112
7168
  self.unreadable = False
7113
7169
  self.stopped = False
7114
7170
  self.parts = self.state = None # not read yet
7115
- self.header = _formatHeader(header, teeLogger=teeLogger, delimiter=self.delimiter) if checkHeader else ['']
7171
+ self.given = _formatHeader(header, teeLogger=teeLogger, delimiter=self.delimiter) if header else ['']
7172
+ self.header = self.given # a loose file's: the header in effect, as _legacyHeader last found it
7173
+ self.mismatch = None # the line 1 last warned of (_legacyHeader)
7116
7174
  if create:
7117
7175
  self._create(header, self.defaults if createDefaults is None else createDefaults)
7118
7176
  if not lazy:
@@ -7138,12 +7196,42 @@ class _ServeStore(object):
7138
7196
  if self.spec:
7139
7197
  self._loadSpec(reporter, log)
7140
7198
  else:
7141
- self._loadLegacy(reporter)
7199
+ self._loadLegacy(reporter, log)
7142
7200
  self.unreadable = reporter.incomplete()
7143
7201
  self.stopped = reporter.has('stopped') # a stop policy ended the read (spec §4.10)
7202
+ except Exception:
7203
+ self.parts = None # (a strict header mismatch): the next request reads again, and fails again
7204
+ raise
7144
7205
  finally:
7145
7206
  reporter.flush()
7146
7207
 
7208
+ def _legacyHeader(self, line, log):
7209
+ """The header in effect for a loose file whose first line is ``line``: the one rule of ``--x-header``.
7210
+
7211
+ ``line`` is None when the file is missing, holds nothing, or its first line cannot be read. With a header
7212
+ given, line 1 is the header when it is that header (``_isHeaderLine``: the same cells, each without the
7213
+ whitespace around it), or when there is no line 1 yet (a write puts the header first). Otherwise 3.39's
7214
+ "Header mismatch" is printed and strict raises (nothing is written); loose, the file has no header and line 1
7215
+ is a row, for every request, the scrub included. Sets and returns ``self.header``."""
7216
+ header = self.given
7217
+ if any(header) and line is not None and not _isHeaderLine(header, line, self.delimiter):
7218
+ if line != self.mismatch or self.strict: # (said once for the line, as 3.39 says it)
7219
+ self.mismatch = line
7220
+ _headerMismatch(header, line, log if log is not None else self.teeLogger, self.strict, self.delimiter)
7221
+ header = ['']
7222
+ self.header = header
7223
+ return header
7224
+
7225
+ def _legacyHeaderNow(self, log):
7226
+ """``_legacyHeader`` for the file as it is now (a write, ``clear`` or ``scrub`` that does not read it)."""
7227
+ line = None
7228
+ if any(self.given) and os.path.isfile(self.path):
7229
+ try:
7230
+ line = _legacyFirstLine(self.path, 'utf8', self.teeLogger) or None
7231
+ except Exception: # a first line the codec cannot decode: the write or scrub that reads it reports it
7232
+ pass
7233
+ return self._legacyHeader(line, log)
7234
+
7147
7235
  def _noteDirMtime(self, dirMtime):
7148
7236
  self.dirMtime = dirMtime
7149
7237
  self.dirRacy = time.time() - dirMtime / 1e9 < _SERVE_RACY_SECONDS
@@ -7181,19 +7269,33 @@ class _ServeStore(object):
7181
7269
  self.label = os.path.basename(self.parts[-1]) + ' ' if len(self.parts) > 1 else ''
7182
7270
  self.check = info.check if info is not None else b'' # as read: the part may be rewritten since
7183
7271
 
7184
- def _loadLegacy(self, reporter):
7272
+ def _loadLegacy(self, reporter, log):
7185
7273
  self.parts = [self.path] if os.path.isfile(self.path) else []
7186
7274
  self.stamps = self._stamps(self.parts)
7187
7275
  self.data = self.mapping()
7188
7276
  self.legacyDefaults = list(self.defaults)
7189
7277
  self.width, self.offset, self.lineNo, self.tailSeen = -1, 0, 0, b''
7278
+ self.header = self.given # (no line 1: a write puts the header first)
7190
7279
  # A compressed file with no byte, or only empty streams, is an empty store, as readTabularFile reads it.
7191
7280
  if self.parts and not (_isCompressedFile(self.path) and _fileHoldsNothing(self.path)):
7192
7281
  try:
7193
7282
  with openFileAsCompressed(self.path, mode='rb', teeLogger=self.teeLogger) as f:
7283
+ if any(self.given):
7284
+ try:
7285
+ raw = f.readline()
7286
+ except Exception as e: # L5: a stream damaged inside line 1 holds nothing more to read
7287
+ if not _isCompressedFile(self.path):
7288
+ raise
7289
+ reporter.note('damaged', None, 'compressed stream is damaged ({}: {}); read up to the '
7290
+ 'damage'.format(type(e).__name__, e))
7291
+ return
7292
+ if raw and any(self._legacyHeader(raw.decode('utf8', errors='replace'), log)):
7293
+ self.width, self.lineNo = len(self.header), 1 # the header: not a row
7294
+ elif raw:
7295
+ self._apply(raw, reporter) # line 1 is a row
7194
7296
  self.width, self.offset, self.lineNo = _legacyReadLoop(
7195
- f, self.path, self.data, -1, 0, self.strict, self.delimiter, self.legacyDefaults, False,
7196
- 'utf8', reporter)
7297
+ f, self.path, self.data, self.width, self.lineNo, self.strict, self.delimiter,
7298
+ self.legacyDefaults, False, 'utf8', reporter)
7197
7299
  except OSError as e:
7198
7300
  reporter.note('unreadable', None, 'could not read {} ({})'.format(self.path, e))
7199
7301
  self.label = ''
@@ -7236,7 +7338,8 @@ class _ServeStore(object):
7236
7338
  """Replay what was appended to the active part; False when it changed some other way."""
7237
7339
  old = self.stamps.get(path)
7238
7340
  if (self.stopped or _parsePartName(path).codec or old is None or stamp[0] != old[0]
7239
- or stamp[1] < self.offset or stamp[1] <= old[1]):
7341
+ or stamp[1] < self.offset or stamp[1] <= old[1] or (not self.spec and self.lineNo <= 1 and any(self.given))):
7342
+ # (a loose file's line 1 is read by a reload, which tells whether it is the header, also once it is whole)
7240
7343
  return False # an append always grows the part; anything else is a rewrite (and a stopped read is read again)
7241
7344
  try:
7242
7345
  with open(path, 'rb') as f:
@@ -7389,6 +7492,7 @@ class _ServeStore(object):
7389
7492
  if rows and any(self.header):
7390
7493
  _specCheckHeader(self.path, self.header, self.delimiter, self.strict, self.verbose, log)
7391
7494
  return _specFormatRecords(rows, self.delimiter).encode('utf-8')
7495
+ self._legacyHeaderNow(log)
7392
7496
  # Rows for a file that holds nothing yet are fitted to the header it is about to begin with.
7393
7497
  assume = self.header if any(self.header) and _fileHoldsNothing(self.path) else None
7394
7498
  return _legacyFormatPayload(self.path, [cells for cells, marker in rows], log, self.header, True, self.verbose,
@@ -7503,9 +7607,12 @@ class _ServeStore(object):
7503
7607
  with self.lock:
7504
7608
  self.writer.barrier()
7505
7609
  try:
7506
- clear = _specClearTabularFile if self.spec else clearTabularFile
7507
- return clear(self.path, teeLogger=log, header=self.header, verifyHeader=self.strict,
7508
- verbose=self.verbose, delimiter=self.delimiter) is not False
7610
+ if self.spec:
7611
+ return _specClearTabularFile(self.path, teeLogger=log, header=self.header, verifyHeader=self.strict,
7612
+ verbose=self.verbose, delimiter=self.delimiter) is not False
7613
+ self._legacyHeaderNow(log) # a strict mismatch is an error; else the file is left with the header given
7614
+ return clearTabularFile(self.path, teeLogger=log, header=self.given, verifyHeader=False,
7615
+ verbose=self.verbose, delimiter=self.delimiter) is not False
7509
7616
  finally:
7510
7617
  self.parts = None # read again at the next request
7511
7618
 
@@ -7518,8 +7625,13 @@ class _ServeStore(object):
7518
7625
  if self.spec:
7519
7626
  return _specScrub(self.path, teeLogger=log, verifyHeader=False, verbose=self.verbose,
7520
7627
  strict=self.strict, delimiter=self.delimiter, defaults=self.defaults)[1]
7521
- return _legacyScrub(self.path, teeLogger=log, verifyHeader=False, verbose=self.verbose,
7522
- strict=self.strict, delimiter=self.delimiter, defaults=self.defaults)[1]
7628
+ # Line 1 is the header exactly when every read takes it for one, so the scrub keeps what they read; in a
7629
+ # file with no header, a row that is the header given is never written as line 1 (reads would take it
7630
+ # for the header).
7631
+ header = self._legacyHeaderNow(log)
7632
+ return _legacyScrub(self.path, teeLogger=log, header=header, verifyHeader=True, verbose=self.verbose,
7633
+ strict=self.strict, delimiter=self.delimiter, defaults=self.defaults,
7634
+ notFirst=self.given if any(self.given) and not any(header) else None)[1]
7523
7635
  finally:
7524
7636
  self.parts = None # read again at the next request
7525
7637
 
@@ -7543,9 +7655,9 @@ class _ServeStore(object):
7543
7655
  else:
7544
7656
  codec = _fileCodec(self.path)
7545
7657
  _requireCodec(codec)
7546
- header = None # a file that holds nothing gets the header first (_appendOpen)
7547
- if any(self.header):
7548
- header = functools.partial(_encodeRow, _sanitize(self.header, delimiter=self.delimiter),
7658
+ header = None # a file that holds nothing gets the header given first (_appendOpen)
7659
+ if any(self.given):
7660
+ header = functools.partial(_encodeRow, _sanitize(self.given, delimiter=self.delimiter),
7549
7661
  self.delimiter, 'utf8', 'strict', what='header')
7550
7662
  _lockedAppend(self.path, payload, 'newline', reporter, fsync=fsync, header=header, codec=codec)
7551
7663
  finally:
@@ -8614,7 +8726,8 @@ options:
8614
8726
  -q, --quiet print errors only
8615
8727
  -v, --verbose print more detail
8616
8728
  -d, --delimiter D .tsv-family delimiter: tab, comma, pipe, null or one character
8617
- --x-header H header to check, or to create a new file with
8729
+ --x-header H a .tsv's header: line 1 when it is H, else a row
8730
+ (--x-strict: an error); a new file begins with H
8618
8731
  --x-defaults D defaults row KEY D1 D2 ... joined by the delimiter (KEY is ignored)
8619
8732
  --x-strict, --x-force turn the 3.39 column and header checks on or off (default: off)
8620
8733
  --x-direct work on the files even when a server runs
@@ -8814,14 +8927,17 @@ def _cliTableCells(rows):
8814
8927
  return [[_CLI_CONTROL_RE.sub(escape, str(cell)) for cell in row] for row in rows]
8815
8928
 
8816
8929
 
8817
- def _cliEmitRows(rows, args, stdout, spec, delimiter, headerRow=False):
8930
+ def _cliEmitRows(rows, args, stdout, spec, delimiter, headerRow=False, header=None):
8818
8931
  """Print store rows for ``read`` / ``get`` (spec §20.4): §13 records, 3.39 records or a table.
8819
8932
 
8820
8933
  The table shows the rows only, each padded to the widest and none truncated to the
8821
8934
  terminal's width. ``headerRow`` keeps 3.39's ``read`` table of a loose file, whose
8822
- first row is the file's header line.
8935
+ first row is the file's header line; ``header``, a loose file's header line that is not
8936
+ a row (``--x-header``), is shown there instead. Records never hold it.
8823
8937
  """
8824
8938
  if _cliFormat(args, stdout) == 'table':
8939
+ if headerRow and header:
8940
+ rows = [header] + rows
8825
8941
  rows = _cliTableCells(rows)
8826
8942
  if headerRow or not rows:
8827
8943
  _cliWrite(stdout, pretty_format_table(rows, delimiter=delimiter) + '\n')
@@ -8952,16 +9068,16 @@ def _cliRequestLine(args):
8952
9068
  return '\t'.join([args.operation] + [_serveEncode(arg) for arg in args.args])
8953
9069
 
8954
9070
 
8955
- def _cliEmitServed(args, delimiter, logger, stdout, response):
9071
+ def _cliEmitServed(args, delimiter, logger, stdout, response, header=None):
8956
9072
  """Print a response (``(rows, diagnostics, status, message)``) as the operation prints on the files (§20.8);
8957
- return the exit status."""
9073
+ return the exit status. ``header``: a loose file's header line in effect (``read``'s table shows it)."""
8958
9074
  rows, diagnostics, status, message = response
8959
9075
  for text in diagnostics:
8960
9076
  logger.teelog(text, 'warning')
8961
9077
  spec = _isSpecPath(args.store)
8962
9078
  operation = args.operation
8963
9079
  if operation in ('read', 'get', 'keys', 'pop', 'popitem', 'setdefault'):
8964
- _cliEmitRows(rows, args, stdout, spec, delimiter, headerRow=operation == 'read' and not spec)
9080
+ _cliEmitRows(rows, args, stdout, spec, delimiter, headerRow=operation == 'read' and not spec, header=header)
8965
9081
  elif operation == 'len':
8966
9082
  _cliWrite(stdout, ''.join(row[0] + '\n' for row in rows))
8967
9083
  elif operation == 'verify':
@@ -9026,8 +9142,7 @@ def _cliEngineOp(args, delimiter, logger, stdin, stdout):
9026
9142
  store = _ServeStore(args.store, delimiter=delimiter, header=args.header, defaults=args.defaults,
9027
9143
  strict=args.strict, teeLogger=logger, verbose=args.verbose,
9028
9144
  create=not exists and operation != 'clear', # clear creates the store itself
9029
- createDefaults=None if operation == 'setdefault' else [],
9030
- checkHeader=operation in ('set', 'append', 'delete', 'clear'), lazy=True) # as 3.39 did
9145
+ createDefaults=None if operation == 'setdefault' else [], lazy=True)
9031
9146
 
9032
9147
  def request(line, bulk=None):
9033
9148
  """The response to ``line``; the engine's messages go to stderr as they come, at their own level."""
@@ -9043,7 +9158,7 @@ def _cliEngineOp(args, delimiter, logger, stdin, stdout):
9043
9158
  response = request('\t'.join(written + [_cliRequestLine(args)]), bulk)
9044
9159
  finally:
9045
9160
  store.close()
9046
- return _cliEmitServed(args, delimiter, logger, stdout, response)
9161
+ return _cliEmitServed(args, delimiter, logger, stdout, response, header=store.header if any(store.header) else None)
9047
9162
 
9048
9163
 
9049
9164
  def _cliServe(args, delimiter, logger, stdin, stdout):
@@ -9073,7 +9188,7 @@ def _cliServe(args, delimiter, logger, stdin, stdout):
9073
9188
  restore = _serveSignals() # before the pointer file announces this handler
9074
9189
  try:
9075
9190
  store = _ServeStore(args.store, delimiter=delimiter, header=args.header, createDefaults=args.defaults,
9076
- teeLogger=logger, verbose=args.verbose, create=True)
9191
+ strict=args.strict, teeLogger=logger, verbose=args.verbose, create=True)
9077
9192
  handle.start(store)
9078
9193
  logger.teelog('tsvz: serving {} at {} (pid {})'.format(args.store, handle.address, os.getpid()))
9079
9194
  handle.serve()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes