diff --git a/jsonstar/default_encoders.py b/jsonstar/default_encoders.py index 796e731..6c74ca4 100644 --- a/jsonstar/default_encoders.py +++ b/jsonstar/default_encoders.py @@ -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 = {} @@ -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 = () @@ -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, ] diff --git a/jsonstar/encoder.py b/jsonstar/encoder.py index c9c1f74..fe9bd4c 100644 --- a/jsonstar/encoder.py +++ b/jsonstar/encoder.py @@ -20,12 +20,23 @@ 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) @@ -33,13 +44,29 @@ def __init__(self, *args, functional_encoders=(), typed_encoders: dict[type, cal 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: @@ -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(): diff --git a/tests/test_encoder.py b/tests/test_encoder.py index 2c48eb3..d1a4f58 100644 --- a/tests/test_encoder.py +++ b/tests/test_encoder.py @@ -13,8 +13,7 @@ class CustomType: class JSONEncoderTest(JSONEncoderStar): - default_typed_encoders = {} - default_functional_encoders = [] + pass @pytest.fixture @@ -22,20 +21,26 @@ 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") @@ -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"' diff --git a/tests/test_register_defaults.py b/tests/test_register_defaults.py index 2ae82db..42a53f8 100644 --- a/tests/test_register_defaults.py +++ b/tests/test_register_defaults.py @@ -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") @@ -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}