Skip to content
Merged
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ env:
# Note: TWINE_PASSWORD is set in Travis settings

install:
- if [ $TRAVIS_OS_NAME = osx ]; then brew install python3; fi
- if [ $TRAVIS_OS_NAME = osx ]; then brew upgrade python; fi
- pip3 install -r requirements-test.txt

script:
- pip3 install . && pytest tests
- pip3 install . && pytest tests && cd docs; make doctest; cd -
- |
pip3 install cibuildwheel==0.6.0
cibuildwheel --output-dir wheelhouse
Expand Down
1 change: 1 addition & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
encoder
decoder
validator
rawjson

.. data:: __author__

Expand Down
44 changes: 44 additions & 0 deletions docs/rawjson.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
.. -*- coding: utf-8 -*-
.. :Project: python-rapidjson -- RawJSON class documentation
.. :Author: Silvio Tomatis <silviot@gmail.com>
.. :License: MIT License
.. :Copyright: © 2018 Silvio Tomatis
.. :Copyright: © 2018 Lele Gaifax
..

===============
RawJSON class
===============

.. module:: rapidjson

.. testsetup::

from rapidjson import RawJSON, dumps

.. class:: RawJSON(value)

Preserialized JSON string.

:param str value: The string that rapidjson should use verbatim when serializing this object

Some applications might decide to store JSON-serialized objects in their database,
but might need to assemble them in a bigger JSON.

The RawJSON class serves this purpose. When serialized, the string value provided will be used verbatim,
whitespace included.

.. doctest::

>>> raw_list = RawJSON('[1, 2,3]')
>>> dumps({'foo': raw_list})
'{"foo":[1, 2,3]}'

Rapidjson runs no checks on the preserialized data. This means that it can
potentially output invalid JSON, if you provide it:

.. doctest::

>>> raw_list = RawJSON('[1, ')
>>> dumps({'foo': raw_list})
'{"foo":[1, }'
116 changes: 116 additions & 0 deletions rapidjson.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,107 @@ inline void PutUnsafe(PyWriteStreamWrapper& stream, char c) {
}


/////////////
// RawJSON //
/////////////


typedef struct {
PyObject_HEAD
PyObject *value;
} RawJSON;

static void
RawJSON_dealloc(RawJSON* self)
{
Py_XDECREF(self->value);
Py_TYPE(self)->tp_free((PyObject*)self);
}


static int
RawJSON_init(RawJSON *self, PyObject *args, PyObject *kwds)
{
PyObject *value=NULL, *tmp;

static char *kwlist[] = {"value", NULL};

if (! PyArg_ParseTupleAndKeywords(args, kwds, "U", kwlist,
&value))
return -1;

if (!PyUnicode_Check(value)) {
PyErr_SetString(PyExc_TypeError, "Only unicode strings allowed");
return -1;
}

if (value) {
tmp = self->value;
Py_INCREF(value);
self->value = value;
Py_XDECREF(tmp);
}

return 0;
}

static PyMemberDef RawJSON_members[] = {
{"value", T_OBJECT_EX, offsetof(RawJSON, value), 0,
"Byte string representing a serialized JSON object"},
{NULL} /* Sentinel */
};


PyDoc_STRVAR(rawjson_doc,
"Raw (preserialized) JSON objects\n"
"\n"
"When rapidjson tries to serialize objects of this class, it will"
" use their literal `value`. For instance:\n"
">>> rapidjson.dumps(RawJSON('{\"already\": \"serialized\"}'))\n"
"'{\"already\": \"serialized\"}'");

static PyTypeObject RawJSON_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
"rapidjson.RawJSON", /* tp_name */
sizeof(RawJSON), /* tp_basicsize */
0, /* tp_itemsize */
(destructor)RawJSON_dealloc, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_compare */
0, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
0, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT, /* tp_flags */
rawjson_doc, /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
0, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
0, /* tp_methods */
RawJSON_members, /* tp_members */
0, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
(initproc)RawJSON_init, /* tp_init */
0, /* tp_alloc */
PyType_GenericNew, /* tp_new */
};


/////////////
// Decoder //
/////////////
Expand Down Expand Up @@ -2623,6 +2724,15 @@ dumps_internal(

writer->EndArray();
}
else if PyObject_TypeCheck(object, &RawJSON_Type) {
const char* jsonStr;
Py_ssize_t l;
jsonStr = PyUnicode_AsUTF8AndSize(((RawJSON*)object)->value, &l);
if (jsonStr == NULL)
return false;
ASSERT_VALID_SIZE(l);
writer->RawValue(jsonStr, (SizeType) l, kStringType);
}
else if (defaultFn) {
PyObject* retval = PyObject_CallFunctionObjArgs(defaultFn, object, NULL);
if (retval == NULL)
Expand Down Expand Up @@ -3686,6 +3796,12 @@ PyInit_rapidjson()
Py_INCREF(&Validator_Type);
PyModule_AddObject(m, "Validator", (PyObject*) &Validator_Type);

RawJSON_Type.tp_new = PyType_GenericNew;
if (PyType_Ready(&RawJSON_Type) < 0)
return NULL;
Py_INCREF(&RawJSON_Type);
PyModule_AddObject(m, "RawJSON", (PyObject *)&RawJSON_Type);

return m;

error:
Expand Down
39 changes: 39 additions & 0 deletions tests/test_rawjson.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# :Project: python-rapidjson -- Tests on RawJSON
# :Author: Silvio Tomatis <silviot@gmail.com>
# :License: MIT License
# :Copyright: © 2018 Silvio Tomatis
# :Copyright: © 2018 Lele Gaifax
#

import pytest

import rapidjson as rj


@pytest.mark.unit
def test_instantiation_positional():
value = 'a string'
raw = rj.RawJSON(value)
assert raw.value == value


@pytest.mark.unit
def test_instantiation_named():
value = 'a string'
raw = rj.RawJSON(value=value)
assert raw.value == value


@pytest.mark.unit
def test_only_bytes_allowed():
with pytest.raises(TypeError):
rj.RawJSON(b'A bytes string')
with pytest.raises(TypeError):
rj.RawJSON({})
with pytest.raises(TypeError):
rj.RawJSON(None)


def test_mix_preserialized():
assert rj.dumps({'a': rj.RawJSON('{1 : 2}')}) == '{"a":{1 : 2}}'
11 changes: 8 additions & 3 deletions tests/test_refs_count.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ def default(obj):
( plain_string, NO_OPTION, NO_OPTION ),
( bigint, NO_OPTION, NO_OPTION ),
( pi, NO_OPTION, NO_OPTION ),
( rj.RawJSON(' "foo" '), NO_OPTION, NO_OPTION),
( right_now, DATETIMES, DATETIMES ),
( date, DATETIMES, DATETIMES ),
( time, DATETIMES, DATETIMES ),
Expand Down Expand Up @@ -138,16 +139,20 @@ def string(self, string):
assert (rc1 - rc0) < THRESHOLD


@pytest.mark.parametrize('value', ['Foo', rj.RawJSON('Foo')])
@pytest.mark.skipif(not hasattr(sys, 'gettotalrefcount'), reason='Non-debug Python')
def test_encoder_call_leaks():
def test_encoder_call_leaks(value):
class MyEncoder(rj.Encoder):
def __init__(self, value):
self.value = value

def default(self, obj):
return 'Foo'
return self.value

class Foo:
pass

encoder = MyEncoder()
encoder = MyEncoder(value)
foo = Foo()
rc0 = sys.gettotalrefcount()
for i in range(1000):
Expand Down