python-dwca-reader 0.16.4__py3-none-any.whl → 0.17.0__py3-none-any.whl

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.
@@ -0,0 +1,617 @@
1
+ """Characterization tests: these pin CURRENT behavior so a rewrite of the parsing
2
+ engine produces a visible diff rather than a silent change.
3
+
4
+ Tests marked with a `# CHARACTERIZATION: wrong, see B<n>` comment assert behavior we
5
+ know to be incorrect. Phase 1 changes them deliberately.
6
+ """
7
+
8
+ import unittest
9
+ from array import array
10
+
11
+ import pytest
12
+
13
+ from dwca.exceptions import InvalidArchive
14
+ from dwca.read import DwCAReader
15
+
16
+ from .archive_builder import build_archive, temp_archive_dir
17
+ from .helpers import sample_data_path
18
+
19
+ TERM1 = "http://rs.tdwg.org/dwc/terms/term1"
20
+ VERNACULAR_TERM = "http://rs.tdwg.org/dwc/terms/vernacularName"
21
+
22
+
23
+ class TestArchiveBuilder(unittest.TestCase):
24
+ def test_builds_a_readable_archive(self):
25
+ path = build_archive(
26
+ temp_archive_dir(self), rows=[["1", "Borneo"], ["2", "Mumbai"]]
27
+ )
28
+
29
+ with DwCAReader(path) as dwca:
30
+ rows = list(dwca)
31
+
32
+ assert 2 == len(rows)
33
+ assert "1" == rows[0].id
34
+ assert "Borneo" == rows[0].data[TERM1]
35
+ assert "Mumbai" == rows[1].data[TERM1]
36
+
37
+ def test_raw_payload_is_written_verbatim(self):
38
+ path = build_archive(
39
+ temp_archive_dir(self), rows=[], columns=2, raw_payload=b"1\tBorneo\n"
40
+ )
41
+
42
+ with DwCAReader(path) as dwca:
43
+ assert ["Borneo"] == [row.data[TERM1] for row in dwca]
44
+
45
+
46
+ class TestHeaderLines(unittest.TestCase):
47
+ """ignoreHeaderLines, for BOTH access paths.
48
+
49
+ The two paths used to disagree: DwCAReader iteration went through get_row_by_position()
50
+ (which offset correctly), while CSVDataFile.__iter__ used readlines(hint), where the
51
+ argument is a byte-size hint rather than a line count. coreid_index was built from the
52
+ second path, so it used to pick up leftover header lines. Both paths now agree.
53
+ """
54
+
55
+ def _archive(self, ignore_header_lines, header_rows):
56
+ return build_archive(
57
+ temp_archive_dir(self),
58
+ rows=[["1", "Borneo"], ["2", "Mumbai"]],
59
+ ignore_header_lines=ignore_header_lines,
60
+ header_rows=header_rows,
61
+ )
62
+
63
+ def test_no_header(self):
64
+ path = self._archive(0, [])
65
+
66
+ with DwCAReader(path) as dwca:
67
+ assert ["1", "2"] == [row.id for row in dwca]
68
+ assert "1" == dwca.core_file.get_row_by_position(0).id
69
+
70
+ def test_one_header(self):
71
+ path = self._archive(1, [["id", "locality"]])
72
+
73
+ with DwCAReader(path) as dwca:
74
+ assert ["1", "2"] == [row.id for row in dwca]
75
+ assert "1" == dwca.core_file.get_row_by_position(0).id
76
+ assert {"1": [0], "2": [1]} == {
77
+ k: list(v) for k, v in dwca.core_file.coreid_index.items()
78
+ }
79
+
80
+ def test_two_headers_iteration_and_random_access_agree(self):
81
+ path = self._archive(2, [["idA", "locA"], ["idB", "locB"]])
82
+
83
+ with DwCAReader(path) as dwca:
84
+ assert ["1", "2"] == [row.id for row in dwca]
85
+ assert "1" == dwca.core_file.get_row_by_position(0).id
86
+
87
+ assert {"1": [0], "2": [1]} == {
88
+ k: list(v) for k, v in dwca.core_file.coreid_index.items()
89
+ }
90
+
91
+
92
+ class TestLineTerminators(unittest.TestCase):
93
+ def test_unix_terminator(self):
94
+ path = build_archive(
95
+ temp_archive_dir(self),
96
+ rows=[["1", "Borneo"], ["2", "Mumbai"]],
97
+ lines_terminated_by="\n",
98
+ )
99
+
100
+ with DwCAReader(path) as dwca:
101
+ assert ["Borneo", "Mumbai"] == [row.data[TERM1] for row in dwca]
102
+
103
+ def test_dos_terminator(self):
104
+ path = build_archive(
105
+ temp_archive_dir(self),
106
+ rows=[["1", "Borneo"], ["2", "Mumbai"]],
107
+ lines_terminated_by="\r\n",
108
+ )
109
+
110
+ with DwCAReader(path) as dwca:
111
+ assert ["Borneo", "Mumbai"] == [row.data[TERM1] for row in dwca]
112
+
113
+ def test_crlf_file_declaring_a_bare_lf_terminator(self):
114
+ """A CRLF file whose Metafile declares "\\n" must not leave a stray CR on the row.
115
+
116
+ This combination is common rather than exotic: git checks text files out with CRLF on
117
+ Windows, so an archive committed to a repository hits it without anyone choosing it.
118
+ The library's own dwca-simple-dir fixture behaves this way on a Windows runner, which
119
+ is how a regression here was caught.
120
+ """
121
+ path = build_archive(
122
+ temp_archive_dir(self),
123
+ rows=[],
124
+ columns=2,
125
+ raw_payload=b"1\tBorneo\r\n2\tMumbai\r\n",
126
+ )
127
+
128
+ with DwCAReader(path) as dwca:
129
+ streamed = [row.data[TERM1] for row in dwca]
130
+ seeked = [
131
+ dwca.core_file.get_row_by_position(i).data[TERM1] for i in range(2)
132
+ ]
133
+
134
+ assert ["Borneo", "Mumbai"] == streamed
135
+ assert streamed == seeked
136
+
137
+ def test_multichar_terminator_is_rejected_by_python_io(self):
138
+ path = build_archive(
139
+ temp_archive_dir(self),
140
+ rows=[["1", "Borneo"]],
141
+ lines_terminated_by="@@\n",
142
+ )
143
+
144
+ # io.open() only accepts None, '', '\n', '\r' and '\r\n' as newline, so an archive
145
+ # declaring anything else cannot be opened at all. Pinned so a rewrite that moves
146
+ # off the newline= parameter has to decide what to do about it.
147
+ with pytest.raises(ValueError):
148
+ DwCAReader(path)
149
+
150
+ def test_unicode_next_line_does_not_split_a_row(self):
151
+ """U+0085 must not be treated as a line break (issue #20).
152
+
153
+ str.splitlines() splits on U+0085 but io line iteration does not, so any rewrite
154
+ using splitlines() would silently double the row count here.
155
+ """
156
+ path = build_archive(
157
+ temp_archive_dir(self),
158
+ rows=[],
159
+ columns=2,
160
+ raw_payload=b"1\tbefore\xc2\x85after\n", # U+0085 encoded as UTF-8
161
+ )
162
+
163
+ with DwCAReader(path) as dwca:
164
+ rows = list(dwca)
165
+
166
+ assert 1 == len(rows)
167
+ assert "before\u0085after" == rows[0].data[TERM1]
168
+
169
+
170
+ class TestEncodings(unittest.TestCase):
171
+ def test_windows1252_data_file(self):
172
+ path = build_archive(
173
+ temp_archive_dir(self),
174
+ rows=[["1", "caf\xe9"], ["2", "na\xefve"]],
175
+ encoding="windows-1252",
176
+ )
177
+
178
+ with DwCAReader(path) as dwca:
179
+ assert ["caf\xe9", "na\xefve"] == [row.data[TERM1] for row in dwca]
180
+
181
+ def test_latin1_data_file(self):
182
+ path = build_archive(
183
+ temp_archive_dir(self), rows=[["1", "caf\xe9"]], encoding="latin-1"
184
+ )
185
+
186
+ with DwCAReader(path) as dwca:
187
+ assert ["caf\xe9"] == [row.data[TERM1] for row in dwca]
188
+
189
+ def test_utf8_bom_leaks_into_the_first_field(self):
190
+ path = build_archive(
191
+ temp_archive_dir(self),
192
+ rows=[],
193
+ columns=2,
194
+ raw_payload=b"\xef\xbb\xbf1\tBorneo\n", # UTF-8 byte order mark
195
+ )
196
+
197
+ with DwCAReader(path) as dwca:
198
+ rows = list(dwca)
199
+
200
+ # Surprising but not a known bug: the encoding is "utf-8", not "utf-8-sig", so the
201
+ # byte order mark becomes part of the id instead of being stripped. A rewrite adding
202
+ # BOM handling would change this deliberately.
203
+ assert "\ufeff1" == rows[0].id
204
+
205
+ def test_undecodable_byte_does_not_break_random_access(self):
206
+ path = build_archive(
207
+ temp_archive_dir(self),
208
+ rows=[],
209
+ columns=2,
210
+ raw_payload=b"1\tcaf\xe9\n2\tMumbai\n3\tBorneo\n",
211
+ )
212
+
213
+ with DwCAReader(path) as dwca:
214
+ # Row 0 is fine: the offset index has not drifted yet.
215
+ assert "caf\ufffd" == dwca.core_file.get_row_by_position(0).data[TERM1]
216
+
217
+ assert "Mumbai" == dwca.core_file.get_row_by_position(1).data[
218
+ "http://rs.tdwg.org/dwc/terms/term1"
219
+ ]
220
+
221
+ assert ["caf\ufffd", "Mumbai", "Borneo"] == [
222
+ row.data["http://rs.tdwg.org/dwc/terms/term1"] for row in dwca
223
+ ]
224
+
225
+
226
+ class TestQuoting(unittest.TestCase):
227
+ def _read_localities(self, payload):
228
+ path = build_archive(
229
+ temp_archive_dir(self),
230
+ rows=[],
231
+ columns=2,
232
+ fields_terminated_by=",",
233
+ fields_enclosed_by='"',
234
+ raw_payload=payload,
235
+ )
236
+
237
+ with DwCAReader(path) as dwca:
238
+ return [row.data[TERM1] for row in dwca]
239
+
240
+ def test_delimiter_inside_a_quoted_field(self):
241
+ """Regression guard for the v0.11.0 fix. Any hand-rolled parser breaks this."""
242
+ assert ["plain, with comma"] == self._read_localities(
243
+ b'"1","plain, with comma"\n'
244
+ )
245
+
246
+ def test_quote_in_the_middle_of_content(self):
247
+ assert ['say "hi" there'] == self._read_localities(b'"1","say ""hi"" there"\n')
248
+
249
+ def test_quote_at_the_edge_of_content_is_preserved(self):
250
+ assert ['say "hi"'] == self._read_localities(b'"1","say ""hi"""\n')
251
+ assert ['"hi" she said'] == self._read_localities(b'"1","""hi"" she said"\n')
252
+
253
+ def test_quote_characters_are_kept_when_the_archive_declares_no_enclosure(self):
254
+ path = build_archive(
255
+ temp_archive_dir(self),
256
+ rows=[],
257
+ columns=2,
258
+ raw_payload=b'1\t"betta" splendens\n',
259
+ )
260
+
261
+ with DwCAReader(path) as dwca:
262
+ assert '"betta" splendens' == list(dwca)[0].data[TERM1]
263
+
264
+
265
+ class TestDegenerateRows(unittest.TestCase):
266
+ def test_row_with_fewer_columns_than_declared(self):
267
+ path = build_archive(
268
+ temp_archive_dir(self), rows=[], columns=3, raw_payload=b"1\tBorneo\n"
269
+ )
270
+
271
+ with DwCAReader(path) as dwca:
272
+ with pytest.raises(InvalidArchive):
273
+ list(dwca)
274
+
275
+ def test_row_with_more_columns_than_declared(self):
276
+ path = build_archive(
277
+ temp_archive_dir(self),
278
+ rows=[],
279
+ columns=2,
280
+ raw_payload=b"1\tBorneo\textra\n",
281
+ )
282
+
283
+ with DwCAReader(path) as dwca:
284
+ rows = list(dwca)
285
+
286
+ # Extra columns are ignored by data but kept in raw_fields.
287
+ assert "Borneo" == rows[0].data[TERM1]
288
+ assert ["1", "Borneo", "extra"] == rows[0].raw_fields
289
+
290
+ def test_blank_line_in_the_middle(self):
291
+ path = build_archive(
292
+ temp_archive_dir(self),
293
+ rows=[],
294
+ columns=2,
295
+ raw_payload=b"1\tBorneo\n\n2\tMumbai\n",
296
+ )
297
+
298
+ with DwCAReader(path) as dwca:
299
+ with pytest.raises(InvalidArchive):
300
+ list(dwca)
301
+
302
+ def test_no_trailing_newline_at_eof(self):
303
+ path = build_archive(
304
+ temp_archive_dir(self),
305
+ rows=[],
306
+ columns=2,
307
+ raw_payload=b"1\tBorneo\n2\tMumbai",
308
+ )
309
+
310
+ with DwCAReader(path) as dwca:
311
+ assert ["Borneo", "Mumbai"] == [row.data[TERM1] for row in dwca]
312
+
313
+ def test_empty_core_file(self):
314
+ path = build_archive(
315
+ temp_archive_dir(self), rows=[], columns=2, raw_payload=b""
316
+ )
317
+
318
+ with DwCAReader(path) as dwca:
319
+ assert [] == list(dwca)
320
+ assert [] == dwca.rows
321
+
322
+ def test_header_only_core_file(self):
323
+ path = build_archive(
324
+ temp_archive_dir(self),
325
+ rows=[],
326
+ columns=2,
327
+ ignore_header_lines=1,
328
+ raw_payload=b"id\tlocality\n",
329
+ )
330
+
331
+ with DwCAReader(path) as dwca:
332
+ assert [] == list(dwca)
333
+
334
+
335
+ class TestIterationSemantics(unittest.TestCase):
336
+ def _archive(self):
337
+ return build_archive(
338
+ temp_archive_dir(self),
339
+ rows=[["1", "Borneo"], ["2", "Mumbai"], ["3", "Paris"]],
340
+ )
341
+
342
+ def test_sequential_re_iteration(self):
343
+ with DwCAReader(self._archive()) as dwca:
344
+ assert 3 == len(list(dwca))
345
+ assert 3 == len(list(dwca))
346
+
347
+ def test_nested_iteration(self):
348
+ with DwCAReader(self._archive()) as dwca:
349
+ pairs = [(outer.id, inner.id) for outer in dwca for inner in dwca]
350
+
351
+ assert 9 == len(pairs)
352
+
353
+ def test_lookup_inside_a_loop(self):
354
+ seen = []
355
+ with DwCAReader(self._archive()) as dwca:
356
+ for row in dwca:
357
+ seen.append(row.id)
358
+ assert "2" == dwca.get_corerow_by_id("2").id
359
+
360
+ assert ["1", "2", "3"] == seen
361
+
362
+ def test_random_access_interleaved_with_iteration_is_safe(self):
363
+ with DwCAReader(self._archive()) as dwca:
364
+ seen = []
365
+ for row in dwca:
366
+ seen.append(row.id)
367
+ # This path is offset-based, so it does not disturb iteration.
368
+ assert "1" == dwca.core_file.get_row_by_position(0).id
369
+
370
+ assert ["1", "2", "3"] == seen
371
+
372
+
373
+ class TestExtensionFiles(unittest.TestCase):
374
+ """extension= was never passed to build_archive by any test, so that whole branch of the
375
+ builder was dead code, and every extension-bearing behavior below ran only on the three
376
+ bundled sample archives, which all happen to share one configuration (utf-8, tab, no
377
+ enclosure, ignoreHeaderLines="1"). CSVDataFile.coreid_index used to be built through
378
+ CSVDataFile.__iter__ (the readlines(byte-hint) header bug, see TestHeaderLines above and
379
+ B1), and get_all_rows_by_coreid() fed those positions into get_row_by_position(), which
380
+ re-applied lines_to_ignore - exactly the seam the B1 bug lived in, and it was unpinned for
381
+ extensions until this test was added. coreid_index is now built through iter_rows()
382
+ instead, so that seam no longer exists here.
383
+ """
384
+
385
+ def _archive(self, **kwargs):
386
+ return build_archive(
387
+ temp_archive_dir(self),
388
+ rows=[["1", "Borneo"], ["2", "Mumbai"]],
389
+ extension=[["1", "elephant"], ["1", "tiger"], ["2", "monkey"]],
390
+ **kwargs,
391
+ )
392
+
393
+ def test_coreid_index(self):
394
+ path = self._archive()
395
+
396
+ with DwCAReader(path) as dwca:
397
+ index = dwca.extension_files[0].coreid_index
398
+
399
+ assert {"1": array("L", [0, 1]), "2": array("L", [2])} == index
400
+
401
+ def test_get_all_rows_by_coreid(self):
402
+ path = self._archive()
403
+
404
+ with DwCAReader(path) as dwca:
405
+ ext = dwca.extension_files[0]
406
+
407
+ several = ext.get_all_rows_by_coreid("1")
408
+ assert ["elephant", "tiger"] == [r.data[VERNACULAR_TERM] for r in several]
409
+
410
+ one = ext.get_all_rows_by_coreid("2")
411
+ assert ["monkey"] == [r.data[VERNACULAR_TERM] for r in one]
412
+
413
+ assert [] == ext.get_all_rows_by_coreid("unknown")
414
+
415
+ def test_orphaned_extension_rows(self):
416
+ path = build_archive(
417
+ temp_archive_dir(self),
418
+ rows=[["1", "Borneo"], ["2", "Mumbai"]],
419
+ extension=[["1", "elephant"], ["99", "ghost"]],
420
+ )
421
+
422
+ with DwCAReader(path) as dwca:
423
+ assert {"extension.txt": {"99": [1]}} == dwca.orphaned_extension_rows()
424
+
425
+ def test_core_row_extensions_content_and_ordering(self):
426
+ path = self._archive()
427
+
428
+ with DwCAReader(path) as dwca:
429
+ rows = list(dwca)
430
+
431
+ assert ["elephant", "tiger"] == [
432
+ r.data[VERNACULAR_TERM] for r in rows[0].extensions
433
+ ]
434
+ assert ["monkey"] == [r.data[VERNACULAR_TERM] for r in rows[1].extensions]
435
+
436
+ def test_ignore_header_lines_are_skipped_in_the_extension_index(self):
437
+ path = self._archive(
438
+ ignore_header_lines=2, header_rows=[["idA", "locA"], ["idB", "locB"]]
439
+ )
440
+
441
+ with DwCAReader(path) as dwca:
442
+ index = dwca.extension_files[0].coreid_index
443
+
444
+ assert {"1": array("L", [0, 1]), "2": array("L", [2])} == index
445
+
446
+
447
+ class TestRandomAccessDirect(unittest.TestCase):
448
+ """DwCAReader.next() is implemented as CSVDataFile.get_row_by_position() (see
449
+ dwca/read.py), so today every iteration test incidentally exercises random access too.
450
+ A rewrite that stops delegating would make that coverage disappear silently unless
451
+ something calls get_row_by_position() directly, which is what these tests do.
452
+ """
453
+
454
+ def test_multibyte_utf8(self):
455
+ # "\u4e2d\u6587" ("Chinese" in Chinese) is 2 characters but 6 bytes in UTF-8, so the
456
+ # byte offset of row 1 (used internally for seek()) differs from what a
457
+ # character-based offset would be.
458
+ locality = "caf\u00e9 \u4e2d\u6587"
459
+ path = build_archive(
460
+ temp_archive_dir(self),
461
+ rows=[["1", locality], ["2", "Mumbai"]],
462
+ )
463
+
464
+ with DwCAReader(path) as dwca:
465
+ first = dwca.core_file.get_row_by_position(0)
466
+ second = dwca.core_file.get_row_by_position(1)
467
+
468
+ assert locality == first.data[TERM1]
469
+ assert ["1", locality] == first.raw_fields
470
+ assert "Mumbai" == second.data[TERM1]
471
+ assert ["2", "Mumbai"] == second.raw_fields
472
+
473
+ def test_dos_terminator(self):
474
+ path = build_archive(
475
+ temp_archive_dir(self),
476
+ rows=[["1", "Borneo"], ["2", "Mumbai"]],
477
+ lines_terminated_by="\r\n",
478
+ )
479
+
480
+ with DwCAReader(path) as dwca:
481
+ first = dwca.core_file.get_row_by_position(0)
482
+ second = dwca.core_file.get_row_by_position(1)
483
+
484
+ assert "Borneo" == first.data[TERM1]
485
+ assert ["1", "Borneo"] == first.raw_fields
486
+ assert "Mumbai" == second.data[TERM1]
487
+ assert ["2", "Mumbai"] == second.raw_fields
488
+
489
+ def test_quoted_field_containing_the_separator(self):
490
+ path = build_archive(
491
+ temp_archive_dir(self),
492
+ rows=[["1", "plain, with comma"], ["2", "second"]],
493
+ fields_terminated_by=",",
494
+ fields_enclosed_by='"',
495
+ )
496
+
497
+ with DwCAReader(path) as dwca:
498
+ first = dwca.core_file.get_row_by_position(0)
499
+ second = dwca.core_file.get_row_by_position(1)
500
+
501
+ assert "plain, with comma" == first.data[TERM1]
502
+ assert ["1", "plain, with comma"] == first.raw_fields
503
+ assert "second" == second.data[TERM1]
504
+ assert ["2", "second"] == second.raw_fields
505
+
506
+
507
+ class TestRowExtensionsDuringCoreIteration(unittest.TestCase):
508
+ def _archive(self):
509
+ return build_archive(
510
+ temp_archive_dir(self),
511
+ rows=[["1", "Borneo"], ["2", "Mumbai"]],
512
+ extension=[["1", "elephant"], ["1", "tiger"], ["2", "monkey"]],
513
+ )
514
+
515
+ def test_row_extensions_accessible_during_core_iteration(self):
516
+ path = self._archive()
517
+
518
+ seen = {}
519
+ with DwCAReader(path) as dwca:
520
+ for row in dwca:
521
+ seen[row.id] = [e.data[VERNACULAR_TERM] for e in row.extensions]
522
+
523
+ assert {"1": ["elephant", "tiger"], "2": ["monkey"]} == seen
524
+
525
+ def test_row_extensions_are_cached(self):
526
+ path = self._archive()
527
+
528
+ with DwCAReader(path) as dwca:
529
+ row = next(iter(dwca))
530
+ first = row.extensions
531
+ second = row.extensions
532
+
533
+ # Lazy-loaded and cached on the instance: same list object both times, not rebuilt.
534
+ assert first is second
535
+ assert ["elephant", "tiger"] == [e.data[VERNACULAR_TERM] for e in first]
536
+
537
+
538
+ class TestNegativePosition(unittest.TestCase):
539
+ def test_negative_position_returns_the_header_line(self):
540
+ # get_row_by_position() computes self._line_offsets[position + lines_to_ignore]. For
541
+ # position=-1 this wraps around to the last entry in the offsets array, which is the
542
+ # header line (kept in the index but skipped during normal iteration).
543
+ with DwCAReader(sample_data_path("dwca-simple-test-archive.zip")) as dwca:
544
+ row = dwca.core_file.get_row_by_position(-1)
545
+
546
+ # CHARACTERIZATION: wrong, see B7. A rewrite will likely raise IndexError instead.
547
+ assert "id" == row.id
548
+
549
+
550
+ class TestDuplicateCoreIds(unittest.TestCase):
551
+ """get_corerow_by_id()'s docstring disclaims which row wins when ids repeat, and
552
+ coreid_index maps one id to several positions. Pin both so a rewrite has to decide
553
+ deliberately rather than by accident.
554
+ """
555
+
556
+ def _archive(self):
557
+ return build_archive(
558
+ temp_archive_dir(self),
559
+ rows=[["1", "Borneo"], ["1", "Mumbai"], ["2", "Paris"]],
560
+ )
561
+
562
+ def test_get_corerow_by_id_returns_the_first_match(self):
563
+ with DwCAReader(self._archive()) as dwca:
564
+ row = dwca.get_corerow_by_id("1")
565
+
566
+ assert "Borneo" == row.data[TERM1]
567
+
568
+ def test_coreid_index_holds_every_position(self):
569
+ with DwCAReader(self._archive()) as dwca:
570
+ index = dwca.core_file.coreid_index
571
+
572
+ assert {"1": array("L", [0, 1]), "2": array("L", [2])} == index
573
+
574
+
575
+ class TestQuotedRecordsSpanningLines(unittest.TestCase):
576
+ """A quoted field may contain the line terminator. Iteration and random access must agree."""
577
+
578
+ QUOTED_META = {"fields_terminated_by": ",", "fields_enclosed_by": '"'}
579
+
580
+ def _archive(self, payload, columns=3):
581
+ return build_archive(
582
+ temp_archive_dir(self),
583
+ rows=[],
584
+ columns=columns,
585
+ raw_payload=payload,
586
+ **self.QUOTED_META
587
+ )
588
+
589
+ def test_iteration_and_random_access_agree(self):
590
+ path = self._archive(b'0,b,a\n1,a,"x\ny\nz"\n2,"x\ny\nz",a\n')
591
+
592
+ with DwCAReader(path) as dwca:
593
+ streamed = [row.raw_fields for row in dwca]
594
+ seeked = [
595
+ dwca.core_file.get_row_by_position(i).raw_fields
596
+ for i in range(len(streamed))
597
+ ]
598
+
599
+ assert [["0", "b", "a"], ["1", "a", "x\ny\nz"], ["2", "x\ny\nz", "a"]] == streamed
600
+ assert streamed == seeked
601
+
602
+ def test_extensions_are_not_truncated(self):
603
+ """coreid_index is built from the streaming pass but consumed through seeks."""
604
+ directory = temp_archive_dir(self)
605
+ path = build_archive(
606
+ directory,
607
+ rows=[["1", "Lagopus"], ["2", "Struthio"]],
608
+ fields_terminated_by=",",
609
+ fields_enclosed_by='"',
610
+ extension=[["1", "grouse\nfoo"], ["2", "ostrich"]],
611
+ )
612
+
613
+ with DwCAReader(path) as dwca:
614
+ per_core = [[e.raw_fields for e in row.extensions] for row in dwca]
615
+
616
+ assert [["1", "grouse\nfoo"]] == per_core[0]
617
+ assert [["2", "ostrich"]] == per_core[1]