Skip to content
Merged
16 changes: 13 additions & 3 deletions jsonstar/default_encoders.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@
try:
from pydantic import BaseModel

def pydantic_dict(o):
return o.dict()

PYDANTIC_TYPED_ENCODERS = {
BaseModel: lambda o: o.dict(),
BaseModel: pydantic_dict,
}
except ImportError:
PYDANTIC_TYPED_ENCODERS = {}
Expand All @@ -28,7 +31,10 @@
try:
import attrs

ATTRS_FUNCTIONAL_ENCODERS = [lambda o: attrs.asdict(o)]
def attrs_dict(o):
return attrs.asdict(o)

ATTRS_FUNCTIONAL_ENCODERS = [attrs_dict]
except ImportError:
ATTRS_FUNCTIONAL_ENCODERS = ()

Expand All @@ -44,8 +50,12 @@ def encode_timedelta_as_iso_string(duration):
return f"{sign}P{days}DT{hours:02d}H{minutes:02d}M{seconds:02d}{ms}S"


def dataclasses_asdict(o):
return dataclasses.asdict(o)


DEFAULT_FUNCTIONAL_ENCODERS = [
lambda o: dataclasses.asdict(o),
dataclasses_asdict,
*ATTRS_FUNCTIONAL_ENCODERS,
]

Expand Down
41 changes: 34 additions & 7 deletions jsonstar/encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,53 @@ def __setitem__(self, type_, function):
self.move_to_end(base)


class JSONEncoderStar(stdlib_json.JSONEncoder):
class EncoderMeta(type):
def __new__(mcs, name, bases, namespace):
if "_default_functional_encoders" not in namespace:
namespace["_default_functional_encoders"] = []

if "_default_typed_encoders" not in namespace:
namespace["_default_typed_encoders"] = TypedEncoderRegistry()

return super().__new__(mcs, name, bases, namespace)


class JSONEncoderStar(stdlib_json.JSONEncoder, metaclass=EncoderMeta):
class FUNCTIONAL:
"""Sentinel type to register a functional encoder."""

default_typed_encoders = TypedEncoderRegistry(DEFAULT_TYPED_ENCODERS)
default_functional_encoders = DEFAULT_FUNCTIONAL_ENCODERS
_default_typed_encoders = TypedEncoderRegistry(DEFAULT_TYPED_ENCODERS)
_default_functional_encoders = DEFAULT_FUNCTIONAL_ENCODERS

def __init__(self, *args, functional_encoders=(), typed_encoders: dict[type, callable] = NULL_DICT, **kwargs):
super().__init__(*args, **kwargs)

self._typed_encoders = TypedEncoderRegistry(typed_encoders)
self._functional_encoders = [*functional_encoders]

@classmethod
def default_functional_encoders(cls):
if cls is JSONEncoderStar:
return cls._default_functional_encoders
else:
return cls._default_functional_encoders + cls.__base__.default_functional_encoders()

@classmethod
def default_typed_encoders(cls):
if cls is JSONEncoderStar:
return cls._default_typed_encoders
else:
return TypedEncoderRegistry(ChainMap(cls._default_typed_encoders, cls.__base__.default_typed_encoders()))

@property
def functional_encoders(self):
return chain(self._functional_encoders, self.default_functional_encoders)
return chain(self._functional_encoders, self.default_functional_encoders())

@property
def typed_encoders(self):
return ChainMap(self._typed_encoders, self.default_typed_encoders)
i = self._typed_encoders
base = self.default_typed_encoders()
return ChainMap(i, base)

def register(self, function, type_=FUNCTIONAL):
if type_ is self.FUNCTIONAL:
Expand All @@ -50,9 +77,9 @@ def register(self, function, type_=FUNCTIONAL):
@classmethod
def register_default_encoder(cls, function, type_=FUNCTIONAL):
if type_ is cls.FUNCTIONAL:
cls.default_functional_encoders.append(function)
cls._default_functional_encoders.append(function)
else:
cls.default_typed_encoders[type_] = function
cls._default_typed_encoders[type_] = function

def default(self, o) -> str:
for base, encoder in self.typed_encoders.items():
Expand Down
19 changes: 12 additions & 7 deletions tests/test_encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,34 @@ class CustomType:


class JSONEncoderTest(JSONEncoderStar):
default_typed_encoders = {}
default_functional_encoders = []
pass


@pytest.fixture
def encoder():
return JSONEncoderTest()


@pytest.fixture(autouse=True)
def empty_encoder_class(monkeypatch):
monkeypatch.setattr(JSONEncoderTest, "_default_functional_encoders", [])
monkeypatch.setattr(JSONEncoderTest, "_default_typed_encoders", {})


class TestTypedEncoders:
def test_default_typed_encoders_are_used_when_nothing_else_is_registered(self, encoder):
encoder.default_typed_encoders = {CustomType: lambda o: "CustomType default encoder"}
encoder.__class__._default_typed_encoders = {CustomType: lambda o: "CustomType default encoder"}

assert encoder.encode(CustomType()) == '"CustomType default encoder"'

def test_typed_encoders_have_precedence_over_default_type_encoders(self, encoder):
encoder.default_typed_encoders = {CustomType: lambda o: "CustomType default encoder"}
encoder.__class__._default_typed_encoders = {CustomType: lambda o: "CustomType default encoder"}
encoder.register(lambda o: "CustomType encoder", CustomType)

assert encoder.encode(CustomType()) == '"CustomType encoder"'

def test_typed_encoders_have_precedence_over_functional_encoders(self, encoder):
encoder.default_typed_encoders = {CustomType: lambda o: "CustomType default encoder"}
encoder.__class__._default_typed_encoders = {CustomType: lambda o: "CustomType default encoder"}
encoder.register(lambda o: "CustomType encoder", CustomType)
encoder.register(lambda o: "Functional encoder")

Expand Down Expand Up @@ -76,12 +81,12 @@ class Child(Mother, Father):

class TestFunctionalEncoders:
def test_default_functional_encoders_are_used_when_nothing_else_is_registered(self, encoder):
encoder.default_functional_encoders = [lambda o: "default functional encoder"]
encoder.__class__._default_functional_encoders = [lambda o: "default functional encoder"]

assert encoder.encode(CustomType()) == '"default functional encoder"'

def test_functional_encoders_have_precedence_over_default_functional_encoders(self, encoder):
encoder.default_functional_encoders = [lambda o: "default functional encoder"]
encoder.__class__._default_functional_encoders = [lambda o: "default functional encoder"]
encoder.register(lambda o: "functional encoder")

assert encoder.encode(CustomType()) == '"functional encoder"'
Expand Down
67 changes: 65 additions & 2 deletions tests/test_register_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ class CustomType:
class TestDefaultEncoderRegistration:
@pytest.fixture
def empty_encoder(self, monkeypatch):
monkeypatch.setattr(jsonstar.JSONEncoderStar, "default_functional_encoders", [])
monkeypatch.setattr(jsonstar.JSONEncoderStar, "default_typed_encoders", {})
monkeypatch.setattr(jsonstar.JSONEncoderStar, "_default_functional_encoders", [])
monkeypatch.setattr(jsonstar.JSONEncoderStar, "_default_typed_encoders", {})

def test_register_default_encoder_with_module_api(self, empty_encoder):
jsonstar.register_default_encoder(lambda o: "functional default encoder")
Expand All @@ -30,3 +30,66 @@ def test_register_default_encoder_with_classmethod(self, empty_encoder):

assert jsonstar.dumps(CustomType()) == '"typed default encoder"'
assert jsonstar.dumps(object()) == '"functional default encoder"'


class TestIsolateDefaultsFromEncoderClasses:
def test_isolate_functional_defaults_from_different_encoder_classes(self):
class EncoderA1(JSONEncoderStar):
pass

class EncoderA2(EncoderA1):
pass

class EncoderB1(JSONEncoderStar):
pass

def a1(o):
return o

def a2(o):
return o

def b1(o):
return o

default_functional_encoders = JSONEncoderStar._default_functional_encoders.copy()

EncoderA1.register_default_encoder(a1)
EncoderA2.register_default_encoder(a2)
EncoderB1.register_default_encoder(b1)

assert JSONEncoderStar.default_functional_encoders() == default_functional_encoders
assert EncoderA1.default_functional_encoders() == [a1] + default_functional_encoders
assert EncoderA2.default_functional_encoders() == [a2, a1] + default_functional_encoders
assert EncoderB1.default_functional_encoders() == [b1] + default_functional_encoders

def test_isolate_typed_defaults_from_different_encoder_classes(self):
class EncoderA1(JSONEncoderStar):
pass

class EncoderA2(EncoderA1):
pass

class EncoderB1(JSONEncoderStar):
pass

def a1(o):
return o

def a2(o):
return o

def b1(o):
return o

default_typed_encoders = JSONEncoderStar._default_typed_encoders.copy()

EncoderA1.register_default_encoder(a1, str)
EncoderA1.register_default_encoder(a1, int)
EncoderA2.register_default_encoder(a2, str)
EncoderB1.register_default_encoder(b1, str)

assert JSONEncoderStar.default_typed_encoders() == default_typed_encoders
assert EncoderA1.default_typed_encoders() == {**default_typed_encoders, str: a1, int: a1}
assert EncoderA2.default_typed_encoders() == {**default_typed_encoders, str: a2, int: a1}
assert EncoderB1.default_typed_encoders() == {**default_typed_encoders, str: b1}