Merge lp:~thisfred/u1db/iterate-over-list-of-dicts into lp:u1db
- iterate-over-list-of-dicts
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Eric Casteleijn |
Approved revision: | 372 |
Merged at revision: | 366 |
Proposed branch: | lp:~thisfred/u1db/iterate-over-list-of-dicts |
Merge into: | lp:u1db |
Diff against target: |
349 lines (+130/-69) 5 files modified
src/u1db_query.c (+74/-46) u1db/query_parser.py (+25/-17) u1db/tests/test_backends.py (+20/-0) u1db/tests/test_query_parser.py (+10/-5) u1db/tests/test_sqlite_backend.py (+1/-1) |
To merge this branch: | bzr merge lp:~thisfred/u1db/iterate-over-list-of-dicts |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Samuele Pedroni | Approve | ||
Review via email: mp+117120@code.launchpad.net |
Commit message
Added support for indexing dictionaries in lists, so this becomes possible:
doc = self.db.
Description of the change
Added support for indexing dictionaries in lists, so this becomes possible:
doc = self.db.
Samuele Pedroni (pedronis) wrote : | # |
- 368. By Eric Casteleijn
-
split in __init__
- 369. By Eric Casteleijn
-
split in __init__
- 370. By Eric Casteleijn
-
don't mutate list
Eric Casteleijn (thisfred) wrote : | # |
Fixes pushed
- 371. By Eric Casteleijn
-
don't copy list around
- 372. By Eric Casteleijn
-
don't copy list around
Preview Diff
1 | === modified file 'src/u1db_query.c' | |||
2 | --- src/u1db_query.c 2012-07-27 15:17:04 +0000 | |||
3 | +++ src/u1db_query.c 2012-07-31 14:58:20 +0000 | |||
4 | @@ -252,60 +252,84 @@ | |||
5 | 252 | op_combine, "combine", json_type_string, -1, JUST_EXPRESSION}; | 252 | op_combine, "combine", json_type_string, -1, JUST_EXPRESSION}; |
6 | 253 | 253 | ||
7 | 254 | static int | 254 | static int |
10 | 255 | extract_field_values(json_object *obj, const string_list *field_path, | 255 | extract_value(json_object *val, int value_type, string_list *values) |
9 | 256 | int value_type, string_list *values) | ||
11 | 257 | { | 256 | { |
13 | 258 | string_list_item *item = NULL; | 257 | int status = U1DB_OK; |
14 | 258 | int i, integer_value, boolean_value, length; | ||
15 | 259 | char string_value[MAX_INT_STR_LEN]; | 259 | char string_value[MAX_INT_STR_LEN]; |
16 | 260 | struct array_list *list_val = NULL; | ||
17 | 261 | json_object *val = NULL; | ||
18 | 262 | int i, integer_value, boolean_value; | ||
19 | 263 | int status = U1DB_OK; | ||
20 | 264 | val = obj; | ||
21 | 265 | if (val == NULL) | ||
22 | 266 | goto finish; | ||
23 | 267 | for (item = field_path->head; item != NULL; item = item->next) | ||
24 | 268 | { | ||
25 | 269 | val = json_object_object_get(val, item->data); | ||
26 | 270 | if (val == NULL) | ||
27 | 271 | goto finish; | ||
28 | 272 | } | ||
29 | 273 | if (json_object_is_type(val, json_type_string) && value_type == | 260 | if (json_object_is_type(val, json_type_string) && value_type == |
30 | 274 | json_type_string) { | 261 | json_type_string) { |
34 | 275 | if ((status = append(values, json_object_get_string(val))) != U1DB_OK) | 262 | status = append(values, json_object_get_string(val)); |
35 | 276 | goto finish; | 263 | goto finish; |
36 | 277 | } else if (json_object_is_type(val, json_type_int) && value_type == | 264 | } |
37 | 265 | if (json_object_is_type(val, json_type_int) && value_type == | ||
38 | 278 | json_type_int) { | 266 | json_type_int) { |
39 | 279 | integer_value = json_object_get_int(val); | 267 | integer_value = json_object_get_int(val); |
40 | 280 | snprintf(string_value, MAX_INT_STR_LEN, "%d", integer_value); | 268 | snprintf(string_value, MAX_INT_STR_LEN, "%d", integer_value); |
45 | 281 | if ((status = append(values, string_value)) != U1DB_OK) | 269 | status = append(values, string_value); |
46 | 282 | goto finish; | 270 | goto finish; |
47 | 283 | } else if (json_object_is_type(val, json_type_boolean) && | 271 | } |
48 | 284 | value_type == json_type_boolean) { | 272 | if (json_object_is_type(val, json_type_boolean) && |
49 | 273 | value_type == json_type_boolean) { | ||
50 | 285 | boolean_value = json_object_get_boolean(val); | 274 | boolean_value = json_object_get_boolean(val); |
51 | 286 | if (boolean_value) { | 275 | if (boolean_value) { |
52 | 287 | status = append(values, "1"); | 276 | status = append(values, "1"); |
53 | 288 | if (status != U1DB_OK) | ||
54 | 289 | goto finish; | ||
55 | 290 | } else { | 277 | } else { |
56 | 291 | status = append(values, "0"); | 278 | status = append(values, "0"); |
57 | 279 | } | ||
58 | 280 | goto finish; | ||
59 | 281 | } | ||
60 | 282 | if (json_object_is_type(val, json_type_array)) { | ||
61 | 283 | length = json_object_array_length(val); | ||
62 | 284 | for (i = 0; i < length; i++) { | ||
63 | 285 | status = extract_value( | ||
64 | 286 | json_object_array_get_idx(val, i), value_type, values); | ||
65 | 292 | if (status != U1DB_OK) | 287 | if (status != U1DB_OK) |
66 | 293 | goto finish; | 288 | goto finish; |
67 | 294 | } | 289 | } |
82 | 295 | } else if (json_object_is_type(val, json_type_array)) { | 290 | } |
83 | 296 | // TODO: recursively check the types | 291 | finish: |
84 | 297 | list_val = json_object_get_array(val); | 292 | return status; |
85 | 298 | for (i = 0; i < list_val->length; i++) | 293 | } |
86 | 299 | { | 294 | |
87 | 300 | if ((status = append(values, json_object_get_string( | 295 | static int |
88 | 301 | array_list_get_idx( | 296 | extract_field_values(json_object *obj, const string_list_item *field, |
89 | 302 | list_val, i)))) != U1DB_OK) | 297 | int value_type, string_list *values) |
90 | 303 | goto finish; | 298 | { |
91 | 304 | } | 299 | json_object *val = NULL; |
92 | 305 | } | 300 | json_object *array_item = NULL; |
93 | 306 | finish: | 301 | int i, length; |
94 | 307 | return status; | 302 | int status = U1DB_OK; |
95 | 308 | } | 303 | |
96 | 304 | if (!json_object_is_type(obj, json_type_object)) { | ||
97 | 305 | goto finish; | ||
98 | 306 | } | ||
99 | 307 | val = json_object_object_get(obj, field->data); | ||
100 | 308 | if (val == NULL) | ||
101 | 309 | goto finish; | ||
102 | 310 | if (field->next != NULL) { | ||
103 | 311 | if (json_object_is_type(val, json_type_array)) { | ||
104 | 312 | length = json_object_array_length(val); | ||
105 | 313 | for (i = 0; i < length; i++) { | ||
106 | 314 | array_item = json_object_array_get_idx(val, i); | ||
107 | 315 | status = extract_field_values( | ||
108 | 316 | array_item, field->next, value_type, values); | ||
109 | 317 | if (status != U1DB_OK) | ||
110 | 318 | goto finish; | ||
111 | 319 | } | ||
112 | 320 | goto finish; | ||
113 | 321 | } | ||
114 | 322 | if (json_object_is_type(val, json_type_object)) { | ||
115 | 323 | status = extract_field_values(val, field->next, value_type, values); | ||
116 | 324 | goto finish; | ||
117 | 325 | } | ||
118 | 326 | goto finish; | ||
119 | 327 | } | ||
120 | 328 | status = extract_value(val, value_type, values); | ||
121 | 329 | finish: | ||
122 | 330 | return status; | ||
123 | 331 | } | ||
124 | 332 | |||
125 | 309 | 333 | ||
126 | 310 | static int | 334 | static int |
127 | 311 | split(string_list *result, char *string, char splitter) | 335 | split(string_list *result, char *string, char splitter) |
128 | @@ -351,7 +375,7 @@ | |||
129 | 351 | status = ((op_function)tree->op)(tree, obj, values); | 375 | status = ((op_function)tree->op)(tree, obj, values); |
130 | 352 | } else { | 376 | } else { |
131 | 353 | status = extract_field_values( | 377 | status = extract_field_values( |
133 | 354 | obj, tree->field_path, json_type_string, values); | 378 | obj, tree->field_path->head, json_type_string, values); |
134 | 355 | } | 379 | } |
135 | 356 | return status; | 380 | return status; |
136 | 357 | } | 381 | } |
137 | @@ -411,7 +435,7 @@ | |||
138 | 411 | if (status != U1DB_OK) | 435 | if (status != U1DB_OK) |
139 | 412 | return status; | 436 | return status; |
140 | 413 | status = extract_field_values( | 437 | status = extract_field_values( |
142 | 414 | obj, node->field_path, json_type_int, values); | 438 | obj, node->field_path->head, json_type_int, values); |
143 | 415 | if (status != U1DB_OK) | 439 | if (status != U1DB_OK) |
144 | 416 | goto finish; | 440 | goto finish; |
145 | 417 | node = node->next_sibling; | 441 | node = node->next_sibling; |
146 | @@ -539,7 +563,7 @@ | |||
147 | 539 | //booleans by extract_field_values. | 563 | //booleans by extract_field_values. |
148 | 540 | 564 | ||
149 | 541 | status = extract_field_values( | 565 | status = extract_field_values( |
151 | 542 | obj, tree->first_child->field_path, json_type_boolean, values); | 566 | obj, tree->first_child->field_path->head, json_type_boolean, values); |
152 | 543 | if (status != U1DB_OK) | 567 | if (status != U1DB_OK) |
153 | 544 | goto finish; | 568 | goto finish; |
154 | 545 | for (item = values->head; item != NULL; item = item->next) | 569 | for (item = values->head; item != NULL; item = item->next) |
155 | @@ -1274,7 +1298,7 @@ | |||
156 | 1274 | char *sep = NULL; | 1298 | char *sep = NULL; |
157 | 1275 | parse_tree *node = NULL; | 1299 | parse_tree *node = NULL; |
158 | 1276 | int status = U1DB_OK; | 1300 | int status = U1DB_OK; |
160 | 1277 | int i; | 1301 | int i, array_size; |
161 | 1278 | 1302 | ||
162 | 1279 | sep = get_token(tokens); | 1303 | sep = get_token(tokens); |
163 | 1280 | if (sep == NULL || strcmp(sep, "(") != 0) { | 1304 | if (sep == NULL || strcmp(sep, "(") != 0) { |
164 | @@ -1321,8 +1345,9 @@ | |||
165 | 1321 | } | 1345 | } |
166 | 1322 | } | 1346 | } |
167 | 1323 | i = 0; | 1347 | i = 0; |
168 | 1348 | array_size = sizeof(result->value_types) / sizeof(int); | ||
169 | 1324 | for (node = result->first_child; node != NULL; node = node->next_sibling) { | 1349 | for (node = result->first_child; node != NULL; node = node->next_sibling) { |
171 | 1325 | node->arg_type = result->value_types[i % result->number_of_children]; | 1350 | node->arg_type = result->value_types[i % array_size]; |
172 | 1326 | if (node->arg_type == EXPRESSION) { | 1351 | if (node->arg_type == EXPRESSION) { |
173 | 1327 | status = to_getter(node); | 1352 | status = to_getter(node); |
174 | 1328 | if (status != U1DB_OK) | 1353 | if (status != U1DB_OK) |
175 | @@ -1538,7 +1563,8 @@ | |||
176 | 1538 | if (ctx->obj == NULL || !json_object_is_type(ctx->obj, json_type_object)) { | 1563 | if (ctx->obj == NULL || !json_object_is_type(ctx->obj, json_type_object)) { |
177 | 1539 | return U1DB_INVALID_JSON; | 1564 | return U1DB_INVALID_JSON; |
178 | 1540 | } | 1565 | } |
180 | 1541 | if ((status = init_list(&values)) != U1DB_OK) | 1566 | status = init_list(&values); |
181 | 1567 | if (status != U1DB_OK) | ||
182 | 1542 | goto finish; | 1568 | goto finish; |
183 | 1543 | status = get_values(tree, ctx->obj, values); | 1569 | status = get_values(tree, ctx->obj, values); |
184 | 1544 | if (status != U1DB_OK) | 1570 | if (status != U1DB_OK) |
185 | @@ -1550,8 +1576,10 @@ | |||
186 | 1550 | goto finish; | 1576 | goto finish; |
187 | 1551 | } | 1577 | } |
188 | 1552 | finish: | 1578 | finish: |
190 | 1553 | if (values != NULL) | 1579 | if (values != NULL) { |
191 | 1554 | destroy_list(values); | 1580 | destroy_list(values); |
192 | 1581 | values = NULL; | ||
193 | 1582 | } | ||
194 | 1555 | return status; | 1583 | return status; |
195 | 1556 | } | 1584 | } |
196 | 1557 | 1585 | ||
197 | 1558 | 1586 | ||
198 | === modified file 'u1db/query_parser.py' | |||
199 | --- u1db/query_parser.py 2012-07-27 14:50:11 +0000 | |||
200 | +++ u1db/query_parser.py 2012-07-31 14:58:20 +0000 | |||
201 | @@ -53,6 +53,29 @@ | |||
202 | 53 | return self.value | 53 | return self.value |
203 | 54 | 54 | ||
204 | 55 | 55 | ||
205 | 56 | def extract_field(raw_doc, subfields, index=0): | ||
206 | 57 | if not isinstance(raw_doc, dict): | ||
207 | 58 | return [] | ||
208 | 59 | val = raw_doc.get(subfields[index]) | ||
209 | 60 | if val is None: | ||
210 | 61 | return [] | ||
211 | 62 | if index < len(subfields) - 1: | ||
212 | 63 | if isinstance(val, list): | ||
213 | 64 | results = [] | ||
214 | 65 | for item in val: | ||
215 | 66 | results.extend(extract_field(item, subfields, index + 1)) | ||
216 | 67 | return results | ||
217 | 68 | if isinstance(val, dict): | ||
218 | 69 | return extract_field(val, subfields, index + 1) | ||
219 | 70 | return [] | ||
220 | 71 | if isinstance(val, dict): | ||
221 | 72 | return [] | ||
222 | 73 | if isinstance(val, list): | ||
223 | 74 | # Strip anything in the list that isn't a simple type | ||
224 | 75 | return [v for v in val if not isinstance(v, (dict, list))] | ||
225 | 76 | return [val] | ||
226 | 77 | |||
227 | 78 | |||
228 | 56 | class ExtractField(Getter): | 79 | class ExtractField(Getter): |
229 | 57 | """Extract a field from the document.""" | 80 | """Extract a field from the document.""" |
230 | 58 | 81 | ||
231 | @@ -69,25 +92,10 @@ | |||
232 | 69 | :param field: a specifier for the field to return. | 92 | :param field: a specifier for the field to return. |
233 | 70 | This is either a field name, or a dotted field name. | 93 | This is either a field name, or a dotted field name. |
234 | 71 | """ | 94 | """ |
236 | 72 | self.field = field | 95 | self.field = field.split('.') |
237 | 73 | 96 | ||
238 | 74 | def get(self, raw_doc): | 97 | def get(self, raw_doc): |
255 | 75 | for subfield in self.field.split('.'): | 98 | return extract_field(raw_doc, self.field) |
240 | 76 | if isinstance(raw_doc, dict): | ||
241 | 77 | raw_doc = raw_doc.get(subfield) | ||
242 | 78 | else: | ||
243 | 79 | return [] | ||
244 | 80 | if isinstance(raw_doc, dict): | ||
245 | 81 | return [] | ||
246 | 82 | if raw_doc is None: | ||
247 | 83 | result = [] | ||
248 | 84 | elif isinstance(raw_doc, list): | ||
249 | 85 | # Strip anything in the list that isn't a simple type | ||
250 | 86 | result = [val for val in raw_doc | ||
251 | 87 | if not isinstance(val, (dict, list))] | ||
252 | 88 | else: | ||
253 | 89 | result = [raw_doc] | ||
254 | 90 | return result | ||
256 | 91 | 99 | ||
257 | 92 | 100 | ||
258 | 93 | class Transformation(Getter): | 101 | class Transformation(Getter): |
259 | 94 | 102 | ||
260 | === modified file 'u1db/tests/test_backends.py' | |||
261 | --- u1db/tests/test_backends.py 2012-07-27 15:24:48 +0000 | |||
262 | +++ u1db/tests/test_backends.py 2012-07-31 14:58:20 +0000 | |||
263 | @@ -1470,6 +1470,26 @@ | |||
264 | 1470 | self.db.create_index('test-idx', 'sub.foo.bar.baz.qux.fnord') | 1470 | self.db.create_index('test-idx', 'sub.foo.bar.baz.qux.fnord') |
265 | 1471 | self.assertEqual([], self.db.get_from_index('test-idx', '*')) | 1471 | self.assertEqual([], self.db.get_from_index('test-idx', '*')) |
266 | 1472 | 1472 | ||
267 | 1473 | def test_nested_traverses_lists(self): | ||
268 | 1474 | # subpath finds dicts in list | ||
269 | 1475 | doc = self.db.create_doc_from_json( | ||
270 | 1476 | '{"foo": [{"zap": "bar"}, {"zap": "baz"}]}') | ||
271 | 1477 | # subpath only finds dicts in list | ||
272 | 1478 | self.db.create_doc_from_json('{"foo": ["zap", "baz"]}') | ||
273 | 1479 | self.db.create_index('test-idx', 'foo.zap') | ||
274 | 1480 | self.assertEqual([doc], self.db.get_from_index('test-idx', 'bar')) | ||
275 | 1481 | self.assertEqual([doc], self.db.get_from_index('test-idx', 'baz')) | ||
276 | 1482 | |||
277 | 1483 | def test_nested_list_traversal(self): | ||
278 | 1484 | # subpath finds dicts in list | ||
279 | 1485 | doc = self.db.create_doc_from_json( | ||
280 | 1486 | '{"foo": [{"zap": [{"qux": "fnord"}, {"qux": "zombo"}]},' | ||
281 | 1487 | '{"zap": "baz"}]}') | ||
282 | 1488 | # subpath only finds dicts in list | ||
283 | 1489 | self.db.create_index('test-idx', 'foo.zap.qux') | ||
284 | 1490 | self.assertEqual([doc], self.db.get_from_index('test-idx', 'fnord')) | ||
285 | 1491 | self.assertEqual([doc], self.db.get_from_index('test-idx', 'zombo')) | ||
286 | 1492 | |||
287 | 1473 | def test_index_list1(self): | 1493 | def test_index_list1(self): |
288 | 1474 | self.db.create_index("index", "name") | 1494 | self.db.create_index("index", "name") |
289 | 1475 | content = '{"name": ["foo", "bar"]}' | 1495 | content = '{"name": ["foo", "bar"]}' |
290 | 1476 | 1496 | ||
291 | === modified file 'u1db/tests/test_query_parser.py' | |||
292 | --- u1db/tests/test_query_parser.py 2012-07-26 13:35:50 +0000 | |||
293 | +++ u1db/tests/test_query_parser.py 2012-07-31 14:58:20 +0000 | |||
294 | @@ -179,6 +179,11 @@ | |||
295 | 179 | def test_get_value_list_of_dicts(self): | 179 | def test_get_value_list_of_dicts(self): |
296 | 180 | self.assertExtractField([], 'foo', {'foo': [{'zap': 'bar'}]}) | 180 | self.assertExtractField([], 'foo', {'foo': [{'zap': 'bar'}]}) |
297 | 181 | 181 | ||
298 | 182 | def test_get_value_list_of_dicts2(self): | ||
299 | 183 | self.assertExtractField( | ||
300 | 184 | ['bar', 'baz'], 'foo.zap', | ||
301 | 185 | {'foo': [{'zap': 'bar'}, {'zap': 'baz'}]}) | ||
302 | 186 | |||
303 | 182 | def test_get_value_int(self): | 187 | def test_get_value_int(self): |
304 | 183 | self.assertExtractField([9], 'foo', {'foo': 9}) | 188 | self.assertExtractField([9], 'foo', {'foo': 9}) |
305 | 184 | 189 | ||
306 | @@ -395,12 +400,12 @@ | |||
307 | 395 | def test_parse_field(self): | 400 | def test_parse_field(self): |
308 | 396 | getter = self.parse("a") | 401 | getter = self.parse("a") |
309 | 397 | self.assertIsInstance(getter, query_parser.ExtractField) | 402 | self.assertIsInstance(getter, query_parser.ExtractField) |
311 | 398 | self.assertEqual("a", getter.field) | 403 | self.assertEqual(["a"], getter.field) |
312 | 399 | 404 | ||
313 | 400 | def test_parse_dotted_field(self): | 405 | def test_parse_dotted_field(self): |
314 | 401 | getter = self.parse("a.b") | 406 | getter = self.parse("a.b") |
315 | 402 | self.assertIsInstance(getter, query_parser.ExtractField) | 407 | self.assertIsInstance(getter, query_parser.ExtractField) |
317 | 403 | self.assertEqual("a.b", getter.field) | 408 | self.assertEqual(["a", "b"], getter.field) |
318 | 404 | 409 | ||
319 | 405 | def test_parse_dotted_field_nothing_after_dot(self): | 410 | def test_parse_dotted_field_nothing_after_dot(self): |
320 | 406 | self.assertParseError("a.") | 411 | self.assertParseError("a.") |
321 | @@ -427,12 +432,12 @@ | |||
322 | 427 | getter = self.parse("lower(a)") | 432 | getter = self.parse("lower(a)") |
323 | 428 | self.assertIsInstance(getter, query_parser.Lower) | 433 | self.assertIsInstance(getter, query_parser.Lower) |
324 | 429 | self.assertIsInstance(getter.inner, query_parser.ExtractField) | 434 | self.assertIsInstance(getter.inner, query_parser.ExtractField) |
326 | 430 | self.assertEqual("a", getter.inner.field) | 435 | self.assertEqual(["a"], getter.inner.field) |
327 | 431 | 436 | ||
328 | 432 | def test_parse_all(self): | 437 | def test_parse_all(self): |
329 | 433 | getters = self.parse_all(["a", "b"]) | 438 | getters = self.parse_all(["a", "b"]) |
330 | 434 | self.assertEqual(2, len(getters)) | 439 | self.assertEqual(2, len(getters)) |
331 | 435 | self.assertIsInstance(getters[0], query_parser.ExtractField) | 440 | self.assertIsInstance(getters[0], query_parser.ExtractField) |
333 | 436 | self.assertEqual("a", getters[0].field) | 441 | self.assertEqual(["a"], getters[0].field) |
334 | 437 | self.assertIsInstance(getters[1], query_parser.ExtractField) | 442 | self.assertIsInstance(getters[1], query_parser.ExtractField) |
336 | 438 | self.assertEqual("b", getters[1].field) | 443 | self.assertEqual(["b"], getters[1].field) |
337 | 439 | 444 | ||
338 | === modified file 'u1db/tests/test_sqlite_backend.py' | |||
339 | --- u1db/tests/test_sqlite_backend.py 2012-07-19 19:50:58 +0000 | |||
340 | +++ u1db/tests/test_sqlite_backend.py 2012-07-31 14:58:20 +0000 | |||
341 | @@ -123,7 +123,7 @@ | |||
342 | 123 | self.db = sqlite_backend.SQLitePartialExpandDatabase(':memory:') | 123 | self.db = sqlite_backend.SQLitePartialExpandDatabase(':memory:') |
343 | 124 | g = self.db._parse_index_definition('fieldname') | 124 | g = self.db._parse_index_definition('fieldname') |
344 | 125 | self.assertIsInstance(g, query_parser.ExtractField) | 125 | self.assertIsInstance(g, query_parser.ExtractField) |
346 | 126 | self.assertEqual('fieldname', g.field) | 126 | self.assertEqual(['fieldname'], g.field) |
347 | 127 | 127 | ||
348 | 128 | def test__update_indexes(self): | 128 | def test__update_indexes(self): |
349 | 129 | self.db = sqlite_backend.SQLitePartialExpandDatabase(':memory:') | 129 | self.db = sqlite_backend.SQLitePartialExpandDatabase(':memory:') |
+ subfields = self.field. split(' .')
I would move this to the constructor
255 + return extract_ field(raw_ doc, subfields)
+def extract_ field(raw_ doc, subfields): get(subfields. pop(0))
206 + if not isinstance(raw_doc, dict):
207 + return []
208 + val = raw_doc.
I would use a index argument and avoid the pop (especially pop(0)) and copy
216 + subfields = []
this line is not needed
same for these:
223 + if val is None:
224 + return []
line 209-210 cover that