Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- uses: actions/checkout@v4.1.7

- name: Setting up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: 3.13

Expand Down Expand Up @@ -54,12 +54,12 @@ jobs:
fail-fast: true
matrix:
os: [ubuntu-latest, windows-latest, macos-14]
py: ["cp39", "cp310", "cp311", "cp312", "cp313", "pp39", "pp310"]
py: ["cp39", "cp310", "cp311", "cp312", "cp313", "cp314", "pp39", "pp310"]

steps:
- uses: actions/checkout@v4.1.7

- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
name: Setting up Python
with:
python-version: '3.13'
Expand All @@ -71,7 +71,7 @@ jobs:
platforms: all

- name: Build & test wheels
uses: pypa/cibuildwheel@v2.22.0
uses: pypa/cibuildwheel@v3.2.1
env:
CIBW_ARCHS_LINUX: auto aarch64 ppc64le s390x
CIBW_ARCHS_MACOS: x86_64 arm64 universal2
Expand Down Expand Up @@ -108,7 +108,7 @@ jobs:
- uses: actions/checkout@v3

- name: Setting up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: 3.13

Expand Down
11 changes: 10 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "yyjson"
version = "4.0.6"
version = "4.1.0"
description = "JSON parser & serializer built on yyjson"
readme = "README.md"
authors = [
Expand All @@ -22,11 +22,13 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Operating System :: OS Independent",
"Topic :: Software Development :: Libraries :: Python Modules",
]
requires-python = ">=3.9"

[project.urls]
Homepage = "https://github.com/tktech/py_yyjson"
Expand All @@ -49,3 +51,10 @@ ext-modules = [
]
packages = ["yyjson"]

[tool.cibuildwheel]
enable = ["pypy"]

[tool.uv]
cache-keys = [
{ file = "yyjson/*.c" }
]
68 changes: 68 additions & 0 deletions tests/test_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,30 @@ def test_document_boolean_type():
assert doc.dumps() == "[false]"
assert doc.as_obj == [False]

def test_document_list_type():
doc = Document('[1,2,3,4]')
assert doc.dumps() == '[1,2,3,4]'
assert doc.as_obj == [1, 2, 3, 4]

doc = Document([1, 2, 3, 4])
assert doc.dumps() == '[1,2,3,4]'
assert doc.as_obj == [1, 2, 3, 4]

def test_document_tuple_type():
doc = Document(())
assert doc.dumps() == '[]'

doc = Document((1,))
assert doc.dumps() == '[1]'

doc = Document((1, 2, 3, 4))
assert doc.dumps() == '[1,2,3,4]'

doc = Document([(1, 2), (3, 4)])
assert doc.dumps() == '[[1,2],[3,4]]'

doc = Document({'test': (1, 2)})
assert doc.dumps() == '{"test":[1,2]}'

def test_document_none_type():
"""
Expand All @@ -166,6 +190,27 @@ def test_document_none_type():
assert doc.as_obj == [None]


def test_document_dict_type():
"""
Ensure we can load and dump the dict type.
"""
doc = Document('{"a": "b"}')
assert doc.dumps() == '{"a":"b"}'
assert doc.as_obj == {'a': 'b'}

doc = Document({"a": "b"})
assert doc.dumps() == '{"a":"b"}'
assert doc.as_obj == {'a': 'b'}

with pytest.raises(TypeError) as exc:
Document({1: 'b'})
assert exc.value.args[0] == 'Dictionary keys must be strings'

with pytest.raises(TypeError) as exc:
Document({'\ud83d\ude47': 'foo'})
assert exc.value.args[0] == 'Dictionary keys must be strings'


def test_document_get_pointer():
"""
Ensure JSON pointers work.
Expand Down Expand Up @@ -240,3 +285,26 @@ def test_document_freeze():

doc.freeze()
assert doc.is_thawed is False


def test_document_size():
"""
Test the size attribute that returns the size of data read from original JSON input.
"""
# Test with immutable document (created from JSON string)
json_str = '{"hello": "world", "number": 42}'
doc = Document(json_str)
assert doc.bytes_read == len(json_str)

# Test with different sized JSON inputs
small_json = "{}"
doc_small = Document(small_json)
assert doc_small.bytes_read == len(small_json)

large_json = '{"users": [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}], "count": 2}'
doc_large = Document(large_json)
assert doc_large.bytes_read == len(large_json)

# Test with mutable document (created from Python object) - should return 0
doc_mutable = Document({"hello": "world"})
assert doc_mutable.bytes_read == 0
2 changes: 2 additions & 0 deletions yyjson/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ class Document:
) -> "Document": ...
@property
def is_thawed(self) -> bool: ...
@property
def bytes_read(self) -> int: ...
def freeze(self) -> None: ...
def thaw(self) -> None: ...

Expand Down
36 changes: 35 additions & 1 deletion yyjson/document.c
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,8 @@ PyTypeObject *type_for_conversion(PyObject *obj) {
return &PyDict_Type;
} else if (obj->ob_type == &PyList_Type) {
return &PyList_Type;
} else if (obj->ob_type == &PyTuple_Type) {
return &PyTuple_Type;
} else if (obj->ob_type == &PyBool_Type) {
return &PyBool_Type;
} else if (obj->ob_type == Py_None->ob_type) {
Expand Down Expand Up @@ -355,6 +357,19 @@ static inline yyjson_mut_val *mut_primitive_to_element(
yyjson_mut_arr_append(val, object_value);
}
return val;
} else if (ob_type == &PyTuple_Type) {
yyjson_mut_val *val = yyjson_mut_arr(doc);
yyjson_mut_val *object_value = NULL;
for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(obj); i++) {
object_value = mut_primitive_to_element(self, doc, PyTuple_GET_ITEM(obj, i));

if (yyjson_unlikely(object_value == NULL)) {
return NULL;
}

yyjson_mut_arr_append(val, object_value);
}
return val;
} else if (ob_type == &PyDict_Type) {
yyjson_mut_val *val = yyjson_mut_obj(doc);
yyjson_mut_val *object_value = NULL;
Expand All @@ -364,6 +379,13 @@ static inline yyjson_mut_val *mut_primitive_to_element(
while (PyDict_Next(obj, &i, &key, &value)) {
Py_ssize_t str_len;
const char *str = PyUnicode_AsUTF8AndSize(key, &str_len);
if (yyjson_unlikely(str == NULL)) {
PyErr_Format(PyExc_TypeError,
"Dictionary keys must be strings",
Py_TYPE(obj)->tp_name
);
return NULL;
}
object_value = mut_primitive_to_element(self, doc, value);
if (yyjson_unlikely(object_value == NULL)) {
return NULL;
Expand Down Expand Up @@ -549,7 +571,6 @@ static int Document_init(DocumentObject *self, PyObject *args, PyObject *kwds) {
self->i_doc = yyjson_read_file(str, r_flag, self->alc, &err);

Py_XDECREF(as_str);
Py_XDECREF(str);

if (!self->i_doc) {
PyErr_SetString(PyExc_ValueError, err.msg);
Expand Down Expand Up @@ -597,6 +618,17 @@ static PyObject *Document_is_thawed(DocumentObject *self, void *closure) {
return PyBool_FromLong(self->m_doc != NULL);
}

/**
* Get the size of data read from the original JSON input.
*/
static PyObject *Document_bytes_read(DocumentObject *self, void *closure) {
if (self->i_doc) {
return PyLong_FromSize_t(yyjson_doc_get_read_size(self->i_doc));
} else {
return PyLong_FromLong(0);
}
}

PyDoc_STRVAR(
Document_dumps_doc,
"Dumps the document to a string and returns it.\n"
Expand Down Expand Up @@ -1009,6 +1041,8 @@ static PyGetSetDef Document_members[] = {
NULL},
{"is_thawed", (getter)Document_is_thawed, NULL,
"Returns whether the Document is thawed/mutable.", NULL},
{"bytes_read", (getter)Document_bytes_read, NULL,
"Returns the size of data read from the original JSON input.", NULL},
{NULL} /* Sentinel */
};

Expand Down
Loading