diff --git a/CMakeLists.txt b/CMakeLists.txt index 64a6ccd..41acdeb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -163,6 +163,13 @@ target_link_libraries(mytest PRIVATE ${PROJECT_NAME} doctest nanobench simdjson) target_compile_options(mytest PRIVATE ${TEST_FLAGS}) doctest_discover_tests(mytest WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) +find_package(Python3 COMPONENTS Interpreter) +if(Python3_Interpreter_FOUND) + add_test(NAME python_bindings + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/test_python_bindings.py) +endif() + include(CMakePushCheckState) include(CheckCXXCompilerFlag) cmake_push_check_state() diff --git a/test_python_bindings.py b/test_python_bindings.py new file mode 100644 index 0000000..5d955a9 --- /dev/null +++ b/test_python_bindings.py @@ -0,0 +1,90 @@ +import json + +import weaseljson + + +class Recorder(weaseljson.WeaselJsonCallbacksBase): + def __init__(self): + self.keys = [] + self.strings = [] + self.numbers = [] + self.events = [] + self._current = bytearray() + + def _flush(self, target, data, done): + self._current.extend(data) + if done: + target.append(bytes(self._current)) + self._current = bytearray() + + def on_begin_object(self): + self.events.append("begin_object") + + def on_end_object(self): + self.events.append("end_object") + + def on_begin_array(self): + self.events.append("begin_array") + + def on_end_array(self): + self.events.append("end_array") + + def on_key_data(self, data, done): + self._flush(self.keys, data, done) + + def on_string_data(self, data, done): + self._flush(self.strings, data, done) + + def on_number_data(self, data, done): + self._flush(self.numbers, data, done) + + def on_true_literal(self): + self.events.append("true") + + def on_false_literal(self): + self.events.append("false") + + def on_null_literal(self): + self.events.append("null") + + +def parse_all(parser, data): + for i in range(len(data)): + status = parser.parse(data[i : i + 1]) + if status != weaseljson.WeaselJsonStatus.AGAIN: + return status + return parser.parse(b"") + + +def test_object_keys_routed_correctly(): + recorder = Recorder() + with weaseljson.WeaselJsonParser(recorder) as parser: + status = parse_all( + parser, json.dumps({"hello": "world", "foo": "bar"}).encode() + ) + + assert status == weaseljson.WeaselJsonStatus.OK, status + assert recorder.keys == [b"hello", b"foo"], recorder.keys + assert recorder.strings == [b"world", b"bar"], recorder.strings + + +def test_mixed_values(): + recorder = Recorder() + with weaseljson.WeaselJsonParser(recorder) as parser: + status = parse_all( + parser, + json.dumps({"answer": 42, "yes": True, "no": False, "nil": None}).encode(), + ) + + assert status == weaseljson.WeaselJsonStatus.OK, status + assert recorder.keys == [b"answer", b"yes", b"no", b"nil"], recorder.keys + assert recorder.numbers == [b"42"], recorder.numbers + assert recorder.events.count("true") == 1 + assert recorder.events.count("false") == 1 + assert recorder.events.count("null") == 1 + + +if __name__ == "__main__": + test_object_keys_routed_correctly() + test_mixed_values() + print("python bindings ok") diff --git a/weaseljson.py b/weaseljson.py index a39d953..f1ece45 100644 --- a/weaseljson.py +++ b/weaseljson.py @@ -15,6 +15,7 @@ class WeaselJsonCallbacks(ctypes.Structure): ("on_begin_object", event_callback), ("on_end_object", event_callback), ("on_string_data", data_callback), + ("on_key_data", data_callback), ("on_begin_array", event_callback), ("on_end_array", event_callback), ("on_number_data", data_callback), @@ -41,6 +42,9 @@ class WeaselJsonCallbacksBase: def on_string_data(self, data, done): pass + def on_key_data(self, data, done): + pass + def on_begin_array(self): pass @@ -151,6 +155,12 @@ def on_string_data(p, buf, len, done): self.on_string_data(bytes(ctypes.string_at(buf, len)), bool(done)) +@ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, ctypes.c_int) +def on_key_data(p, buf, len, done): + self = ctypes.cast(p, ctypes.POINTER(ctypes.py_object)).contents.value + self.on_key_data(bytes(ctypes.string_at(buf, len)), bool(done)) + + @ctypes.CFUNCTYPE(None, ctypes.c_void_p) def on_begin_array(p): self = ctypes.cast(p, ctypes.POINTER(ctypes.py_object)).contents.value @@ -191,6 +201,7 @@ c_callbacks = WeaselJsonCallbacks( on_begin_object, on_end_object, on_string_data, + on_key_data, on_begin_array, on_end_array, on_number_data, @@ -206,14 +217,15 @@ class MyCallbacks(WeaselJsonCallbacksBase): print(data) -with WeaselJsonParser(MyCallbacks()) as parser: - raw = json.dumps({"hello": "world", "foo": 42}).encode() - i = 0 - stride = 1 - while True: - slice = raw[i : i + stride] - s = parser.parse(slice) - if s != WeaselJsonStatus.AGAIN: - break - i += stride - print(s) +if __name__ == "__main__": + with WeaselJsonParser(MyCallbacks()) as parser: + raw = json.dumps({"hello": "world", "foo": 42}).encode() + i = 0 + stride = 1 + while True: + slice = raw[i : i + stride] + s = parser.parse(slice) + if s != WeaselJsonStatus.AGAIN: + break + i += stride + print(s)