fastsimdjson 0.6.0__tar.gz → 0.7.0__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.
Files changed (25) hide show
  1. {fastsimdjson-0.6.0/src/fastsimdjson.egg-info → fastsimdjson-0.7.0}/PKG-INFO +42 -6
  2. {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/README.md +40 -5
  3. {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/pyproject.toml +2 -2
  4. {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/src/fastsimdjson.cpp +308 -13
  5. {fastsimdjson-0.6.0 → fastsimdjson-0.7.0/src/fastsimdjson.egg-info}/PKG-INFO +42 -6
  6. {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/src/fastsimdjson.egg-info/requires.txt +1 -0
  7. {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/tests/test_stream.py +101 -0
  8. {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/MANIFEST.in +0 -0
  9. {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/setup.cfg +0 -0
  10. {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/setup.py +0 -0
  11. {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/src/fastsimdjson.egg-info/SOURCES.txt +0 -0
  12. {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/src/fastsimdjson.egg-info/dependency_links.txt +0 -0
  13. {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/src/fastsimdjson.egg-info/top_level.txt +0 -0
  14. {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/tests/test_dumps.py +0 -0
  15. {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/tests/test_files.py +0 -0
  16. {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/tests/test_lazy.py +0 -0
  17. {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/tests/test_loads.py +0 -0
  18. {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/vendor/simdjson.cpp +0 -0
  19. {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/vendor/simdjson.h +0 -0
  20. {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/vendor/simdutf.cpp +0 -0
  21. {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/vendor/simdutf.h +0 -0
  22. {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/vendor/simdutf_c.h +0 -0
  23. {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/vendor/zmij-LICENSE +0 -0
  24. {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/vendor/zmij.cc +0 -0
  25. {fastsimdjson-0.6.0 → fastsimdjson-0.7.0}/vendor/zmij.h +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastsimdjson
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Fast JSON parsing for Python, built on simdjson
5
5
  Author-email: Daniel Lemire <daniel@lemire.me>
6
6
  License-Expression: Apache-2.0
@@ -25,6 +25,7 @@ Provides-Extra: test
25
25
  Requires-Dist: pytest; extra == "test"
26
26
  Provides-Extra: bench
27
27
  Requires-Dist: orjson; extra == "bench"
28
+ Requires-Dist: jiter; extra == "bench"
28
29
  Requires-Dist: msgspec; extra == "bench"
29
30
  Requires-Dist: pysimdjson; extra == "bench"
30
31
  Requires-Dist: cysimdjson; extra == "bench"
@@ -38,7 +39,8 @@ replacement for the `json` module's `loads`, `load`, `dumps` and `dump`, and
38
39
  it can be over 3 times faster than the standard `json.loads` and
39
40
  `json.dumps`. When you only need part of a document, its lazy `parse`
40
41
  function is faster still. It also reads streams of documents (NDJSON, JSON
41
- Lines). It writes JSON in two ways: `dumps` returns the same `str` as
42
+ Lines), on many threads with free-threaded Python, and can extract only the
43
+ fields you need from each document. It writes JSON in two ways: `dumps` returns the same `str` as
42
44
  `json.dumps`, and `dumpb` returns the same `bytes` as `orjson.dumps`, as fast
43
45
  as orjson.
44
46
 
@@ -233,6 +235,37 @@ document, and the Python objects of all of them, to the calling thread.
233
235
  With the GIL (the default build of Python), the calling thread does all the
234
236
  work: `threads` has no effect on speed.
235
237
 
238
+ ### Only some fields: `fields=`
239
+
240
+ When you need a few values from each document, `fields` extracts them
241
+ without building the documents: only the values found become Python
242
+ objects.
243
+
244
+ ```python
245
+ events = fastsimdjson.loads_many(data, fields=["/type", "/payload/action"])
246
+ # (type, action) for each document
247
+
248
+ def count(t):
249
+ kind, action, language = t
250
+ ...
251
+ fastsimdjson.map_many(count, data, threads=64,
252
+ fields=["/type", "/payload/action",
253
+ "/payload/pull_request/base/repo/language"])
254
+ ```
255
+
256
+ `loads_many` and `map_many` accept `fields`: a JSON Pointer (RFC 6901), or a
257
+ list or tuple of them. Each document then becomes the value at the pointer,
258
+ or a tuple of the values at the pointers, in order. A value that is an
259
+ object or an array is built in full. A pointer that leads nowhere (a
260
+ missing key, an index past the end, or a step into a scalar) gives
261
+ `default`, `None` unless you pass another value; a JSON `null` gives `None`.
262
+ As with `at_pointer`, a key that appears twice in an object refers to its
263
+ first occurrence. `fields` cannot be combined with `lazy=True`.
264
+
265
+ On 7.6 GB of GitHub events, `map_many` with three fields ran at 2.8 GB/s on
266
+ one core of an Intel Xeon Gold 6548N, against 2.2 GB/s with lazy views
267
+ (`lazy=True`). With 64 threads, it ran at 78 GB/s (lazy views: 61 GB/s).
268
+
236
269
  ### `release`
237
270
 
238
271
  `release()` frees the simdjson parser and the string caches kept by the
@@ -307,8 +340,8 @@ The suite builds an ~80 MB document and a list of 16,777,221 integers, so
307
340
  give it some RAM.
308
341
 
309
342
  The benchmark scripts need simdjson-data and the `bench` extra (orjson,
310
- msgspec, pysimdjson, cysimdjson). `bench.py` times `loads` against
311
- `json.loads` and orjson, `bench_lazy.py` times `parse` against pysimdjson and
343
+ jiter, msgspec, pysimdjson, cysimdjson). `bench.py` times `loads` against
344
+ `json.loads`, orjson and jiter, `bench_lazy.py` times `parse` against pysimdjson and
312
345
  cysimdjson, `bench_dumps.py` times `dumps` against `json.dumps` and `dumpb`
313
346
  against orjson and msgspec, and `bench_many.py` times `loads_many`.
314
347
 
@@ -347,7 +380,7 @@ geometric mean over the 22 files (higher is better).
347
380
 
348
381
  <picture>
349
382
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads_dark.png">
350
- <img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads.png" width="80%" alt="Parsing JSON in Python: fastsimdjson loads 0.78 GB/s, orjson 0.60, msgspec 0.53, pysimdjson 0.44, cysimdjson 0.43, ujson 0.37, python-rapidjson 0.24, simplejson 0.23, json 0.22">
383
+ <img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads.png" width="80%" alt="Parsing JSON in Python: fastsimdjson loads 0.78 GB/s, orjson 0.60, msgspec 0.53, jiter 0.48, pysimdjson 0.44, cysimdjson 0.43, ujson 0.37, python-rapidjson 0.24, simplejson 0.23, json 0.22">
351
384
  </picture>
352
385
 
353
386
  | parser | GB/s | vs `json.loads` |
@@ -358,6 +391,7 @@ geometric mean over the 22 files (higher is better).
358
391
  | ujson 6.0.0 | 0.37 | 1.66× |
359
392
  | cysimdjson 26.27 | 0.43 | 1.96× |
360
393
  | pysimdjson 7.0.2 | 0.44 | 1.98× |
394
+ | jiter 0.17.0 | 0.48 | 2.21× |
361
395
  | msgspec 0.22.0 | 0.53 | 2.42× |
362
396
  | orjson 3.12.0 | 0.60 | 2.74× |
363
397
  | **fastsimdjson `loads`** | **0.78** | **3.53×** |
@@ -371,7 +405,9 @@ gain comes from pausing the garbage collector while the objects are built.
371
405
  Timing each call on its own, fastsimdjson's lead over orjson is 1.33× with
372
406
  the collector enabled and 1.22× with it disabled (geometric means); without
373
407
  the collector, `canada.json`, `mesh.json` and `numbers.json` are ties. yyjson 4.0.6 is left out: it
374
- returns wrong strings for non-ASCII text.
408
+ returns wrong strings for non-ASCII text. jiter (the parser of pydantic)
409
+ caches strings by default; turning the cache off (`cache_mode=False`) makes it
410
+ slower here (0.44 GB/s).
375
411
 
376
412
  Parsing is no longer the bottleneck. simdjson alone parses these files at
377
413
  3.1 GB/s. It accounts for about a third of the time of `loads`; the rest goes
@@ -7,7 +7,8 @@ replacement for the `json` module's `loads`, `load`, `dumps` and `dump`, and
7
7
  it can be over 3 times faster than the standard `json.loads` and
8
8
  `json.dumps`. When you only need part of a document, its lazy `parse`
9
9
  function is faster still. It also reads streams of documents (NDJSON, JSON
10
- Lines). It writes JSON in two ways: `dumps` returns the same `str` as
10
+ Lines), on many threads with free-threaded Python, and can extract only the
11
+ fields you need from each document. It writes JSON in two ways: `dumps` returns the same `str` as
11
12
  `json.dumps`, and `dumpb` returns the same `bytes` as `orjson.dumps`, as fast
12
13
  as orjson.
13
14
 
@@ -202,6 +203,37 @@ document, and the Python objects of all of them, to the calling thread.
202
203
  With the GIL (the default build of Python), the calling thread does all the
203
204
  work: `threads` has no effect on speed.
204
205
 
206
+ ### Only some fields: `fields=`
207
+
208
+ When you need a few values from each document, `fields` extracts them
209
+ without building the documents: only the values found become Python
210
+ objects.
211
+
212
+ ```python
213
+ events = fastsimdjson.loads_many(data, fields=["/type", "/payload/action"])
214
+ # (type, action) for each document
215
+
216
+ def count(t):
217
+ kind, action, language = t
218
+ ...
219
+ fastsimdjson.map_many(count, data, threads=64,
220
+ fields=["/type", "/payload/action",
221
+ "/payload/pull_request/base/repo/language"])
222
+ ```
223
+
224
+ `loads_many` and `map_many` accept `fields`: a JSON Pointer (RFC 6901), or a
225
+ list or tuple of them. Each document then becomes the value at the pointer,
226
+ or a tuple of the values at the pointers, in order. A value that is an
227
+ object or an array is built in full. A pointer that leads nowhere (a
228
+ missing key, an index past the end, or a step into a scalar) gives
229
+ `default`, `None` unless you pass another value; a JSON `null` gives `None`.
230
+ As with `at_pointer`, a key that appears twice in an object refers to its
231
+ first occurrence. `fields` cannot be combined with `lazy=True`.
232
+
233
+ On 7.6 GB of GitHub events, `map_many` with three fields ran at 2.8 GB/s on
234
+ one core of an Intel Xeon Gold 6548N, against 2.2 GB/s with lazy views
235
+ (`lazy=True`). With 64 threads, it ran at 78 GB/s (lazy views: 61 GB/s).
236
+
205
237
  ### `release`
206
238
 
207
239
  `release()` frees the simdjson parser and the string caches kept by the
@@ -276,8 +308,8 @@ The suite builds an ~80 MB document and a list of 16,777,221 integers, so
276
308
  give it some RAM.
277
309
 
278
310
  The benchmark scripts need simdjson-data and the `bench` extra (orjson,
279
- msgspec, pysimdjson, cysimdjson). `bench.py` times `loads` against
280
- `json.loads` and orjson, `bench_lazy.py` times `parse` against pysimdjson and
311
+ jiter, msgspec, pysimdjson, cysimdjson). `bench.py` times `loads` against
312
+ `json.loads`, orjson and jiter, `bench_lazy.py` times `parse` against pysimdjson and
281
313
  cysimdjson, `bench_dumps.py` times `dumps` against `json.dumps` and `dumpb`
282
314
  against orjson and msgspec, and `bench_many.py` times `loads_many`.
283
315
 
@@ -316,7 +348,7 @@ geometric mean over the 22 files (higher is better).
316
348
 
317
349
  <picture>
318
350
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads_dark.png">
319
- <img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads.png" width="80%" alt="Parsing JSON in Python: fastsimdjson loads 0.78 GB/s, orjson 0.60, msgspec 0.53, pysimdjson 0.44, cysimdjson 0.43, ujson 0.37, python-rapidjson 0.24, simplejson 0.23, json 0.22">
351
+ <img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads.png" width="80%" alt="Parsing JSON in Python: fastsimdjson loads 0.78 GB/s, orjson 0.60, msgspec 0.53, jiter 0.48, pysimdjson 0.44, cysimdjson 0.43, ujson 0.37, python-rapidjson 0.24, simplejson 0.23, json 0.22">
320
352
  </picture>
321
353
 
322
354
  | parser | GB/s | vs `json.loads` |
@@ -327,6 +359,7 @@ geometric mean over the 22 files (higher is better).
327
359
  | ujson 6.0.0 | 0.37 | 1.66× |
328
360
  | cysimdjson 26.27 | 0.43 | 1.96× |
329
361
  | pysimdjson 7.0.2 | 0.44 | 1.98× |
362
+ | jiter 0.17.0 | 0.48 | 2.21× |
330
363
  | msgspec 0.22.0 | 0.53 | 2.42× |
331
364
  | orjson 3.12.0 | 0.60 | 2.74× |
332
365
  | **fastsimdjson `loads`** | **0.78** | **3.53×** |
@@ -340,7 +373,9 @@ gain comes from pausing the garbage collector while the objects are built.
340
373
  Timing each call on its own, fastsimdjson's lead over orjson is 1.33× with
341
374
  the collector enabled and 1.22× with it disabled (geometric means); without
342
375
  the collector, `canada.json`, `mesh.json` and `numbers.json` are ties. yyjson 4.0.6 is left out: it
343
- returns wrong strings for non-ASCII text.
376
+ returns wrong strings for non-ASCII text. jiter (the parser of pydantic)
377
+ caches strings by default; turning the cache off (`cache_mode=False`) makes it
378
+ slower here (0.44 GB/s).
344
379
 
345
380
  Parsing is no longer the bottleneck. simdjson alone parses these files at
346
381
  3.1 GB/s. It accounts for about a third of the time of `loads`; the rest goes
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fastsimdjson"
7
- version = "0.6.0"
7
+ version = "0.7.0"
8
8
  description = "Fast JSON parsing for Python, built on simdjson"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -34,7 +34,7 @@ Issues = "https://github.com/simdjson/fastpysimdjson/issues"
34
34
 
35
35
  [project.optional-dependencies]
36
36
  test = ["pytest"]
37
- bench = ["orjson", "msgspec", "pysimdjson", "cysimdjson"]
37
+ bench = ["orjson", "jiter", "msgspec", "pysimdjson", "cysimdjson"]
38
38
 
39
39
  [tool.cibuildwheel]
40
40
  skip = ["pp*"]
@@ -892,11 +892,10 @@ PyObject *value_at(DocumentObject *owner, size_t i) {
892
892
  }
893
893
  }
894
894
 
895
- // Tape index of the value of the first field named key, or 0.
896
- size_t find_key(DocumentObject *owner, size_t start, const char *key,
897
- size_t len) {
898
- const uint64_t *tape = tape_of(owner);
899
- const uint8_t *strings = owner->doc->string_buf.get();
895
+ // Tape index of the value of the first field named key in the object at
896
+ // start, or 0.
897
+ size_t find_key_in(const uint64_t *tape, const uint8_t *strings, size_t start,
898
+ const char *key, size_t len) {
900
899
  size_t end = close_of(tape, start);
901
900
  for (size_t i = start + 1; i < end; i = skip(tape, i + 1)) {
902
901
  size_t idx = size_t(tape[i] & simdjson::internal::JSON_VALUE_MASK);
@@ -909,6 +908,11 @@ size_t find_key(DocumentObject *owner, size_t start, const char *key,
909
908
  return 0;
910
909
  }
911
910
 
911
+ size_t find_key(DocumentObject *owner, size_t start, const char *key,
912
+ size_t len) {
913
+ return find_key_in(tape_of(owner), owner->doc->string_buf.get(), start, key, len);
914
+ }
915
+
912
916
  // Tape index of element k of an array, starting from element j at tape
913
917
  // index i, or 0 when the array, which closes at `end`, is shorter.
914
918
  size_t walk_to(const uint64_t *tape, size_t i, size_t j, size_t k, size_t end) {
@@ -1002,6 +1006,234 @@ PyObject *at_pointer(DocumentObject *owner, size_t i, PyObject *arg) {
1002
1006
  return value_at(owner, i);
1003
1007
  }
1004
1008
 
1009
+ // --- fields= ------------------------------------------------------------------
1010
+ // loads_many(data, fields=...) and map_many(fn, data, fields=...): instead of
1011
+ // each document, the values at some JSON Pointers, found on the tape. Only
1012
+ // these values become Python objects.
1013
+
1014
+ struct FieldStep {
1015
+ std::string key;
1016
+ size_t index; // the token as an array index, or SIZE_MAX
1017
+ PyObject *name; // the token as a str (documents that json decoded)
1018
+ };
1019
+
1020
+ struct Fields {
1021
+ std::vector<std::vector<FieldStep>> paths;
1022
+ std::vector<size_t> shared; // leading steps in common with the previous path
1023
+ size_t depth = 0; // steps of the longest path
1024
+ bool single = false; // fields was one str: the value, not a tuple
1025
+ PyObject *dflt = nullptr; // the value of a missing field
1026
+
1027
+ ~Fields() {
1028
+ for (auto &p : paths) {
1029
+ for (auto &s : p) {
1030
+ Py_XDECREF(s.name);
1031
+ }
1032
+ }
1033
+ Py_XDECREF(dflt);
1034
+ }
1035
+ };
1036
+
1037
+ // Appends the steps of the JSON Pointer `arg` to `path`.
1038
+ bool compile_pointer(PyObject *arg, std::vector<FieldStep> &path) {
1039
+ Py_ssize_t plen;
1040
+ const char *p = PyUnicode_Check(arg) ? PyUnicode_AsUTF8AndSize(arg, &plen) : nullptr;
1041
+ if (p == nullptr) {
1042
+ if (!PyErr_Occurred()) {
1043
+ PyErr_Format(PyExc_TypeError, "fields must be JSON pointers (str), not %.200s",
1044
+ Py_TYPE(arg)->tp_name);
1045
+ }
1046
+ return false;
1047
+ }
1048
+ if (plen > 0 && p[0] != '/') {
1049
+ PyErr_Format(PyExc_ValueError, "invalid JSON pointer: %R", arg);
1050
+ return false;
1051
+ }
1052
+ const char *end = p + plen;
1053
+ for (const char *cur = p; cur < end;) {
1054
+ cur++; // '/'
1055
+ const char *stop = static_cast<const char *>(memchr(cur, '/', size_t(end - cur)));
1056
+ if (stop == nullptr) {
1057
+ stop = end;
1058
+ }
1059
+ std::string token(cur, size_t(stop - cur));
1060
+ cur = stop;
1061
+ for (size_t t = token.find('~'); t != std::string::npos; t = token.find('~', t + 1)) {
1062
+ if (t + 1 >= token.size() || (token[t + 1] != '0' && token[t + 1] != '1')) {
1063
+ PyErr_Format(PyExc_ValueError, "invalid JSON pointer: %R", arg);
1064
+ return false;
1065
+ }
1066
+ token.replace(t, 2, token[t + 1] == '0' ? "~" : "/");
1067
+ }
1068
+ bool digits = !token.empty() && (token.size() == 1 || token[0] != '0') &&
1069
+ token.size() <= 18;
1070
+ size_t k = 0;
1071
+ for (char ch : token) {
1072
+ digits = digits && ch >= '0' && ch <= '9';
1073
+ k = k * 10 + size_t(ch - '0');
1074
+ }
1075
+ PyObject *name = PyUnicode_FromStringAndSize(token.data(), Py_ssize_t(token.size()));
1076
+ if (name == nullptr) {
1077
+ return false;
1078
+ }
1079
+ path.push_back(FieldStep{std::move(token), digits ? k : SIZE_MAX, name});
1080
+ }
1081
+ return true;
1082
+ }
1083
+
1084
+ // The fields of `spec` (a JSON Pointer, or a list or tuple of them), or
1085
+ // nullptr with an exception.
1086
+ Fields *compile_fields(PyObject *spec, PyObject *dflt) {
1087
+ Fields *f = new (std::nothrow) Fields();
1088
+ if (f == nullptr) {
1089
+ PyErr_NoMemory();
1090
+ return nullptr;
1091
+ }
1092
+ f->dflt = Py_NewRef(dflt != nullptr ? dflt : Py_None);
1093
+ f->single = PyUnicode_Check(spec);
1094
+ PyObject *seq = f->single ? PyTuple_Pack(1, spec)
1095
+ : PyList_Check(spec) || PyTuple_Check(spec)
1096
+ ? PySequence_Tuple(spec)
1097
+ : nullptr;
1098
+ if (seq == nullptr) {
1099
+ if (!PyErr_Occurred()) {
1100
+ PyErr_Format(PyExc_TypeError,
1101
+ "fields must be a JSON pointer (str) or a list or tuple of them, not %.200s",
1102
+ Py_TYPE(spec)->tp_name);
1103
+ }
1104
+ delete f;
1105
+ return nullptr;
1106
+ }
1107
+ for (Py_ssize_t n = 0; n < PyTuple_GET_SIZE(seq); n++) {
1108
+ f->paths.emplace_back();
1109
+ if (!compile_pointer(PyTuple_GET_ITEM(seq, n), f->paths.back())) {
1110
+ Py_DECREF(seq);
1111
+ delete f;
1112
+ return nullptr;
1113
+ }
1114
+ const std::vector<FieldStep> &path = f->paths.back();
1115
+ size_t common = 0;
1116
+ if (n > 0) {
1117
+ const std::vector<FieldStep> &prev = f->paths[size_t(n) - 1];
1118
+ while (common < path.size() && common < prev.size() &&
1119
+ path[common].key == prev[common].key) {
1120
+ common++;
1121
+ }
1122
+ }
1123
+ f->shared.push_back(common);
1124
+ f->depth = std::max(f->depth, path.size());
1125
+ }
1126
+ Py_DECREF(seq);
1127
+ return f;
1128
+ }
1129
+
1130
+ // The fields of the document on the tape (root at index 1).
1131
+ PyObject *extract_fields(const document &doc, const Fields &f) {
1132
+ const uint64_t *tape = doc.tape.get();
1133
+ const uint8_t *strings = doc.string_buf.get();
1134
+ Builder b(doc);
1135
+ size_t n = f.paths.size();
1136
+ PyObject *tuple = f.single ? nullptr : PyTuple_New(Py_ssize_t(n));
1137
+ if (!f.single && tuple == nullptr) {
1138
+ return nullptr;
1139
+ }
1140
+ // pos[d]: tape index reached after d steps of the previous path, which
1141
+ // reached `reached` steps; paths that share a prefix walk it once.
1142
+ size_t stack[16];
1143
+ std::vector<size_t> heap;
1144
+ size_t *pos = stack;
1145
+ if (f.depth + 1 > 16) {
1146
+ heap.resize(f.depth + 1);
1147
+ pos = heap.data();
1148
+ }
1149
+ pos[0] = 1;
1150
+ size_t reached = 0;
1151
+ for (size_t p = 0; p < n; p++) {
1152
+ const std::vector<FieldStep> &path = f.paths[p];
1153
+ size_t d = std::min(f.shared[p], reached);
1154
+ size_t i = pos[d];
1155
+ for (; d < path.size(); d++) {
1156
+ uint8_t type = uint8_t(tape[i] >> 56);
1157
+ const FieldStep &s = path[d];
1158
+ if (type == '{') {
1159
+ i = find_key_in(tape, strings, i, s.key.data(), s.key.size());
1160
+ } else if (type == '[' && s.index != SIZE_MAX) {
1161
+ i = walk_to(tape, i + 1, 0, s.index, close_of(tape, i));
1162
+ } else {
1163
+ i = 0;
1164
+ }
1165
+ if (i == 0) {
1166
+ break;
1167
+ }
1168
+ pos[d + 1] = i;
1169
+ }
1170
+ reached = d;
1171
+ PyObject *v;
1172
+ if (d == path.size()) {
1173
+ v = b.build(i);
1174
+ if (v == nullptr) {
1175
+ Py_XDECREF(tuple);
1176
+ return nullptr;
1177
+ }
1178
+ } else {
1179
+ v = Py_NewRef(f.dflt);
1180
+ }
1181
+ if (f.single) {
1182
+ return v;
1183
+ }
1184
+ PyTuple_SET_ITEM(tuple, Py_ssize_t(p), v);
1185
+ }
1186
+ return tuple;
1187
+ }
1188
+
1189
+ PyObject *extract_fields_gc_paused(const document &doc, const Fields &f) {
1190
+ #ifndef Py_GIL_DISABLED
1191
+ int gc_was_enabled = PyGC_Disable();
1192
+ #endif
1193
+ PyObject *res = extract_fields(doc, f);
1194
+ #ifndef Py_GIL_DISABLED
1195
+ if (gc_was_enabled) {
1196
+ PyGC_Enable();
1197
+ }
1198
+ #endif
1199
+ return res;
1200
+ }
1201
+
1202
+ // The fields of a document that json decoded into Python objects.
1203
+ PyObject *extract_fields_py(PyObject *value, const Fields &f) {
1204
+ size_t n = f.paths.size();
1205
+ PyObject *tuple = f.single ? nullptr : PyTuple_New(Py_ssize_t(n));
1206
+ if (!f.single && tuple == nullptr) {
1207
+ return nullptr;
1208
+ }
1209
+ for (size_t p = 0; p < n; p++) {
1210
+ PyObject *v = value; // borrowed
1211
+ for (const FieldStep &s : f.paths[p]) {
1212
+ if (PyDict_Check(v)) {
1213
+ v = PyDict_GetItemWithError(v, s.name);
1214
+ if (v == nullptr && PyErr_Occurred()) {
1215
+ Py_XDECREF(tuple);
1216
+ return nullptr;
1217
+ }
1218
+ } else if (PyList_Check(v) && s.index != SIZE_MAX &&
1219
+ s.index < size_t(PyList_GET_SIZE(v))) {
1220
+ v = PyList_GET_ITEM(v, Py_ssize_t(s.index));
1221
+ } else {
1222
+ v = nullptr;
1223
+ }
1224
+ if (v == nullptr) {
1225
+ break;
1226
+ }
1227
+ }
1228
+ v = Py_NewRef(v != nullptr ? v : f.dflt);
1229
+ if (f.single) {
1230
+ return v;
1231
+ }
1232
+ PyTuple_SET_ITEM(tuple, Py_ssize_t(p), v);
1233
+ }
1234
+ return tuple;
1235
+ }
1236
+
1005
1237
  // --- Document ---------------------------------------------------------------
1006
1238
 
1007
1239
  void free_copy(document *d);
@@ -3102,6 +3334,8 @@ struct StreamObject {
3102
3334
  bool no_fallback; // a chunk of a parallel run: do not call json (see run_chunk)
3103
3335
  bool need_fallback; // set instead of calling json, with no_fallback
3104
3336
  bool borrowed_parser; // p is this thread's chunk parser (not deleted)
3337
+ Fields *fields; // fields=: the values at these pointers, not the documents
3338
+ bool owns_fields; // false in the chunks of a parallel run
3105
3339
  parser *p;
3106
3340
  document_stream *stream;
3107
3341
  document_stream::iterator it;
@@ -3266,6 +3500,9 @@ void free_copy(document *d) {
3266
3500
  }
3267
3501
 
3268
3502
  PyObject *stream_value(StreamObject *s) {
3503
+ if (s->fields != nullptr) {
3504
+ return extract_fields_gc_paused(s->p->doc, *s->fields);
3505
+ }
3269
3506
  if (!s->lazy) {
3270
3507
  return build_with_gc_paused(s->p->doc, 1);
3271
3508
  }
@@ -3313,6 +3550,11 @@ PyObject *stream_fallback(StreamObject *s) {
3313
3550
  return nullptr;
3314
3551
  }
3315
3552
  s->active = false; // resume with simdjson after this document
3553
+ if (s->fields != nullptr) {
3554
+ PyObject *v = extract_fields_py(value, *s->fields);
3555
+ Py_DECREF(value);
3556
+ return v;
3557
+ }
3316
3558
  // As with parse(), a document that only json accepts is returned as plain
3317
3559
  // Python objects.
3318
3560
  return value;
@@ -3387,6 +3629,9 @@ void stream_dealloc(PyObject *self) {
3387
3629
  } else {
3388
3630
  delete s->p;
3389
3631
  }
3632
+ if (s->owns_fields) {
3633
+ delete s->fields;
3634
+ }
3390
3635
  delete s->copy;
3391
3636
  Py_XDECREF(s->source);
3392
3637
  Py_XDECREF(s->text);
@@ -3474,6 +3719,8 @@ PyObject *make_stream(PyObject *args, PyObject *kwargs, bool lazy) {
3474
3719
  s->base = 0;
3475
3720
  s->last_end = 0;
3476
3721
  s->no_fallback = s->need_fallback = s->borrowed_parser = false;
3722
+ s->fields = nullptr;
3723
+ s->owns_fields = false;
3477
3724
  PyObject *self = reinterpret_cast<PyObject *>(s);
3478
3725
  if (must_copy) {
3479
3726
  PyObject *b = PyBytes_FromObject(data);
@@ -3574,6 +3821,8 @@ StreamObject *stream_over(StreamObject *whole, size_t from, size_t to, bool chun
3574
3821
  s->open = whole->open;
3575
3822
  s->close = whole->close;
3576
3823
  s->no_fallback = s->need_fallback = s->borrowed_parser = false;
3824
+ s->fields = whole->fields; // whole outlives its chunks
3825
+ s->owns_fields = false;
3577
3826
  if (chunk && !g_thread_parser.chunk_parser_busy) {
3578
3827
  if (g_thread_parser.chunk_parser == nullptr) {
3579
3828
  register_thread_cleanup();
@@ -4011,21 +4260,67 @@ PyObject *take_threads(PyObject *kwargs, Py_ssize_t *threads, bool *given) {
4011
4260
  return rest;
4012
4261
  }
4013
4262
 
4263
+ // Removes the keyword `name` from the dict kwargs (may be nullptr); returns
4264
+ // its value (new reference), or nullptr when absent or on error.
4265
+ PyObject *pop_keyword(PyObject *kwargs, const char *name) {
4266
+ if (kwargs == nullptr) {
4267
+ return nullptr;
4268
+ }
4269
+ PyObject *v = PyDict_GetItemString(kwargs, name); // borrowed
4270
+ if (v == nullptr) {
4271
+ return nullptr;
4272
+ }
4273
+ Py_INCREF(v);
4274
+ if (PyDict_DelItemString(kwargs, name) < 0) {
4275
+ Py_DECREF(v);
4276
+ return nullptr;
4277
+ }
4278
+ return v;
4279
+ }
4280
+
4014
4281
  // The stream for loads_many/parse_many/map_many; with threads, the list of
4015
- // all results instead.
4282
+ // all results instead. With fields (not for parse_many), the values at the
4283
+ // fields' pointers come instead of the documents.
4016
4284
  PyObject *stream_or_list(PyObject *args, PyObject *kwargs, bool lazy, PyObject *fn,
4017
- bool force_list) {
4285
+ bool force_list, bool allow_fields) {
4018
4286
  Py_ssize_t threads;
4019
4287
  bool given;
4020
4288
  PyObject *rest = take_threads(kwargs, &threads, &given);
4021
4289
  if (rest == nullptr && PyErr_Occurred()) {
4022
4290
  return nullptr;
4023
4291
  }
4292
+ Fields *fields = nullptr;
4293
+ if (allow_fields) {
4294
+ PyObject *spec = pop_keyword(rest, "fields");
4295
+ PyObject *dflt = PyErr_Occurred() ? nullptr : pop_keyword(rest, "default");
4296
+ if (PyErr_Occurred()) {
4297
+ // reported below
4298
+ } else if (spec != nullptr && spec != Py_None) {
4299
+ if (lazy) {
4300
+ PyErr_SetString(PyExc_ValueError, "fields cannot be combined with lazy=True");
4301
+ } else {
4302
+ fields = compile_fields(spec, dflt);
4303
+ }
4304
+ } else if (dflt != nullptr) {
4305
+ PyErr_SetString(PyExc_TypeError, "default requires fields");
4306
+ }
4307
+ Py_XDECREF(spec);
4308
+ Py_XDECREF(dflt);
4309
+ if (PyErr_Occurred()) {
4310
+ Py_XDECREF(rest);
4311
+ return nullptr;
4312
+ }
4313
+ }
4024
4314
  PyObject *self = make_stream(args, rest, lazy);
4025
4315
  Py_XDECREF(rest);
4026
4316
  if (self == nullptr) {
4317
+ delete fields;
4027
4318
  return nullptr;
4028
4319
  }
4320
+ if (fields != nullptr) {
4321
+ reinterpret_cast<StreamObject *>(self)->fields = fields;
4322
+ reinterpret_cast<StreamObject *>(self)->owns_fields = true;
4323
+ }
4029
4324
  if (!given && !force_list) {
4030
4325
  return self;
4031
4326
  }
@@ -4045,16 +4340,16 @@ PyObject *stream_or_list(PyObject *args, PyObject *kwargs, bool lazy, PyObject *
4045
4340
  }
4046
4341
 
4047
4342
  PyObject *loads_many(PyObject *, PyObject *args, PyObject *kwargs) {
4048
- return stream_or_list(args, kwargs, false, nullptr, false);
4343
+ return stream_or_list(args, kwargs, false, nullptr, false, true);
4049
4344
  }
4050
4345
 
4051
4346
  PyObject *parse_many(PyObject *, PyObject *args, PyObject *kwargs) {
4052
- return stream_or_list(args, kwargs, true, nullptr, false);
4347
+ return stream_or_list(args, kwargs, true, nullptr, false, false);
4053
4348
  }
4054
4349
 
4055
- // map_many(fn, data, *, format, batch_size, threads, lazy=False): [fn(doc) for
4056
- // doc in loads_many(data)] (parse_many with lazy=True), with fn called in the
4057
- // threads.
4350
+ // map_many(fn, data, *, format, batch_size, threads, lazy=False, fields,
4351
+ // default): [fn(doc) for doc in loads_many(data)] (parse_many with
4352
+ // lazy=True), with fn called in the threads.
4058
4353
  PyObject *map_many(PyObject *, PyObject *args, PyObject *kwargs) {
4059
4354
  if (PyTuple_GET_SIZE(args) < 1) {
4060
4355
  PyErr_SetString(PyExc_TypeError, "map_many() missing required argument 'fn'");
@@ -4086,7 +4381,7 @@ PyObject *map_many(PyObject *, PyObject *args, PyObject *kwargs) {
4086
4381
  Py_XDECREF(rest);
4087
4382
  return nullptr;
4088
4383
  }
4089
- PyObject *r = stream_or_list(tail, rest, lazy, fn, true);
4384
+ PyObject *r = stream_or_list(tail, rest, lazy, fn, true, true);
4090
4385
  Py_DECREF(tail);
4091
4386
  Py_XDECREF(rest);
4092
4387
  return r;
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastsimdjson
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Fast JSON parsing for Python, built on simdjson
5
5
  Author-email: Daniel Lemire <daniel@lemire.me>
6
6
  License-Expression: Apache-2.0
@@ -25,6 +25,7 @@ Provides-Extra: test
25
25
  Requires-Dist: pytest; extra == "test"
26
26
  Provides-Extra: bench
27
27
  Requires-Dist: orjson; extra == "bench"
28
+ Requires-Dist: jiter; extra == "bench"
28
29
  Requires-Dist: msgspec; extra == "bench"
29
30
  Requires-Dist: pysimdjson; extra == "bench"
30
31
  Requires-Dist: cysimdjson; extra == "bench"
@@ -38,7 +39,8 @@ replacement for the `json` module's `loads`, `load`, `dumps` and `dump`, and
38
39
  it can be over 3 times faster than the standard `json.loads` and
39
40
  `json.dumps`. When you only need part of a document, its lazy `parse`
40
41
  function is faster still. It also reads streams of documents (NDJSON, JSON
41
- Lines). It writes JSON in two ways: `dumps` returns the same `str` as
42
+ Lines), on many threads with free-threaded Python, and can extract only the
43
+ fields you need from each document. It writes JSON in two ways: `dumps` returns the same `str` as
42
44
  `json.dumps`, and `dumpb` returns the same `bytes` as `orjson.dumps`, as fast
43
45
  as orjson.
44
46
 
@@ -233,6 +235,37 @@ document, and the Python objects of all of them, to the calling thread.
233
235
  With the GIL (the default build of Python), the calling thread does all the
234
236
  work: `threads` has no effect on speed.
235
237
 
238
+ ### Only some fields: `fields=`
239
+
240
+ When you need a few values from each document, `fields` extracts them
241
+ without building the documents: only the values found become Python
242
+ objects.
243
+
244
+ ```python
245
+ events = fastsimdjson.loads_many(data, fields=["/type", "/payload/action"])
246
+ # (type, action) for each document
247
+
248
+ def count(t):
249
+ kind, action, language = t
250
+ ...
251
+ fastsimdjson.map_many(count, data, threads=64,
252
+ fields=["/type", "/payload/action",
253
+ "/payload/pull_request/base/repo/language"])
254
+ ```
255
+
256
+ `loads_many` and `map_many` accept `fields`: a JSON Pointer (RFC 6901), or a
257
+ list or tuple of them. Each document then becomes the value at the pointer,
258
+ or a tuple of the values at the pointers, in order. A value that is an
259
+ object or an array is built in full. A pointer that leads nowhere (a
260
+ missing key, an index past the end, or a step into a scalar) gives
261
+ `default`, `None` unless you pass another value; a JSON `null` gives `None`.
262
+ As with `at_pointer`, a key that appears twice in an object refers to its
263
+ first occurrence. `fields` cannot be combined with `lazy=True`.
264
+
265
+ On 7.6 GB of GitHub events, `map_many` with three fields ran at 2.8 GB/s on
266
+ one core of an Intel Xeon Gold 6548N, against 2.2 GB/s with lazy views
267
+ (`lazy=True`). With 64 threads, it ran at 78 GB/s (lazy views: 61 GB/s).
268
+
236
269
  ### `release`
237
270
 
238
271
  `release()` frees the simdjson parser and the string caches kept by the
@@ -307,8 +340,8 @@ The suite builds an ~80 MB document and a list of 16,777,221 integers, so
307
340
  give it some RAM.
308
341
 
309
342
  The benchmark scripts need simdjson-data and the `bench` extra (orjson,
310
- msgspec, pysimdjson, cysimdjson). `bench.py` times `loads` against
311
- `json.loads` and orjson, `bench_lazy.py` times `parse` against pysimdjson and
343
+ jiter, msgspec, pysimdjson, cysimdjson). `bench.py` times `loads` against
344
+ `json.loads`, orjson and jiter, `bench_lazy.py` times `parse` against pysimdjson and
312
345
  cysimdjson, `bench_dumps.py` times `dumps` against `json.dumps` and `dumpb`
313
346
  against orjson and msgspec, and `bench_many.py` times `loads_many`.
314
347
 
@@ -347,7 +380,7 @@ geometric mean over the 22 files (higher is better).
347
380
 
348
381
  <picture>
349
382
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads_dark.png">
350
- <img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads.png" width="80%" alt="Parsing JSON in Python: fastsimdjson loads 0.78 GB/s, orjson 0.60, msgspec 0.53, pysimdjson 0.44, cysimdjson 0.43, ujson 0.37, python-rapidjson 0.24, simplejson 0.23, json 0.22">
383
+ <img src="https://raw.githubusercontent.com/simdjson/fastpysimdjson/main/doc/perf_loads.png" width="80%" alt="Parsing JSON in Python: fastsimdjson loads 0.78 GB/s, orjson 0.60, msgspec 0.53, jiter 0.48, pysimdjson 0.44, cysimdjson 0.43, ujson 0.37, python-rapidjson 0.24, simplejson 0.23, json 0.22">
351
384
  </picture>
352
385
 
353
386
  | parser | GB/s | vs `json.loads` |
@@ -358,6 +391,7 @@ geometric mean over the 22 files (higher is better).
358
391
  | ujson 6.0.0 | 0.37 | 1.66× |
359
392
  | cysimdjson 26.27 | 0.43 | 1.96× |
360
393
  | pysimdjson 7.0.2 | 0.44 | 1.98× |
394
+ | jiter 0.17.0 | 0.48 | 2.21× |
361
395
  | msgspec 0.22.0 | 0.53 | 2.42× |
362
396
  | orjson 3.12.0 | 0.60 | 2.74× |
363
397
  | **fastsimdjson `loads`** | **0.78** | **3.53×** |
@@ -371,7 +405,9 @@ gain comes from pausing the garbage collector while the objects are built.
371
405
  Timing each call on its own, fastsimdjson's lead over orjson is 1.33× with
372
406
  the collector enabled and 1.22× with it disabled (geometric means); without
373
407
  the collector, `canada.json`, `mesh.json` and `numbers.json` are ties. yyjson 4.0.6 is left out: it
374
- returns wrong strings for non-ASCII text.
408
+ returns wrong strings for non-ASCII text. jiter (the parser of pydantic)
409
+ caches strings by default; turning the cache off (`cache_mode=False`) makes it
410
+ slower here (0.44 GB/s).
375
411
 
376
412
  Parsing is no longer the bottleneck. simdjson alone parses these files at
377
413
  3.1 GB/s. It accounts for about a third of the time of `loads`; the rest goes
@@ -1,6 +1,7 @@
1
1
 
2
2
  [bench]
3
3
  orjson
4
+ jiter
4
5
  msgspec
5
6
  pysimdjson
6
7
  cysimdjson
@@ -381,3 +381,104 @@ def test_map_many_lazy_views_kept():
381
381
  with pytest.raises(ValueError) as info:
382
382
  fastsimdjson.map_many(boom, data, threads=threads, lazy=True)
383
383
  assert to_py(info.value.args[0]) == expected[20000]
384
+
385
+
386
+ # fields=: the values at JSON Pointers instead of the documents.
387
+
388
+ _MISSING = object()
389
+
390
+
391
+ def _pointer_get(doc, pointer, default=None):
392
+ if pointer == "":
393
+ return doc
394
+ for token in pointer[1:].split("/"):
395
+ token = token.replace("~1", "/").replace("~0", "~")
396
+ if isinstance(doc, dict) and token in doc:
397
+ doc = doc[token]
398
+ elif (isinstance(doc, list) and token.isdigit() and (token == "0" or token[0] != "0")
399
+ and int(token) < len(doc)):
400
+ doc = doc[int(token)]
401
+ else:
402
+ return default
403
+ return doc
404
+
405
+
406
+ def _pointers(doc, prefix=""):
407
+ yield prefix
408
+ if isinstance(doc, dict):
409
+ for k, v in doc.items():
410
+ yield from _pointers(v, prefix + "/" + k.replace("~", "~0").replace("/", "~1"))
411
+ elif isinstance(doc, list):
412
+ for i, v in enumerate(doc):
413
+ yield from _pointers(v, f"{prefix}/{i}")
414
+
415
+
416
+ @pytest.mark.parametrize("seed", range(30))
417
+ def test_fields_random(seed):
418
+ rnd = random.Random(seed)
419
+ docs = [random_value(rnd) for _ in range(40)]
420
+ docs += [{"a/b": {"~": [1, {"k1": 2}]}, "": 3, "07": 4}, ["x", ["y"]]]
421
+ candidates = sorted({p for d in docs for p in _pointers(d)})
422
+ candidates += ["/nope", "/k1/nope", "/0/0/0", "/00", "/-", "/1x", "/a~1b/~0/1/k1", "/", "/07"]
423
+ text = ndjson(docs).encode()
424
+ for _ in range(10):
425
+ pointers = rnd.sample(candidates, min(len(candidates), rnd.randrange(1, 8)))
426
+ if rnd.random() < 0.5:
427
+ pointers.sort() # shared prefixes, one after the other
428
+ got = list(fastsimdjson.loads_many(text, fields=pointers))
429
+ assert len(got) == len(docs)
430
+ for g, d in zip(got, docs):
431
+ assert isinstance(g, tuple)
432
+ assert all(same(a, _pointer_get(d, p)) for a, p in zip(g, pointers))
433
+ one = pointers[0]
434
+ got = list(fastsimdjson.loads_many(text, fields=one, default=_MISSING))
435
+ assert all(same(a, _pointer_get(d, one, _MISSING)) for a, d in zip(got, docs))
436
+ assert fastsimdjson.map_many(lambda t: t, text, fields=tuple(pointers)) == \
437
+ list(fastsimdjson.loads_many(text, fields=pointers))
438
+
439
+
440
+ def test_fields_api():
441
+ data = b'{"a": 1, "b": {"c": [10, 20, {"d": "x"}]}}\n{"a": null}\n[5]\n7\n'
442
+ assert list(fastsimdjson.loads_many(data, fields=["/a", "/b/c/2/d", "/b/c/2", "/0"])) == [
443
+ (1, "x", {"d": "x"}, None), (None, None, None, None), (None, None, None, 5),
444
+ (None, None, None, None)]
445
+ # A missing field gets the default; null is a value.
446
+ assert list(fastsimdjson.loads_many(data, fields="/a", default=0)) == [1, None, 0, 0]
447
+ assert list(fastsimdjson.loads_many(data, fields="")) == list(fastsimdjson.loads_many(data))
448
+ assert list(fastsimdjson.loads_many(data, fields=[])) == [(), (), (), ()]
449
+ assert list(fastsimdjson.loads_many(data, fields=None)) == list(fastsimdjson.loads_many(data))
450
+ # Duplicate keys: the first one, as for views (at_pointer).
451
+ assert list(fastsimdjson.loads_many(b'{"a": 1, "a": 2}', fields="/a")) == [1]
452
+ # Documents that only json accepts.
453
+ assert list(fastsimdjson.loads_many(b'{"a": 1e400, "b": [1]}\n{"a": 2}',
454
+ fields=["/a", "/b/0"])) == [(float("inf"), 1), (2, None)]
455
+ for bad in ["a", "/~2", "/~"]:
456
+ with pytest.raises(ValueError):
457
+ fastsimdjson.loads_many(data, fields=bad)
458
+ for bad in [1, ["/a", 2], {"/a"}]:
459
+ with pytest.raises(TypeError):
460
+ fastsimdjson.loads_many(data, fields=bad)
461
+ with pytest.raises(ValueError):
462
+ fastsimdjson.map_many(len, data, fields="/a", lazy=True)
463
+ with pytest.raises(TypeError):
464
+ fastsimdjson.loads_many(data, default=1)
465
+ with pytest.raises(TypeError):
466
+ fastsimdjson.parse_many(data, fields="/a")
467
+ with pytest.raises(fastsimdjson.JSONDecodeError):
468
+ list(fastsimdjson.loads_many(b'{"a": 1}\n{"a": ', fields="/a"))
469
+
470
+
471
+ @pytest.mark.parametrize("fmt", ["whitespace", "lines", "json_seq"])
472
+ @pytest.mark.parametrize("seed", range(6))
473
+ def test_fields_threads(fmt, seed):
474
+ data = _threaded_input(seed, fmt)
475
+ pointers = ["/i", "/s", "/big", "/0", "/pretty/1", "/f"]
476
+ expected = _outcome(lambda: list(fastsimdjson.loads_many(data, format=fmt, fields=pointers)))
477
+ if expected[0] == "ok":
478
+ docs = list(fastsimdjson.loads_many(data, format=fmt))
479
+ assert expected[1] == [tuple(_pointer_get(d, p) for p in pointers) for d in docs]
480
+ for threads in (None, 1, 4):
481
+ assert _outcome(lambda: fastsimdjson.loads_many(data, format=fmt, fields=pointers,
482
+ threads=threads)) == expected
483
+ assert _outcome(lambda: fastsimdjson.map_many(lambda t: t, data, format=fmt,
484
+ fields=pointers, threads=threads)) == expected
File without changes
File without changes
File without changes
File without changes