Skip to content

Commit

Permalink
Merge pull request #40 from lelit/uuid-roundtrip-v2
Browse files Browse the repository at this point in the history
New option uuid_mode, to dump/load UUIDs
  • Loading branch information
kenrobbins committed Aug 28, 2016
2 parents 747b8cd + dbb57ed commit d80784c
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 18 deletions.
6 changes: 3 additions & 3 deletions python-rapidjson/docstrings.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ PyDoc_STRVAR(rapidjson_module_docstring,

PyDoc_STRVAR(rapidjson_loads_docstring,
"loads(s, object_hook=None, use_decimal=False, precise_float=True,"
" allow_nan=True, datetime_mode=None)\n"
" allow_nan=True, datetime_mode=None, uuid_mode=None)\n"
"\n"
"Decodes a JSON string into Python object.\n");
"Decodes a JSON string into Python object.");

PyDoc_STRVAR(rapidjson_dumps_docstring,
"dumps(obj, skipkeys=False, ensure_ascii=True, allow_nan=True, indent=None,"
" default=None, sort_keys=False, use_decimal=False, max_recursion_depth=2048,"
" datetime_mode=None)\n"
" datetime_mode=None, uuid_mode=None)\n"
"\n"
"Encodes Python object into a JSON string.");

Expand Down
150 changes: 135 additions & 15 deletions python-rapidjson/rapidjson.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ using namespace rapidjson;
static PyObject* rapidjson_decimal_type = NULL;
static PyObject* rapidjson_timezone_type = NULL;
static PyObject* rapidjson_timezone_utc = NULL;
static PyObject* rapidjson_uuid_type = NULL;

struct HandlerContext {
PyObject* object;
Expand Down Expand Up @@ -49,16 +50,24 @@ days_per_month(int year, int month) {
return 28;
}

enum UuidMode {
UUID_MODE_NONE = 0,
UUID_MODE_CANONICAL = 1, // only 4-dashed 32 hex chars: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
UUID_MODE_HEX = 2 // canonical OR 32 hex chars
};

struct PyHandler {
int useDecimal;
int allowNan;
PyObject* root;
PyObject* objectHook;
DatetimeMode datetimeMode;
UuidMode uuidMode;
std::vector<HandlerContext> stack;

PyHandler(int ud, PyObject* hook, int an, DatetimeMode dm)
: useDecimal(ud), allowNan(an), root(NULL), objectHook(hook), datetimeMode(dm)
PyHandler(int ud, PyObject* hook, int an, DatetimeMode dm, UuidMode um)
: useDecimal(ud), allowNan(an), root(NULL), objectHook(hook), datetimeMode(dm),
uuidMode(um)
{
stack.reserve(128);
}
Expand Down Expand Up @@ -652,13 +661,48 @@ struct PyHandler {

#undef digit

bool IsUuid(const char* str, SizeType length) {
if (uuidMode == UUID_MODE_HEX && length == 32) {
for (int i = length - 1; i >= 0; --i)
if (!isxdigit(str[i]))
return false;
return true;
} else if (length == 36
&& str[8] == '-' && str[13] == '-'
&& str[18] == '-' && str[23] == '-') {
for (int i = length - 1; i >= 0; --i)
if (i != 8 && i != 13 && i != 18 && i != 23 && !isxdigit(str[i]))
return false;
return true;
}
return false;
}

bool HandleUuid(const char* str, SizeType length) {
PyObject* pystr = PyUnicode_FromStringAndSize(str, length);
if (pystr == NULL)
return false;

PyObject* value = PyObject_CallFunctionObjArgs(rapidjson_uuid_type, pystr, NULL);
Py_DECREF(pystr);

if (value == NULL)
return false;
else
return HandleSimpleType(value);
}

bool String(const char* str, SizeType length, bool copy) {
PyObject* value;

if (datetimeMode != DATETIME_MODE_NONE && IsIso8601(str, length))
return HandleIso8601(str, length);
else {
PyObject* value = PyUnicode_FromStringAndSize(str, length);
return HandleSimpleType(value);
}

if (uuidMode != UUID_MODE_NONE && IsUuid(str, length))
return HandleUuid(str, length);

value = PyUnicode_FromStringAndSize(str, length);
return HandleSimpleType(value);
}
};

Expand All @@ -674,6 +718,8 @@ rapidjson_loads(PyObject* self, PyObject* args, PyObject* kwargs)
int allowNan = 1;
PyObject* datetimeModeObj = NULL;
DatetimeMode datetimeMode = DATETIME_MODE_NONE;
PyObject* uuidModeObj = NULL;
UuidMode uuidMode = UUID_MODE_NONE;

static char* kwlist[] = {
"s",
Expand All @@ -682,17 +728,19 @@ rapidjson_loads(PyObject* self, PyObject* args, PyObject* kwargs)
"precise_float",
"allow_nan",
"datetime_mode",
"uuid_mode",
NULL
};

if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|OpppO:rapidjson.loads",
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|OpppOO:rapidjson.loads",
kwlist,
&jsonObject,
&objectHook,
&useDecimal,
&preciseFloat,
&allowNan,
&datetimeModeObj))
&datetimeModeObj,
&uuidModeObj))

if (objectHook && !PyCallable_Check(objectHook)) {
PyErr_SetString(PyExc_TypeError, "object_hook is not callable");
Expand Down Expand Up @@ -725,10 +773,18 @@ rapidjson_loads(PyObject* self, PyObject* args, PyObject* kwargs)
}
}

if (uuidModeObj && PyLong_Check(uuidModeObj)) {
uuidMode = (UuidMode) PyLong_AsLong(uuidModeObj);
if (uuidMode < UUID_MODE_NONE || uuidMode > UUID_MODE_HEX) {
PyErr_SetString(PyExc_ValueError, "Invalid uuid_time");
return NULL;
}
}

char* jsonStrCopy = (char*) malloc(sizeof(char) * (jsonStrLen+1));
memcpy(jsonStrCopy, jsonStr, jsonStrLen+1);

PyHandler handler(useDecimal, objectHook, allowNan, datetimeMode);
PyHandler handler(useDecimal, objectHook, allowNan, datetimeMode, uuidMode);
Reader reader;
InsituStringStream ss(jsonStrCopy);

Expand Down Expand Up @@ -807,7 +863,8 @@ rapidjson_dumps_internal(
int sortKeys,
int useDecimal,
unsigned maxRecursionDepth,
DatetimeMode datetimeMode)
DatetimeMode datetimeMode,
UuidMode uuidMode)
{
int isDec;
std::vector<WriterContext> stack;
Expand Down Expand Up @@ -1105,6 +1162,20 @@ rapidjson_dumps_internal(
snprintf(isoformat, ISOFORMAT_LEN-1, "%04d-%02d-%02d", year, month, day);
writer->String(isoformat);
}
else if (uuidMode != UUID_MODE_NONE
&& PyObject_TypeCheck(object, (PyTypeObject *) rapidjson_uuid_type)) {
PyObject* retval;
if (uuidMode == UUID_MODE_CANONICAL)
retval = PyObject_Str(object);
else
retval = PyObject_GetAttrString(object, "hex");
if (retval == NULL)
goto error;

// Decref the return value once it's done being dumped to a string.
stack.push_back(WriterContext(NULL, NULL, false, currentLevel, retval));
stack.push_back(WriterContext(NULL, retval, false, currentLevel));
}
else if (defaultFn) {
PyObject* retval = PyObject_CallFunctionObjArgs(defaultFn, object, NULL);
if (retval == NULL)
Expand Down Expand Up @@ -1140,7 +1211,8 @@ rapidjson_dumps_internal(
sortKeys, \
useDecimal, \
maxRecursionDepth, \
datetimeMode)
datetimeMode, \
uuidMode)


static PyObject*
Expand All @@ -1159,6 +1231,8 @@ rapidjson_dumps(PyObject* self, PyObject* args, PyObject* kwargs)
unsigned maxRecursionDepth = MAX_RECURSION_DEPTH;
PyObject* datetimeModeObj = NULL;
DatetimeMode datetimeMode = DATETIME_MODE_NONE;
PyObject* uuidModeObj = NULL;
UuidMode uuidMode = UUID_MODE_NONE;

bool prettyPrint = false;
const char indentChar = ' ';
Expand All @@ -1175,9 +1249,10 @@ rapidjson_dumps(PyObject* self, PyObject* args, PyObject* kwargs)
"use_decimal",
"max_recursion_depth",
"datetime_mode",
"uuid_mode",
NULL
};
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|pppOOppIO:rapidjson.dumps",
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|pppOOppIOO:rapidjson.dumps",
kwlist,
&value,
&skipKeys,
Expand All @@ -1188,7 +1263,8 @@ rapidjson_dumps(PyObject* self, PyObject* args, PyObject* kwargs)
&sortKeys,
&useDecimal,
&maxRecursionDepth,
&datetimeModeObj))
&datetimeModeObj,
&uuidModeObj))
return NULL;

if (defaultFn && !PyCallable_Check(defaultFn)) {
Expand Down Expand Up @@ -1216,6 +1292,14 @@ rapidjson_dumps(PyObject* self, PyObject* args, PyObject* kwargs)
}
}

if (uuidModeObj && PyLong_Check(uuidModeObj)) {
uuidMode = (UuidMode) PyLong_AsLong(uuidModeObj);
if (uuidMode < UUID_MODE_NONE || uuidMode > UUID_MODE_HEX) {
PyErr_SetString(PyExc_ValueError, "Invalid uuid_time");
return NULL;
}
}

if (!prettyPrint) {
if (ensureAscii) {
GenericStringBuffer<ASCII<> > buf;
Expand Down Expand Up @@ -1280,24 +1364,60 @@ PyInit_rapidjson()
return NULL;
}

PyObject* uuidModule = PyImport_ImportModule("uuid");
if (uuidModule == NULL) {
Py_DECREF(rapidjson_timezone_type);
Py_DECREF(rapidjson_timezone_utc);
return NULL;
}

rapidjson_uuid_type = PyObject_GetAttrString(uuidModule, "UUID");
Py_DECREF(uuidModule);

if (rapidjson_uuid_type == NULL) {
Py_DECREF(rapidjson_timezone_type);
Py_DECREF(rapidjson_timezone_utc);
return NULL;
}

PyObject* decimalModule = PyImport_ImportModule("decimal");
if (decimalModule == NULL)
if (decimalModule == NULL) {
Py_DECREF(rapidjson_timezone_type);
Py_DECREF(rapidjson_timezone_utc);
Py_DECREF(rapidjson_uuid_type);
return NULL;
}

rapidjson_decimal_type = PyObject_GetAttrString(decimalModule, "Decimal");
Py_DECREF(decimalModule);

if (rapidjson_decimal_type == NULL) {
Py_DECREF(rapidjson_timezone_type);
Py_DECREF(rapidjson_timezone_utc);
Py_DECREF(rapidjson_uuid_type);
return NULL;
}

PyObject* module;

module = PyModule_Create(&rapidjson_module);
if (module == NULL)
if (module == NULL) {
Py_DECREF(rapidjson_timezone_type);
Py_DECREF(rapidjson_timezone_utc);
Py_DECREF(rapidjson_decimal_type);
Py_DECREF(rapidjson_uuid_type);
return NULL;
}

PyModule_AddIntConstant(module, "DATETIME_MODE_NONE", DATETIME_MODE_NONE);
PyModule_AddIntConstant(module, "DATETIME_MODE_ISO8601", DATETIME_MODE_ISO8601);
PyModule_AddIntConstant(module, "DATETIME_MODE_ISO8601_IGNORE_TZ", DATETIME_MODE_ISO8601_IGNORE_TZ);
PyModule_AddIntConstant(module, "DATETIME_MODE_ISO8601_UTC", DATETIME_MODE_ISO8601_UTC);

PyModule_AddIntConstant(module, "UUID_MODE_NONE", UUID_MODE_NONE);
PyModule_AddIntConstant(module, "UUID_MODE_HEX", UUID_MODE_HEX);
PyModule_AddIntConstant(module, "UUID_MODE_CANONICAL", UUID_MODE_CANONICAL);

PyModule_AddStringConstant(module, "__version__", PYTHON_RAPIDJSON_VERSION);
PyModule_AddStringConstant(
module,
Expand Down
70 changes: 70 additions & 0 deletions tests/test_params.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from datetime import date, datetime, time
import math
import uuid

import pytest
import rapidjson

Expand Down Expand Up @@ -249,6 +251,47 @@ def test_datetime_values(value):
assert loaded == value


@pytest.mark.unit
def test_uuid_mode():
assert rapidjson.UUID_MODE_NONE == 0
assert rapidjson.UUID_MODE_CANONICAL == 1
assert rapidjson.UUID_MODE_HEX == 2

value = uuid.uuid1()
with pytest.raises(TypeError):
rapidjson.dumps(value)

with pytest.raises(ValueError):
rapidjson.dumps(value, uuid_mode=42)

with pytest.raises(ValueError):
rapidjson.loads('""', uuid_mode=42)

dumped = rapidjson.dumps(value, uuid_mode=rapidjson.UUID_MODE_CANONICAL)
loaded = rapidjson.loads(dumped, uuid_mode=rapidjson.UUID_MODE_CANONICAL)
assert loaded == value

# When loading, hex mode implies canonical format
loaded = rapidjson.loads(dumped, uuid_mode=rapidjson.UUID_MODE_HEX)
assert loaded == value

dumped = rapidjson.dumps(value, uuid_mode=rapidjson.UUID_MODE_HEX)
loaded = rapidjson.loads(dumped, uuid_mode=rapidjson.UUID_MODE_HEX)
assert loaded == value


@pytest.mark.unit
def test_uuid_and_datetime_mode_together():
value = [date.today(), uuid.uuid1()]
dumped = rapidjson.dumps(value,
datetime_mode=rapidjson.DATETIME_MODE_ISO8601,
uuid_mode=rapidjson.UUID_MODE_CANONICAL)
loaded = rapidjson.loads(dumped,
datetime_mode=rapidjson.DATETIME_MODE_ISO8601,
uuid_mode=rapidjson.UUID_MODE_CANONICAL)
assert loaded == value


@pytest.mark.unit
@pytest.mark.parametrize(
'value,cls', [
Expand Down Expand Up @@ -298,6 +341,33 @@ def test_datetime_iso8601(value, cls):
assert isinstance(result, cls)


@pytest.mark.unit
@pytest.mark.parametrize(
'value,cls', [
('7a683da49aa011e5972e3085a99ccac7', str),
('7a683da4 9aa0-11e5-972e-3085a99ccac7', str),
('za683da4-9aa0-11e5-972e-3085a99ccac7', str),
('7a683da4-9aa0-11e5-972e-3085a99ccac7', uuid.UUID),
])
def test_uuid_canonical(value, cls):
result = rapidjson.loads('"%s"' % value, uuid_mode=rapidjson.UUID_MODE_CANONICAL)
assert isinstance(result, cls), type(result)


@pytest.mark.unit
@pytest.mark.parametrize(
'value,cls', [
('za683da49aa011e5972e3085a99ccac7', str),
('7a683da49aa011e5972e3085a99ccac7', uuid.UUID),
('7a683da4-9aa0-11e5-972e-3085a99ccac7', uuid.UUID),
])
def test_uuid_hex(value, cls):
result = rapidjson.loads('"%s"' % value, uuid_mode=rapidjson.UUID_MODE_HEX)
assert isinstance(result, cls), type(result)


@pytest.mark.unit
def test_precise_float():
f = "1.234567890E+34"
Expand Down

0 comments on commit d80784c

Please sign in to comment.