python-dwca-reader 0.16.4__py3-none-any.whl → 0.17.1__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.
- dwca/descriptors.py +239 -3
- dwca/files.py +381 -55
- dwca/read.py +65 -17
- dwca/rows.py +105 -78
- dwca/star_record.py +2 -3
- dwca/test/archive_builder.py +188 -0
- dwca/test/test_characterization.py +617 -0
- dwca/test/test_datafile.py +286 -3
- dwca/test/test_descriptors.py +331 -0
- dwca/test/test_dwcareader.py +74 -12
- dwca/test/test_rows.py +173 -0
- dwca/version.py +1 -1
- {python_dwca_reader-0.16.4.dist-info → python_dwca_reader-0.17.1.dist-info}/METADATA +16 -3
- python_dwca_reader-0.17.1.dist-info/RECORD +29 -0
- {python_dwca_reader-0.16.4.dist-info → python_dwca_reader-0.17.1.dist-info}/WHEEL +1 -1
- dwca/minibench.py +0 -61
- python_dwca_reader-0.16.4.dist-info/RECORD +0 -28
- {python_dwca_reader-0.16.4.dist-info → python_dwca_reader-0.17.1.dist-info/licenses}/AUTHORS +0 -0
- {python_dwca_reader-0.16.4.dist-info → python_dwca_reader-0.17.1.dist-info/licenses}/LICENSE.txt +0 -0
- {python_dwca_reader-0.16.4.dist-info → python_dwca_reader-0.17.1.dist-info}/top_level.txt +0 -0
|
@@ -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]
|