my.core.serialize: simplejson support, more types (#176)

* my.core.serialize: simplejson support, more types

I added a couple extra checks to the default function,
serializing datetime, dates and dataclasses (incase
orjson isn't installed)

(copied from below)

if orjson couldn't be imported, try simplejson
This is included for compatibility reasons because orjson
is rust-based and compiling on rarer architectures may not work
out of the box

as an example, I've been having issues getting it to install
on my phone (termux/android)

unlike the builtin JSON modue which serializes NamedTuples as lists
(even if you provide a default function), simplejson correctly
serializes namedtuples to dictionaries

this just gives another option to people, simplejson is pure python
so no one should have issues with that. orjson is still way faster,
so still preferable if its easy and theres a precompiled build
for your architecture (which there typically is)

If you're ever running this with simplejson installed and not orjson,
its pretty easy to tell as the JSON styling is different; orjson has
no spaces between tokens, simplejson puts spaces between tokens. e.g.

simplejson: {"a": 5, "b": 10}
orjson: {"a":5,"b":10}
This commit is contained in:
Sean Breckenridge 2021-07-08 15:02:56 -07:00 committed by GitHub
parent 821bc08a23
commit 46198a6447
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 67 additions and 9 deletions

View file

@ -1,4 +1,5 @@
import datetime import datetime
import dataclasses
from pathlib import Path from pathlib import Path
from typing import Any, Optional, Callable, NamedTuple from typing import Any, Optional, Callable, NamedTuple
from functools import lru_cache from functools import lru_cache
@ -26,9 +27,13 @@ def _default_encode(obj: Any) -> Any:
return obj._asdict() return obj._asdict()
if isinstance(obj, datetime.timedelta): if isinstance(obj, datetime.timedelta):
return obj.total_seconds() return obj.total_seconds()
if isinstance(obj, datetime.datetime) or isinstance(obj, datetime.date):
return str(obj)
# convert paths to their string representation # convert paths to their string representation
if isinstance(obj, Path): if isinstance(obj, Path):
return str(obj) return str(obj)
if dataclasses.is_dataclass(obj):
return dataclasses.asdict(obj)
if isinstance(obj, Exception): if isinstance(obj, Exception):
return error_to_json(obj) return error_to_json(obj)
# note: _serialize would only be called for items which aren't already # note: _serialize would only be called for items which aren't already
@ -76,15 +81,36 @@ def _dumps_factory(**kwargs) -> Callable[[Any], str]:
return _orjson_dumps return _orjson_dumps
except ModuleNotFoundError: except ModuleNotFoundError:
import json pass
from .warnings import high
high("You might want to install 'orjson' to support serialization for lots more types!") try:
from simplejson import dumps as simplejson_dumps
# if orjson couldn't be imported, try simplejson
# This is included for compatibility reasons because orjson
# is rust-based and compiling on rarer architectures may not work
# out of the box
#
# unlike the builtin JSON modue which serializes NamedTuples as lists
# (even if you provide a default function), simplejson correctly
# serializes namedtuples to dictionaries
def _stdlib_dumps(obj: Any) -> str: def _simplejson_dumps(obj: Any) -> str:
return json.dumps(obj, **kwargs) return simplejson_dumps(obj, namedtuple_as_object=True, **kwargs)
return _stdlib_dumps return _simplejson_dumps
except ModuleNotFoundError:
pass
import json
from .warnings import high
high("You might want to install 'orjson' to support serialization for lots more types! If that does not work for you, you can install 'simplejson' instead")
def _stdlib_dumps(obj: Any) -> str:
return json.dumps(obj, **kwargs)
return _stdlib_dumps
def dumps( def dumps(
@ -93,8 +119,8 @@ def dumps(
**kwargs, **kwargs,
) -> str: ) -> str:
""" """
Any additional arguments are forwarded -- either to orjson.dumps Any additional arguments are forwarded -- either to orjson.dumps,
or json.dumps if orjson is not installed simplejson.dumps or json.dumps if orjson is not installed
You can pass the 'option' kwarg to orjson, see here for possible options: You can pass the 'option' kwarg to orjson, see here for possible options:
https://github.com/ijl/orjson#option https://github.com/ijl/orjson#option

View file

@ -0,0 +1,23 @@
'''
This file should only run when simplejson is installed,
but orjson is not installed to check compatability
'''
# none of these should fail
import json
import simplejson
import pytest
from my.core.serialize import dumps, _A
def test_simplejson_fallback() -> None:
# should fail to import
with pytest.raises(ModuleNotFoundError):
import orjson
# simplejson should serialize namedtuple properly
res: str = dumps(_A(x=1, y=2.0))
assert json.loads(res) == {"x": 1, "y": 2.0}

11
tox.ini
View file

@ -23,6 +23,13 @@ commands =
setenv = MY_CONFIG = nonexistent setenv = MY_CONFIG = nonexistent
commands = commands =
pip install -e .[testing] pip install -e .[testing]
# installed to test my.core.serialize while using simplejson and not orjson
pip install simplejson
python3 -m pytest \
tests/serialize_simplejson.py \
{posargs}
pip install cachew pip install cachew
pip install orjson pip install orjson
@ -43,7 +50,9 @@ commands =
python3 -m pytest tests \ python3 -m pytest tests \
# ignore some tests which might take a while to run on ci.. # ignore some tests which might take a while to run on ci..
--ignore tests/takeout.py \ --ignore tests/takeout.py \
--ignore tests/extra/polar.py --ignore tests/extra/polar.py \
# dont run simplejson compatability test since orjson is now installed
--ignore tests/serialize_simplejson.py
{posargs} {posargs}