├── tests ├── simple_json.json ├── __pycache__ │ ├── test.cpython-39-pytest-6.2.4.pyc │ ├── basic_test.cpython-39-pytest-6.2.4.pyc │ ├── test_basic.cpython-39-pytest-6.2.4.pyc │ ├── test_basic.cpython-39-pytest-7.1.2.pyc │ └── test_unicode.cpython-39-pytest-6.2.4.pyc └── test_basic.py ├── .gitignore ├── pyyjson ├── __init__.pyi ├── __init__.py ├── memory.h ├── memory.c ├── binding.c ├── wrapper.py └── serde.h ├── MANIFEST.in ├── setup.py ├── pyproject.toml ├── benchmark ├── simple_loads_bench.py ├── simple_dumps_bench.py └── bench.py ├── .github └── workflows │ └── python-app.yaml ├── README.md └── LICENSE /tests/simple_json.json: -------------------------------------------------------------------------------- 1 | {"a":1,"b":2,"c":3} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .pytest_cache/ 3 | *.egg-info 4 | compile_flags.txt 5 | -------------------------------------------------------------------------------- /pyyjson/__init__.pyi: -------------------------------------------------------------------------------- 1 | import enum 2 | from typing import Any, Optional, List, Dict, Union 3 | -------------------------------------------------------------------------------- /tests/__pycache__/test.cpython-39-pytest-6.2.4.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ita9naiwa/pyyjson/HEAD/tests/__pycache__/test.cpython-39-pytest-6.2.4.pyc -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | #check https://setuptools.pypa.io/en/latest/userguide/miscellaneous.html#controlling-files-in-the-distribution 2 | #to include headers 3 | graft pyyjson/*.h -------------------------------------------------------------------------------- /tests/__pycache__/basic_test.cpython-39-pytest-6.2.4.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ita9naiwa/pyyjson/HEAD/tests/__pycache__/basic_test.cpython-39-pytest-6.2.4.pyc -------------------------------------------------------------------------------- /tests/__pycache__/test_basic.cpython-39-pytest-6.2.4.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ita9naiwa/pyyjson/HEAD/tests/__pycache__/test_basic.cpython-39-pytest-6.2.4.pyc -------------------------------------------------------------------------------- /tests/__pycache__/test_basic.cpython-39-pytest-7.1.2.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ita9naiwa/pyyjson/HEAD/tests/__pycache__/test_basic.cpython-39-pytest-7.1.2.pyc -------------------------------------------------------------------------------- /tests/__pycache__/test_unicode.cpython-39-pytest-6.2.4.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ita9naiwa/pyyjson/HEAD/tests/__pycache__/test_unicode.cpython-39-pytest-6.2.4.pyc -------------------------------------------------------------------------------- /pyyjson/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | 'load', 3 | 'loads', 4 | 'dump', 5 | 'dumps', 6 | 'ReaderFlags', 7 | 'WriterFlags' 8 | ] 9 | 10 | from .wrapper import load, loads, dump, dumps 11 | from .wrapper import ReaderFlags, WriterFlags 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, Extension 2 | 3 | setup(ext_modules=[ 4 | Extension( 5 | 'pyyjson.cserde', 6 | [ 7 | 'pyyjson/binding.c', 8 | 'pyyjson/memory.c', 9 | 'pyyjson/yyjson.c', 10 | ], 11 | language='c' 12 | ) 13 | ] 14 | ) 15 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pyyjson" 7 | version = "0.2.0" 8 | authors = [ 9 | {name = "Hyunsung Lee", email = "ita9naiwa@gmail.com"} 10 | ] 11 | description="yet another json libs using yyjson" 12 | readme="README.md" 13 | requires-python = ">=3.3" 14 | license = {text = "MIT"} 15 | classifiers=['Programming Language :: Python :: 3', 16 | 'License :: OSI Approved :: MIT License'] 17 | # dependencies = [] 18 | -------------------------------------------------------------------------------- /pyyjson/memory.h: -------------------------------------------------------------------------------- 1 | #ifndef PY_YYJSON_MEMORY_H 2 | #define PY_YYJSON_MEMORY_H 3 | 4 | #define PY_SSIZE_T_CLEAN 5 | #include 6 | #include "yyjson.h" 7 | 8 | /** wrapper to use PyMem_Malloc with yyjson's allocator. **/ 9 | void* py_malloc(void *ctx, size_t size); 10 | 11 | /** wrapper to use PyMem_Realloc with yyjson's allocator. **/ 12 | void* py_realloc(void *ctx, void *ptr, size_t size); 13 | 14 | /** wrapper to use PyMem_Free with yyjson's allocator. **/ 15 | void py_free(void *ctx, void *ptr); 16 | 17 | extern yyjson_alc PyMem_Allocator; 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /pyyjson/memory.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "memory.h" 3 | 4 | /** wrapper to use PyMem_Malloc with yyjson's allocator. **/ 5 | void* 6 | py_malloc(void *ctx, size_t size) 7 | { 8 | return PyMem_Malloc(size); 9 | } 10 | 11 | /** wrapper to use PyMem_Realloc with yyjson's allocator. **/ 12 | void* 13 | py_realloc(void *ctx, void *ptr, size_t size) 14 | { 15 | return PyMem_Realloc(ptr, size); 16 | } 17 | 18 | /** wrapper to use PyMem_Free with yyjson's allocator. **/ 19 | void 20 | py_free(void *ctx, void *ptr) 21 | { 22 | PyMem_Free(ptr); 23 | } 24 | 25 | yyjson_alc PyMem_Allocator = { 26 | py_malloc, 27 | py_realloc, 28 | py_free, 29 | NULL 30 | }; 31 | -------------------------------------------------------------------------------- /benchmark/simple_loads_bench.py: -------------------------------------------------------------------------------- 1 | import json 2 | import orjson 3 | import ujson 4 | import pyyjson 5 | import time 6 | 7 | N = 5000 8 | 9 | obj_1 = [[{a: 10 * ["하하하하하하하하하", 3.14159, 1341341] for (i, a) in 10 | enumerate("abcdefghijklfaksdfjasejfasjiccabsbkq")}]] 11 | obj_2 = {"text": [[{str(a): a * a for a in range(1000)}] for _ in range(5)]} 12 | 13 | str_1 = json.dumps(obj_1) 14 | str_2 = json.dumps(obj_2) 15 | print("simple loads test") 16 | for lib, name in [(json,"json"), (ujson, "ujson"), (orjson, "orjson"), (pyyjson, "pyyjson")]: 17 | prev = time.time() 18 | for i in range(N): 19 | lib.dumps(obj_1) 20 | lib.dumps(obj_2) 21 | now = time.time() 22 | print(f"[lib {name}]:", now - prev, "secs") 23 | -------------------------------------------------------------------------------- /pyyjson/binding.c: -------------------------------------------------------------------------------- 1 | #define PY_SSIZE_T_CLEAN 2 | #include 3 | #include "yyjson.h" 4 | #include "memory.h" 5 | #include "serde.h" 6 | 7 | static PyMethodDef yymethods[] = { 8 | {"_loads", loads, METH_VARARGS, "json loads"}, 9 | {"_dumps", dumps, METH_VARARGS, "json dumps"}, 10 | {NULL, NULL, 0, NULL} /* Sentinel */ 11 | }; 12 | 13 | static PyModuleDef yymodule = { 14 | PyModuleDef_HEAD_INIT, 15 | "cserde", 16 | "Python bindings for the yyjson project.", 17 | -1, 18 | yymethods 19 | }; 20 | 21 | PyMODINIT_FUNC 22 | PyInit_cserde(void) 23 | { 24 | PyObject *m; 25 | m = PyModule_Create(&yymodule); 26 | if (m == NULL) { 27 | return NULL; 28 | } 29 | return m; 30 | } 31 | -------------------------------------------------------------------------------- /benchmark/simple_dumps_bench.py: -------------------------------------------------------------------------------- 1 | import json 2 | import orjson 3 | import ujson 4 | import pyyjson 5 | import time 6 | 7 | N = 10000 8 | 9 | obj_1 = [[{a: 10 * ["하하하하하하하하하", 3.14159, 1341341] for (i, a) in 10 | enumerate("abcdefghijklfaksdfjasejfasjiccabsbkq")}]] 11 | obj_2 = {"text": [[{str(a): a * a for a in range(1000)}] for _ in range(5)]} 12 | 13 | str_1 = json.dumps(obj_1) 14 | str_2 = json.dumps(obj_2) 15 | print("simple dumps test") 16 | for lib, name in [(json, "json"), (ujson, "ujson"), (orjson, "orjson"), (pyyjson, "pyyjson")]: 17 | prev = time.time() 18 | for i in range(N): 19 | lib.dumps(str_1) 20 | lib.dumps(str_2) 21 | now = time.time() 22 | print(f"[lib {name}]:", now - prev, "secs") 23 | -------------------------------------------------------------------------------- /.github/workflows/python-app.yaml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Build 5 | 6 | on: 7 | push: 8 | branches: [ "dev" ] 9 | pull_request: 10 | branches: [ "dev" ] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | lint: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Set up Python 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: 3.9 24 | - name: installation 25 | run: | 26 | pip install flake8; 27 | flake8 ./ --filename='*.py' --ignore=E402,E501,E731,E741 --exclude=__init__.py,setup.py,build/,tests/,benchmark/ 28 | 29 | build-and-test: 30 | runs-on: ${{ matrix.os }} 31 | strategy: 32 | matrix: 33 | os: [ubuntu-latest] 34 | python-version: ["3.7", "3.8", "3.9", "3.10"] 35 | cibw-arch: [auto, aarch64] 36 | steps: 37 | - uses: actions/checkout@v3 38 | with: 39 | path: source 40 | - uses: actions/setup-python@v4 41 | with: 42 | python-version: ${{ matrix.python-version }} 43 | - name: installation 44 | run: | 45 | cd source; 46 | pip install pytest 47 | pip install .; 48 | cd tests; 49 | pytest ./ 50 | # -1 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyyjson 2 | 3 | Yet another Json Parser for Python 4 | - based on yyjson bindings from @TkTech's ([py_yyjson](https://github.com/ibireme/yyjson)) 5 | - original c library: [yyjson](https://github.com/ibireme/yyjson) 6 | - benchmarks are based on [ultrajson](https://github.com/ultrajson/ultrajson) 7 | 8 | ### Installation 9 | 10 | 1. installation from pypi 11 | ``` 12 | pip install pyyjson 13 | ``` 14 | 15 | 2. installation from source 16 | ``` 17 | git clone https://github.com/ita9naiwa/pyyjson 18 | cd pyyjson 19 | pip install . 20 | ``` 21 | 22 | ### Supports 23 | it supports standard `json` lib's 24 | - dumps 25 | - loads 26 | - dump 27 | - load 28 | 29 | functions 30 | 31 | 32 | ### loads examples 33 | ```python 34 | >>> import pyyjson 35 | >>> pyyjson.loads("[1,2,3]") 36 | [1, 2, 3] 37 | >>> pyyjson.loads('[{"a":"b"}, 3, 4]') 38 | [{'a': 'b'}, 3, 4] 39 | ``` 40 | 41 | ### dumps example 42 | ```python 43 | >>> pyyjson.dumps([{'a': 'b'}, 3, 4]) 44 | '[{"a":"b"},3,4]' 45 | ``` 46 | 47 | ### load example 48 | 49 | `load` take either string or file object. If string is given, it is assumed that it's a path to json file 50 | 51 | 52 | ```python 53 | >>> import pyyjson 54 | >>> pyyjson.load("simple_json.json") # in "tests/" directory 55 | {'a': 1, 'b': 2, 'c': 3} 56 | ``` 57 | 58 | ### dump example 59 | 60 | `dump` take either string or file object. If string is given, it is assumed that it's a path to save the file 61 | 62 | ```python 63 | >>> import pyyjson 64 | >>> pyyjson.dump({'a': 1, 'b': 2, 'c': 3}, "simple_json.json") 65 | ``` 66 | 67 | ### Benchmarks 68 | each elements in cols denotes "calls/sec". Test suite is adapted from ujson's benchmark format. 69 | 70 | | content | | json | ujson | orjson | pyyjson(mine) | 71 | |:-----------------------:|:------:|:-----:|:-----:|:------:|:-------------:| 72 | | Arr of 256 doubles | ENCODE | 10119 | 35361 | 170829 |44925 | 73 | | Arr of 256 dobules | DECODE | 28684 | 54593 | 170383 | 155127 | 74 | | Arr of 256 UTF-8 string | ENCODE | 5247 | 7344 | 45684 | 8554 | 75 | | Arr of 256 UTF-8 String | DECODE | 2838 | 5223 | 7248 | 6424 | 76 | | Arr of 256 strings | ENCODE | 36624 | 59568 | 216707 | 87624 | 77 | | Arr of 256 strings | DECODE | 66842 | 66960 | 100252 | 98242 | 78 | | Medium complex object | ENCODE | 9948 | 22344 | 80465 | 30888 | 79 | | Medium complex object | DECODE | 17183 | 28733 | 45008 | 29267 | 80 | -------------------------------------------------------------------------------- /tests/test_basic.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import json, pyyjson 3 | 4 | obj_1 = [[{a:10 * ["AKGAKEKAKCASKC", "ASDKAEKAC", 1341341] for (i, a) in 5 | enumerate("abcdefghijklfaksdfjasejfasjiccabsbkq")}]] 6 | obj_2 = {"text": [[{str(a):a*a for a in range(1000)}] for _ in range(13)]} 7 | 8 | str_1 = json.dumps(obj_1) 9 | str_2 = json.dumps(obj_2) 10 | 11 | def test0_basic_load_dump(): 12 | for obj in [obj_1, obj_2]: 13 | with open("/tmp/test_pyyjson.json", 'w') as f: 14 | json.dump(obj, f) 15 | with open("/tmp/test_pyyjson.json", 'r') as f: 16 | ret = json.load(f) 17 | assert ret == obj 18 | 19 | def test1_basic_loads(): 20 | assert json.loads(str_1) == pyyjson.loads(str_1) 21 | assert json.loads(str_2) == pyyjson.loads(str_2) 22 | 23 | def test2_basic_dumps(): 24 | str_1_ = pyyjson.dumps(obj_1) 25 | assert json.loads(str_1_) == pyyjson.loads(str_1_) 26 | assert pyyjson.loads(str_1_) == obj_1 27 | 28 | str_2_ = pyyjson.dumps(obj_2) 29 | assert json.loads(str_2_) == pyyjson.loads(str_2_) 30 | assert pyyjson.loads(str_2_) == obj_2 31 | 32 | def test3_load_bytes_and_strs(): 33 | a = b"[1,2,3,4]" 34 | b = "[1,2,3,4]" 35 | assert json.loads(a) == [1,2,3,4] 36 | assert json.loads(b) == [1,2,3,4] 37 | 38 | def test4_unicode_json_compatibility(): 39 | for ensure_ascii in [False, True]: 40 | msg = {"김밥": "이는고양이", "오뎅": ["그러면", "누가", "멍멍이지?"]} 41 | d = json.dumps(msg, ensure_ascii=ensure_ascii) 42 | assert json.loads(d) == pyyjson.loads(d) 43 | d = pyyjson.dumps(msg, ensure_ascii=ensure_ascii) 44 | assert json.loads(d) == pyyjson.loads(d) 45 | 46 | def test5_bytes(): 47 | msg_bytes = {"ita9naiwa": b"blahblah"} 48 | msg_nobytes = {"ita9naiwa": "blahblah"} 49 | assert pyyjson.dumps(msg_bytes) == pyyjson.dumps(msg_nobytes) 50 | 51 | def test6_default_example(): 52 | import datetime, time 53 | now = datetime.datetime.now() 54 | def default_parser1(obj): 55 | if isinstance(obj, datetime.datetime): 56 | return int(time.mktime(obj.timetuple())) 57 | raise TypeError(f"Type {type(obj)} is not serializable") 58 | pyyjson.dumps([1,2, now], default=default_parser1) 59 | 60 | def test7_default_more(): 61 | def default_f(x, y): 62 | return x 63 | with pytest.raises(TypeError): 64 | pyyjson.dumps(obj_1, default=default_f) 65 | not_callable_default = "it is not a function" 66 | with pytest.raises(TypeError): 67 | pyyjson.dumps(obj_1, default=not_callable_default) 68 | -------------------------------------------------------------------------------- /pyyjson/wrapper.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import io 3 | import os 4 | from pyyjson.cserde import _loads, _dumps 5 | from inspect import signature 6 | 7 | 8 | class ReaderFlags(enum.IntFlag): 9 | """ 10 | Flags that can be passed into JSON reading functions to control parsing 11 | behaviour. 12 | """ 13 | #: Stop when done instead of issues an error if there's additional content 14 | #: after a JSON document. This option may be used to parse small pieces of 15 | # JSON in larger data, such as NDJSON. 16 | STOP_WHEN_DONE = 0x02 17 | #: Allow single trailing comma at the end of an object or array, such as 18 | #: [1,2,3,] {"a":1,"b":2,}. 19 | ALLOW_TRAILING_COMMAS = 0x04 20 | #: Allow C-style single line and multiple line comments. 21 | ALLOW_COMMENTS = 0x08 22 | #: Allow inf/nan number and literal, case-insensitive, such as 1e999, NaN, 23 | #: inf, -Infinity 24 | ALLOW_INF_AND_NAN = 0x10 25 | #: Read number as raw string. inf/nan 26 | #: literal is also read as raw with `ALLOW_INF_AND_NAN` flag. 27 | NUMBERS_AS_RAW = 0x20 28 | 29 | 30 | class WriterFlags(enum.IntFlag): 31 | """ 32 | Flags that can be passed into JSON writing functions to control writing 33 | behaviour. 34 | """ 35 | #: Write the JSON with 4-space indents and newlines. 36 | PRETTY = 0x01 37 | #: Escapes unicode as \uXXXXX so that all output is ASCII. 38 | ESCAPE_UNICODE = 0x02 39 | #: Escapes / as \/. 40 | ESCAPE_SLASHES = 0x04 41 | #: Writes Infinity and NaN. 42 | ALLOW_INF_AND_NAN = 0x08 43 | #: Writes Infinity and NaN as `null` instead of raising an error. 44 | INF_AND_NAN_AS_NULL = 0x10 45 | 46 | 47 | def loads(doc, flags=0x00): 48 | return _loads(doc, flags) 49 | 50 | 51 | def load(fp, flags=0x00): 52 | if type(fp) == io.TextIOWrapper: 53 | txt = fp.read() 54 | elif type(fp) == str: 55 | if not os.path.exists(fp): 56 | raise FileNotFoundError(f"{fp} not found!") 57 | with open(fp, 'r') as f: 58 | txt = f.read() 59 | return loads(txt, flags) 60 | 61 | 62 | def __default(x): 63 | return x 64 | 65 | 66 | def dumps(obj, ensure_ascii=False, default=None, escape_slash=False, flags=0x00): 67 | _flags = 0x00 68 | if ensure_ascii: 69 | _flags |= WriterFlags.ESCAPE_UNICODE 70 | if escape_slash: 71 | _flags |= WriterFlags.ESCAPE_SLASHES 72 | _flags |= flags 73 | if default is None: 74 | default = __default 75 | else: 76 | if not callable(default): 77 | raise TypeError("default must be a callable object(such as function)") 78 | sig = signature(default) 79 | if len(sig.parameters) != 1: 80 | raise TypeError("default function must have a single parameter") 81 | return _dumps(obj, default, flags) 82 | 83 | 84 | def dump(obj, fp, ensure_ascii=False, default=None, escape_slash=False, flags=0x00): 85 | ret_str = dumps(obj, ensure_ascii=ensure_ascii, default=default, escape_slash=escape_slash, flags=flags) 86 | # return fp.write(ret_str) 87 | if type(fp) == io.TextIOWrapper: 88 | fp.write(ret_str) 89 | elif type(fp) == str: 90 | if not os.path.exists(fp): 91 | raise FileNotFoundError(f"{fp} not found!") 92 | with open(fp, 'w') as f: 93 | f.write(ret_str) 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Hyunsung Lee 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | 21 | 22 | ======== 23 | 24 | py_yyjson module is used under the licence following: 25 | 26 | Copyright (c) 2020 Tyler Kennedy 27 | 28 | Permission is hereby granted, free of charge, to any person obtaining a copy 29 | of this software and associated documentation files (the "Software"), to deal 30 | in the Software without restriction, including without limitation the rights 31 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 32 | copies of the Software, and to permit persons to whom the Software is 33 | furnished to do so, subject to the following conditions: 34 | 35 | The above copyright notice and this permission notice shall be included in all 36 | copies or substantial portions of the Software. 37 | 38 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 39 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 40 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 41 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 42 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 43 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 44 | SOFTWARE. 45 | 46 | ======== 47 | 48 | yyjson.c and yyjson.h are used under the following license: 49 | 50 | MIT License 51 | 52 | Copyright (c) 2020 YaoYuan 53 | 54 | Permission is hereby granted, free of charge, to any person obtaining a copy 55 | of this software and associated documentation files (the "Software"), to deal 56 | in the Software without restriction, including without limitation the rights 57 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 58 | copies of the Software, and to permit persons to whom the Software is 59 | furnished to do so, subject to the following conditions: 60 | 61 | The above copyright notice and this permission notice shall be included in all 62 | copies or substantial portions of the Software. 63 | 64 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 65 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 66 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 67 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 68 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 69 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 70 | SOFTWARE. 71 | -------------------------------------------------------------------------------- /pyyjson/serde.h: -------------------------------------------------------------------------------- 1 | #define PY_SSIZE_T_CLEAN 2 | #include "Python.h" 3 | #include "object.h" 4 | #include "yyjson.h" 5 | #include "memory.h" 6 | 7 | #include 8 | 9 | static inline PyObject * 10 | yyjson_val_to_py_obj(yyjson_val * val); 11 | 12 | static inline yyjson_mut_val* 13 | py_obj_to_yyjson_mut_val(yyjson_mut_doc *doc, PyObject *obj, PyObject * fallback_f); 14 | 15 | static PyObject * 16 | loads(PyObject *self, PyObject *args); 17 | 18 | static PyObject * 19 | dumps(PyObject *self, PyObject *args); 20 | 21 | static inline PyObject * 22 | yyjson_val_to_py_obj(yyjson_val * val) 23 | { 24 | yyjson_type type = yyjson_get_type(val); 25 | switch (type) { 26 | case YYJSON_TYPE_NULL: 27 | Py_RETURN_NONE; 28 | case YYJSON_TYPE_BOOL: 29 | if (yyjson_get_subtype(val) == YYJSON_SUBTYPE_TRUE) 30 | Py_RETURN_TRUE; 31 | else 32 | Py_RETURN_FALSE; 33 | case YYJSON_TYPE_NUM: { 34 | switch (yyjson_get_subtype(val)) { 35 | case YYJSON_SUBTYPE_UINT: 36 | return PyLong_FromUnsignedLongLong(yyjson_get_uint(val)); 37 | case YYJSON_SUBTYPE_SINT: 38 | return PyLong_FromLongLong(yyjson_get_sint(val)); 39 | case YYJSON_SUBTYPE_REAL: 40 | return PyFloat_FromDouble(yyjson_get_real(val)); 41 | } 42 | } 43 | case YYJSON_TYPE_STR: { 44 | size_t str_len = yyjson_get_len(val); 45 | const char *str = yyjson_get_str(val); 46 | return PyUnicode_FromStringAndSize(str, str_len); 47 | } 48 | case YYJSON_TYPE_ARR: { 49 | PyObject *arr = PyList_New(yyjson_arr_size(val)); 50 | if (!arr) 51 | return NULL; 52 | yyjson_val *obj_val; 53 | PyObject *py_val; 54 | yyjson_arr_iter iter = {0}; 55 | yyjson_arr_iter_init(val, &iter); 56 | size_t idx = 0; 57 | while ((obj_val = yyjson_arr_iter_next(&iter))) { 58 | py_val = yyjson_val_to_py_obj(obj_val); 59 | if (!py_val) 60 | return NULL; 61 | PyList_SET_ITEM(arr, idx++, py_val); 62 | } 63 | return arr; 64 | } 65 | case YYJSON_TYPE_OBJ: { 66 | PyObject *dict = PyDict_New(); 67 | if (!dict) 68 | return NULL; 69 | yyjson_val *obj_key, *obj_val; 70 | PyObject *py_key, *py_val; 71 | yyjson_obj_iter iter = {0}; 72 | yyjson_obj_iter_init(val, &iter); 73 | while ((obj_key = yyjson_obj_iter_next(&iter))) { 74 | obj_val = yyjson_obj_iter_get_val(obj_key); 75 | size_t str_len = yyjson_get_len(obj_key); 76 | const char *str = yyjson_get_str(obj_key); 77 | py_key = PyUnicode_FromStringAndSize(str, str_len); 78 | if (!py_key) 79 | return NULL; 80 | py_val = yyjson_val_to_py_obj(obj_val); 81 | if (!py_val) { 82 | Py_DecRef(py_key); // is it necessary? 83 | return NULL; 84 | } 85 | if(PyDict_SetItem(dict, py_key, py_val) == -1) 86 | return NULL; 87 | Py_DecRef(py_key); 88 | Py_DecRef(py_val); 89 | } 90 | return dict; 91 | } 92 | case YYJSON_TYPE_RAW: 93 | return PyLong_FromString(yyjson_get_raw(val), NULL, 10); 94 | case YYJSON_TYPE_NONE: 95 | default: 96 | PyErr_SetString(PyExc_TypeError, "Unknown Json Type Encountered"); 97 | return NULL; 98 | } 99 | } 100 | 101 | 102 | static inline yyjson_mut_val* 103 | py_obj_to_yyjson_mut_val(yyjson_mut_doc *doc, PyObject *obj, PyObject *fallback_f) { 104 | const PyTypeObject *ob_type = obj->ob_type; 105 | if (ob_type == &PyUnicode_Type) { 106 | Py_ssize_t str_len; 107 | const char *str = PyUnicode_AsUTF8AndSize(obj, &str_len); 108 | return yyjson_mut_strncpy(doc, str, str_len); 109 | } else if (ob_type == &PyBytes_Type) { 110 | const char *errors; 111 | Py_ssize_t str_len; 112 | PyObject *new_obj = PyCodec_Decode(obj, "utf-8", errors); 113 | const char *str = PyUnicode_AsUTF8AndSize(new_obj, &str_len); 114 | Py_DecRef(new_obj); 115 | return yyjson_mut_strncpy(doc, str, str_len); 116 | } else if (PyObject_IsInstance(obj, (PyObject*)&PyLong_Type)) { 117 | // ignore just integers larger than 64bits 118 | int overflow = 0; 119 | const int64_t num = PyLong_AsLongLongAndOverflow(obj, &overflow); 120 | if (!overflow){ 121 | return yyjson_mut_sint(doc, num); 122 | } else { 123 | PyObject *str_repr = PyObject_Str(obj); 124 | Py_ssize_t str_len; 125 | const char *str = PyUnicode_AsUTF8AndSize(str_repr, &str_len); 126 | return yyjson_mut_rawncpy(doc, str, str_len); 127 | } 128 | } else if (PyObject_IsInstance(obj, (PyObject*)&PyList_Type)) { 129 | yyjson_mut_val *val = yyjson_mut_arr(doc); 130 | for (Py_ssize_t i = 0; i < PyList_GET_SIZE(obj); i++) { 131 | yyjson_mut_arr_append(val, py_obj_to_yyjson_mut_val(doc, PyList_GET_ITEM(obj, i), fallback_f)); 132 | } 133 | return val; 134 | } else if (PyObject_IsInstance(obj, (PyObject*)&PyDict_Type)) { 135 | yyjson_mut_val *val = yyjson_mut_obj(doc); 136 | Py_ssize_t i = 0; 137 | PyObject *k, *v; 138 | while (PyDict_Next(obj, &i, &k, &v)) { 139 | // key must be string in json representation force transform 140 | PyObject *str_repr = PyObject_Str(k); 141 | Py_ssize_t str_len; 142 | const char *str = PyUnicode_AsUTF8AndSize(str_repr, &str_len); 143 | yyjson_mut_val *new_key = yyjson_mut_strncpy(doc, str, str_len); 144 | yyjson_mut_obj_add(val, new_key, py_obj_to_yyjson_mut_val(doc, v, fallback_f)); 145 | } 146 | return val; 147 | } else if (PyObject_IsInstance(obj, (PyObject*)&PyFloat_Type)) { 148 | double dnum = PyFloat_AsDouble(obj); 149 | if (dnum == -1 && PyErr_Occurred()) return NULL; 150 | return yyjson_mut_real(doc, dnum); 151 | } else if (obj == Py_True) { 152 | return yyjson_mut_true(doc); 153 | } else if (obj == Py_False) { 154 | return yyjson_mut_false(doc); 155 | } else if (obj == Py_None) { 156 | return yyjson_mut_null(doc); 157 | } else { 158 | if (fallback_f != NULL) { 159 | PyObject *args = PyTuple_Pack(1, obj); 160 | PyObject *new_repr = PyObject_CallObject(fallback_f, args); 161 | yyjson_mut_val *val = py_obj_to_yyjson_mut_val(doc, new_repr, NULL); 162 | Py_DecRef(args); 163 | Py_DecRef(new_repr); 164 | return val; 165 | } else { 166 | // looped here 167 | // force fallback to string 168 | PyObject *str_repr = PyObject_Str(obj); 169 | Py_ssize_t str_len; 170 | const char *str = PyUnicode_AsUTF8AndSize(str_repr, &str_len); 171 | Py_DecRef(str_repr); 172 | return yyjson_mut_rawncpy(doc, str, str_len); 173 | } 174 | PyObject *args = PyTuple_Pack(1, obj); 175 | PyObject *new_repr = PyObject_CallObject(fallback_f, args); 176 | yyjson_mut_val *val = py_obj_to_yyjson_mut_val(doc, new_repr, NULL); 177 | PyObject *str_repr = PyObject_Str(obj); 178 | Py_ssize_t str_len; 179 | const char *str = PyUnicode_AsUTF8AndSize(str_repr, &str_len); 180 | Py_DecRef(str_repr); 181 | return yyjson_mut_rawncpy(doc, str, str_len); 182 | } 183 | } 184 | 185 | static PyObject * 186 | loads(PyObject *self, PyObject *args) { 187 | PyObject *content_py; 188 | yyjson_read_err err; 189 | yyjson_read_flag r_flag = 0; 190 | 191 | if(!PyArg_ParseTuple(args, "OI", &content_py, &r_flag)) { 192 | PyErr_SetString(PyExc_TypeError, "Args Parse Error"); 193 | return Py_None; 194 | } 195 | Py_ssize_t str_len; 196 | const char *content; 197 | if (PyUnicode_Check(content_py)) 198 | content = PyUnicode_AsUTF8AndSize(content_py, &str_len); 199 | else if (PyBytes_Check(content_py)) 200 | PyBytes_AsStringAndSize(content_py, (char**) &content, &str_len); 201 | else { 202 | PyErr_SetString(PyExc_ValueError, "Either Bytes or String should be given"); 203 | return Py_None; 204 | } 205 | yyjson_doc *val = yyjson_read_opts( 206 | (char*)content, 207 | str_len, 208 | r_flag, 209 | &PyMem_Allocator, 210 | &err 211 | ); 212 | if (!val) { 213 | PyErr_SetString(PyExc_ValueError, err.msg); 214 | return Py_None; 215 | } 216 | yyjson_val *root = yyjson_doc_get_root(val); 217 | PyObject *ret = yyjson_val_to_py_obj(root); 218 | yyjson_doc_free(val); 219 | return ret; 220 | } 221 | 222 | static PyObject * 223 | dumps(PyObject *self, PyObject *args) { 224 | PyObject *obj; 225 | PyObject *fallback_f; 226 | yyjson_read_flag w_flag = 0; 227 | 228 | if(!PyArg_ParseTuple(args, "OOI", &obj, &fallback_f, &w_flag)) { 229 | PyErr_SetString(PyExc_TypeError, "Args Parse Error"); 230 | return Py_None; 231 | } 232 | yyjson_mut_doc *new_doc = yyjson_mut_doc_new(&PyMem_Allocator); 233 | yyjson_mut_val *root = py_obj_to_yyjson_mut_val(new_doc, obj, fallback_f); 234 | yyjson_mut_doc_set_root(new_doc, root); 235 | char* result = NULL; 236 | size_t w_len; 237 | yyjson_write_err w_err; 238 | 239 | result = yyjson_mut_val_write_opts(root, w_flag, &PyMem_Allocator, &w_len, &w_err); 240 | PyObject *obj_ret = PyUnicode_FromStringAndSize(result, w_len); 241 | PyMem_Allocator.free(NULL, result); 242 | yyjson_mut_doc_free(new_doc); 243 | return obj_ret; 244 | } 245 | -------------------------------------------------------------------------------- /benchmark/bench.py: -------------------------------------------------------------------------------- 1 | # https://github.com/ultrajson/ultrajson/blob/main/tests/benchmark.py 2 | # original author are @hugovk, and other contributors to ultrajson 3 | # coding=UTF-8 4 | import json 5 | import os 6 | import platform 7 | import random 8 | import sys 9 | import timeit 10 | from collections import defaultdict 11 | 12 | import ujson, yyjson 13 | 14 | # Will be set by "main" if user requests them 15 | simplejson = None 16 | nujson = None 17 | orjson = None 18 | 19 | USER = { 20 | "userId": 3381293, 21 | "age": 213, 22 | "username": "johndoe", 23 | "fullname": "John Doe the Second", 24 | "isAuthorized": True, 25 | "liked": 31231.31231202, 26 | "approval": 31.1471, 27 | "jobs": [1, 2], 28 | "currJob": None, 29 | } 30 | FRIENDS = [USER, USER, USER, USER, USER, USER, USER, USER] 31 | 32 | decode_data = None 33 | test_object = None 34 | 35 | benchmark_results = [] 36 | 37 | 38 | benchmark_registry = defaultdict(dict) 39 | 40 | 41 | # ============================================================================= 42 | # Benchmark registration 43 | # ============================================================================= 44 | 45 | 46 | def register_benchmark(libname, testname): 47 | def _wrap(func): 48 | benchmark_registry[testname][libname] = func 49 | return func 50 | 51 | return _wrap 52 | 53 | 54 | # ============================================================================= 55 | # Logging benchmarking results. 56 | # ============================================================================= 57 | def results_new_benchmark(name): 58 | benchmark_results.append((name, {}, {})) 59 | print(name) 60 | 61 | 62 | def results_record_result(callback, is_encode, count): 63 | callback_name = callback.__name__ 64 | library = callback_name.split("_")[-1] 65 | try: 66 | results = timeit.repeat( 67 | f"{callback_name}()", 68 | f"from __main__ import {callback_name}", 69 | repeat=10, 70 | number=count, 71 | ) 72 | except (TypeError, json.decoder.JSONDecodeError): 73 | return 74 | result = count / min(results) 75 | benchmark_results[-1][1 if is_encode else 2][library] = result 76 | 77 | print( 78 | "{} {}: {:,.02f} calls/sec".format( 79 | library, "encode" if is_encode else "decode", result 80 | ) 81 | ) 82 | 83 | 84 | def results_output_table(libraries): 85 | uname_system, _, uname_release, uname_version, _, uname_processor = platform.uname() 86 | print() 87 | print("### Test machine") 88 | print() 89 | print(uname_system, uname_release, uname_processor, uname_version) 90 | print() 91 | print("### Versions") 92 | print() 93 | print( 94 | "- {} {}".format( 95 | platform.python_implementation(), sys.version.replace("\n", "") 96 | ) 97 | ) 98 | for libname in libraries: 99 | module = globals()[libname] 100 | print(f"- {libname:<12} : {module.__version__}") 101 | print() 102 | 103 | column_widths = [max(len(r[0]) for r in benchmark_results)] 104 | for library in libraries: 105 | column_widths.append(max(10, len(library))) 106 | 107 | columns = [" " * (width + 2) for width in column_widths] 108 | for i, library in enumerate(libraries): 109 | columns[i + 1] = (" " + library).ljust(column_widths[i + 1] + 2) 110 | print("|{}|".format("|".join(columns))) 111 | 112 | line = ( 113 | "|" 114 | + "-" * (column_widths[0] + 2) 115 | + "|" 116 | + ":|".join("-" * (width + 1) for width in column_widths[1:]) 117 | + ":|" 118 | ) 119 | print(line) 120 | 121 | for name, encodes, decodes in benchmark_results: 122 | columns = [" " * (width + 2) for width in column_widths] 123 | columns[0] = (" " + name).ljust(column_widths[0] + 2) 124 | print("|{}|".format("|".join(columns))) 125 | 126 | columns = [None] * len(column_widths) 127 | columns[0] = " encode".ljust(column_widths[0] + 2) 128 | for i, library in enumerate(libraries): 129 | if library in encodes: 130 | columns[i + 1] = f"{encodes[library]:,.0f} ".rjust( 131 | column_widths[i + 1] + 2 132 | ) 133 | else: 134 | columns[i + 1] = " " * (column_widths[i + 1] + 2) 135 | print("|{}|".format("|".join(columns))) 136 | 137 | if decodes: 138 | columns = [None] * len(column_widths) 139 | columns[0] = " decode".ljust(column_widths[0] + 2) 140 | for i, library in enumerate(libraries): 141 | if library in decodes: 142 | columns[i + 1] = f"{decodes[library]:,.0f} ".rjust( 143 | column_widths[i + 1] + 2 144 | ) 145 | else: 146 | columns[i + 1] = " " * (column_widths[i + 1] + 2) 147 | print("|{}|".format("|".join(columns))) 148 | 149 | print() 150 | print("Above metrics are in call/sec, larger is better.") 151 | 152 | 153 | # ============================================================================= 154 | # JSON encoding. 155 | # ============================================================================= 156 | 157 | _testname = "dumps" 158 | 159 | @register_benchmark("json", _testname) 160 | def dumps_with_json(): 161 | json.dumps(test_object) 162 | 163 | 164 | @register_benchmark("nujson", _testname) 165 | def dumps_with_nujson(): 166 | nujson.dumps(test_object) 167 | 168 | 169 | @register_benchmark("orjson", _testname) 170 | def dumps_with_orjson(): 171 | orjson.dumps(test_object) 172 | 173 | 174 | @register_benchmark("simplejson", _testname) 175 | def dumps_with_simplejson(): 176 | simplejson.dumps(test_object) 177 | 178 | @register_benchmark("ujson", _testname) 179 | def dumps_with_ujson(): 180 | ujson.dumps(test_object, ensure_ascii=False) 181 | 182 | @register_benchmark("pyyjson", _testname) 183 | def dumps_with_pyyjson(): 184 | def tmp(y): 185 | return y 186 | pyyjson.dumps(test_object, default=tmp) 187 | 188 | 189 | @register_benchmark("yyjson", _testname) 190 | def dumps_with_yyjson(): 191 | yyjson.Document(test_object).dumps() 192 | 193 | # ============================================================================= 194 | # JSON decoding. 195 | # ============================================================================= 196 | 197 | _testname = "loads" 198 | 199 | @register_benchmark("json", _testname) 200 | def loads_with_json(): 201 | json.loads(decode_data) 202 | 203 | 204 | @register_benchmark("nujson", _testname) 205 | def loads_with_nujson(): 206 | nujson.loads(decode_data) 207 | 208 | 209 | @register_benchmark("orjson", _testname) 210 | def loads_with_orjson(): 211 | orjson.loads(decode_data) 212 | 213 | 214 | @register_benchmark("simplejson", _testname) 215 | def loads_with_simplejson(): 216 | simplejson.loads(decode_data) 217 | 218 | 219 | @register_benchmark("ujson", _testname) 220 | def loads_with_ujson(): 221 | ujson.loads(decode_data) 222 | 223 | @register_benchmark("pyyjson", _testname) 224 | def loads_with_pyyjson(): 225 | pyyjson.loads(decode_data) 226 | 227 | @register_benchmark("yyjson", _testname) 228 | def loads_with_yyjson(): 229 | yyjson.Document(decode_data).as_obj 230 | 231 | # ============================================================================= 232 | # Benchmarks. 233 | # ============================================================================= 234 | def run_decode(count, libraries): 235 | _testname = "loads" 236 | for libname in libraries: 237 | func = benchmark_registry[_testname][libname] 238 | results_record_result(func, False, count) 239 | 240 | 241 | def run_encode(count, libraries): 242 | _testname = "dumps" 243 | for libname in libraries: 244 | func = benchmark_registry[_testname][libname] 245 | results_record_result(func, True, count) 246 | 247 | 248 | def benchmark_array_doubles(libraries, factor=1): 249 | global decode_data, test_object 250 | results_new_benchmark("Array with 256 doubles") 251 | COUNT = max(int(10000 * factor), 1) 252 | 253 | test_object = [] 254 | for x in range(256): 255 | test_object.append(sys.maxsize * random.random()) 256 | run_encode(COUNT, libraries) 257 | 258 | decode_data = json.dumps(test_object) 259 | test_object = None 260 | run_decode(COUNT, libraries) 261 | 262 | decode_data = None 263 | 264 | 265 | def benchmark_array_utf8_strings(libraries, factor=1): 266 | global decode_data, test_object 267 | results_new_benchmark("Array with 256 UTF-8 strings") 268 | COUNT = max(int(2000 * factor), 1) 269 | 270 | test_object = [] 271 | for x in range(256): 272 | test_object.append( 273 | "نظام الحكم سلطاني وراثي " 274 | "في الذكور من ذرية السيد تركي بن سعيد بن سلطان ويشترط فيمن يختار لولاية" 275 | " الحكم من بينهم ان يكون مسلما رشيدا عاقلا ًوابنا شرعيا لابوين عمانيين " 276 | ) 277 | run_encode(COUNT, libraries) 278 | 279 | decode_data = json.dumps(test_object) 280 | test_object = None 281 | run_decode(COUNT, libraries) 282 | 283 | decode_data = None 284 | 285 | 286 | def benchmark_array_byte_strings(libraries, factor=1): 287 | global decode_data, test_object 288 | results_new_benchmark("Array with 256 strings") 289 | COUNT = max(int(10000 * factor), 1) 290 | 291 | test_object = [] 292 | for x in range(256): 293 | test_object.append("A pretty long string which is in a list") 294 | run_encode(COUNT, libraries) 295 | 296 | decode_data = json.dumps(test_object) 297 | test_object = None 298 | run_decode(COUNT, libraries) 299 | 300 | decode_data = None 301 | 302 | 303 | def benchmark_medium_complex_object(libraries, factor=1): 304 | global decode_data, test_object 305 | results_new_benchmark("Medium complex object") 306 | COUNT = max(int(5000 * factor), 1) 307 | 308 | test_object = [ 309 | [USER, FRIENDS], 310 | [USER, FRIENDS], 311 | [USER, FRIENDS], 312 | [USER, FRIENDS], 313 | [USER, FRIENDS], 314 | [USER, FRIENDS], 315 | ] 316 | run_encode(COUNT, libraries) 317 | 318 | decode_data = json.dumps(test_object) 319 | test_object = None 320 | run_decode(COUNT, libraries) 321 | 322 | decode_data = None 323 | 324 | 325 | def benchmark_array_true_values(libraries, factor=1): 326 | global decode_data, test_object 327 | results_new_benchmark("Array with 256 True values") 328 | COUNT = max(int(50000 * factor), 1) 329 | 330 | test_object = [] 331 | for x in range(256): 332 | test_object.append(True) 333 | run_encode(COUNT, libraries) 334 | 335 | decode_data = json.dumps(test_object) 336 | test_object = None 337 | run_decode(COUNT, libraries) 338 | 339 | decode_data = None 340 | 341 | 342 | def benchmark_array_of_dict_string_int_pairs(libraries, factor=1): 343 | global decode_data, test_object 344 | results_new_benchmark("Array with 256 dict{string, int} pairs") 345 | COUNT = max(int(5000 * factor), 1) 346 | 347 | test_object = [] 348 | for x in range(256): 349 | test_object.append({str(random.random() * 20): int(random.random() * 1000000)}) 350 | run_encode(COUNT, libraries) 351 | 352 | decode_data = json.dumps(test_object) 353 | test_object = None 354 | run_decode(COUNT, libraries) 355 | 356 | decode_data = None 357 | 358 | def benchmark_complex_object(libraries, factor=1): 359 | global decode_data, test_object 360 | results_new_benchmark("Complex object") 361 | COUNT = int(100 * factor) 362 | 363 | with open(os.path.join(os.path.dirname(__file__), "sample.json")) as f: 364 | test_object = json.load(f) 365 | run_encode(COUNT, libraries) 366 | 367 | decode_data = json.dumps(test_object) 368 | test_object = None 369 | run_decode(COUNT, libraries) 370 | 371 | decode_data = None 372 | 373 | 374 | # ============================================================================= 375 | # Main. 376 | # ============================================================================= 377 | 378 | 379 | def main(): 380 | import argparse 381 | import importlib 382 | 383 | parser = argparse.ArgumentParser( 384 | prog="ujson-benchmarks", 385 | description="Benchmark ujson against other json implementations", 386 | ) 387 | 388 | known_libraries = [ 389 | "yyjson", 390 | "pyyjson", 391 | "ujson", 392 | "nujson", 393 | "orjson", 394 | "simplejson", 395 | "json", 396 | ] 397 | 398 | parser.add_argument( 399 | "--disable", 400 | nargs="+", 401 | choices=known_libraries, 402 | help="Remove specified libraries from the benchmarks", 403 | default=[], 404 | ) 405 | 406 | parser.add_argument( 407 | "--factor", 408 | type=float, 409 | default=1.0, 410 | help="Specify as a fraction speed up benchmarks for development / testing", 411 | ) 412 | 413 | args = parser.parse_args() 414 | 415 | disabled_libraries = ["simplejson", "nujson", "orjson", "json",] 416 | enabled_libraries = {} 417 | for libname in known_libraries: 418 | if libname not in disabled_libraries: 419 | try: 420 | module = importlib.import_module(libname) 421 | except ImportError: 422 | raise ImportError(f"{libname} is not available") 423 | else: 424 | enabled_libraries[libname] = module 425 | # Ensure the modules are available in the global scope 426 | for libname, module in enabled_libraries.items(): 427 | print(f"Enabled {libname} benchmarks") 428 | globals()[libname] = module 429 | 430 | libraries = list(enabled_libraries.keys()) 431 | factor = args.factor 432 | benchmark_array_doubles(libraries, factor) 433 | benchmark_array_utf8_strings(libraries, factor) 434 | benchmark_array_byte_strings(libraries, factor) 435 | benchmark_medium_complex_object(libraries, factor) 436 | benchmark_array_true_values(libraries, factor) 437 | benchmark_array_of_dict_string_int_pairs(libraries, factor) 438 | benchmark_complex_object(libraries, factor) 439 | 440 | results_output_table(libraries) 441 | 442 | 443 | if __name__ == "__main__": 444 | main() 445 | --------------------------------------------------------------------------------