Skip to content

Commit

Permalink
Allow to use converters directly (#18)
Browse files Browse the repository at this point in the history
* add `from_converters`

* bump version to `0.2.1`
  • Loading branch information
PythonFZ authored Oct 4, 2022
1 parent a4691ce commit ec3e495
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 23 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,9 @@ and will result in
"value": "2022-03-11T09:47:35.280331"
}
````

If you don't want to register your converter to be used everywhere, simply use:

```python
json_string = json.dumps(dt, cls=znjson.ZnEncoder.from_converters(DatetimeConverter))
```
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ multi_line_output = 3

[tool.poetry]
name = "znjson"
version = "0.2.0"
version = "0.2.1"
description = "A Python Package to Encode/Decode some common file formats to json"
authors = ["zincwarecode <[email protected]>"]
license = "Apache-2.0"
Expand Down
55 changes: 54 additions & 1 deletion tests/test_main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import pathlib

import numpy as np
import pytest

import znjson
Expand Down Expand Up @@ -40,7 +41,8 @@ def test_decode_pathlib_wo_value():


def test_not_encodeable():
function = lambda x: x
def function():
...

with pytest.raises(TypeError):
json.dumps(function, cls=znjson.ZnEncoder)
Expand All @@ -51,3 +53,54 @@ def test_not_decodeable():

with pytest.raises(TypeError):
json.loads(data_str, cls=znjson.ZnDecoder)


@pytest.mark.parametrize("enable", (True, False))
def test_from_converter(enable):
data = np.arange(10)

if enable:
encoded_str = json.dumps(
data,
cls=znjson.ZnEncoder.from_converters([znjson.converter.NumpyConverterSmall]),
)
assert json.loads(encoded_str)["_type"] == "np.ndarray_small"
else:
with pytest.raises(TypeError):
# only can encode pathlib
_ = json.dumps(
data,
cls=znjson.ZnEncoder.from_converters([znjson.converter.PathlibConverter]),
)


def test_from_converter_multi():
"""Check that it does not mess with any global configurations"""
array = np.arange(10)
path = pathlib.Path.cwd()

with pytest.raises(TypeError):
_ = json.dumps({array, path}, cls=znjson.ZnEncoder.from_converters([]))

encoded_str = json.dumps(
array, cls=znjson.ZnEncoder.from_converters(znjson.converter.NumpyConverter)
)
assert json.loads(encoded_str)["_type"] == "np.ndarray_b64"

encoded_str = json.dumps(
[array, path],
cls=znjson.ZnEncoder.from_converters(
[znjson.converter.NumpyConverter, znjson.converter.PathlibConverter]
),
)
assert json.loads(encoded_str)[0]["_type"] == "np.ndarray_b64"
assert json.loads(encoded_str)[1]["_type"] == "pathlib.Path"

with pytest.raises(TypeError):
_ = json.dumps({array, path}, cls=znjson.ZnEncoder.from_converters([]))

encoded_str = json.dumps(
[array, path], cls=znjson.ZnEncoder.from_converters([], add_default=True)
)
assert json.loads(encoded_str)[0]["_type"] == "np.ndarray_small"
assert json.loads(encoded_str)[1]["_type"] == "pathlib.Path"
2 changes: 1 addition & 1 deletion tests/test_znjson.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@


def test_version():
assert znjson.__version__ == "0.2.0"
assert znjson.__version__ == "0.2.1"
79 changes: 59 additions & 20 deletions znjson/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,76 @@
from znjson.config import config


class ZnEncoder(json.JSONEncoder):
class SelectConverters:
"""Mixin to manipulate to converters in use"""

_active_converters = None

@classmethod
def from_converters(cls, converter, add_default=False):
"""Return a new class definition with defined active converters
Parameters
----------
converter: converter|list of converters to use
add_default: bool, default False
In addition to the converter argument use the config.ACTIVE_CONVERTER as well
Returns
-------
return a child class with altered _active_converters.
! This does not return an instance of the child class !
"""

if not isinstance(converter, list):
converter = [converter]

class ZnEncoderCopy(cls):
"""Create a child of cls with altered _active_converters"""

_active_converters = (
converter + config.ACTIVE_CONVERTER if add_default else converter
)

return ZnEncoderCopy

@property
def active_converters(self) -> list:
"""Get the converters in use"""
config.sort()
if self._active_converters is None:
return config.ACTIVE_CONVERTER
return self._active_converters


class ZnEncoder(json.JSONEncoder, SelectConverters):
"""Encode objects using the ZnJSON Converter"""

def default(self, o: Any) -> Any:
config.sort()
for converter in config.ACTIVE_CONVERTER:
for converter in self.active_converters:
if converter() == o:
return converter().encode_obj(o)
raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")


def object_hook(obj):
"""Object hook for decoding data in ZnDecoder"""
try:
# must have "_type" and "value" keys
instance = obj["_type"]
_ = obj["value"]
except KeyError:
return obj
config.sort()
for converter in config.ACTIVE_CONVERTER:
if converter.representation == instance:
return converter().decode_obj(obj)
raise TypeError(f"Object of type {instance} could not be converted")


class ZnDecoder(json.JSONDecoder):
class ZnDecoder(json.JSONDecoder, SelectConverters):
"""Decode data converted with ZnJSON encoder"""

def __init__(self):
super().__init__(object_hook=object_hook)
super().__init__(object_hook=self._object_hook)

def _object_hook(self, obj):
"""Object hook for decoding data in ZnDecoder"""
try:
# must have "_type" and "value" keys
instance = obj["_type"]
_ = obj["value"]
except KeyError:
return obj
for converter in self.active_converters:
if converter.representation == instance:
return converter().decode_obj(obj)
raise TypeError(f"Object of type {instance} could not be converted")


if __name__ == "__main__":
Expand Down

0 comments on commit ec3e495

Please sign in to comment.