Skip to content

Commit 9e66fac

Browse files
Allow JSONEncoder subclasses to have isolated namespaces to compose with their base class without impacting other subclasses.
1 parent 83ac429 commit 9e66fac

File tree

3 files changed

+116
-16
lines changed

3 files changed

+116
-16
lines changed

jsonstar/encoder.py

+39-7
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,58 @@ def __setitem__(self, type_, function):
2020
self.move_to_end(base)
2121

2222

23-
class JSONEncoderStar(stdlib_json.JSONEncoder):
23+
class EncoderMeta(type):
24+
def __new__(mcs, name, bases, namespace):
25+
if "_default_functional_encoders" not in namespace:
26+
namespace["_default_functional_encoders"] = []
27+
28+
if "_default_typed_encoders" not in namespace:
29+
namespace["_default_typed_encoders"] = TypedEncoderRegistry()
30+
31+
return super().__new__(mcs, name, bases, namespace)
32+
33+
34+
class JSONEncoderStar(stdlib_json.JSONEncoder, metaclass=EncoderMeta):
2435
class FUNCTIONAL:
2536
"""Sentinel type to register a functional encoder."""
2637

27-
default_typed_encoders = TypedEncoderRegistry(DEFAULT_TYPED_ENCODERS)
28-
default_functional_encoders = DEFAULT_FUNCTIONAL_ENCODERS
38+
_default_typed_encoders = TypedEncoderRegistry(DEFAULT_TYPED_ENCODERS)
39+
_default_functional_encoders = DEFAULT_FUNCTIONAL_ENCODERS
2940

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

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

47+
@classmethod
48+
def default_functional_encoders(cls):
49+
if cls is JSONEncoderStar:
50+
return cls._default_functional_encoders
51+
else:
52+
return cls._default_functional_encoders + cls.__base__.default_functional_encoders()
53+
54+
@classmethod
55+
def default_typed_encoders(cls):
56+
if cls is JSONEncoderStar:
57+
return cls._default_typed_encoders
58+
else:
59+
a = cls._default_typed_encoders
60+
b = cls.__base__.default_typed_encoders()
61+
c = ChainMap(a, b)
62+
d = TypedEncoderRegistry(c)
63+
return d
64+
# return TypedEncoderRegistry(ChainMap(cls.__base__.default_typed_encoders(), cls._default_typed_encoders))
65+
3666
@property
3767
def functional_encoders(self):
38-
return chain(self._functional_encoders, self.default_functional_encoders)
68+
return chain(self._functional_encoders, self.default_functional_encoders())
3969

4070
@property
4171
def typed_encoders(self):
42-
return ChainMap(self._typed_encoders, self.default_typed_encoders)
72+
i = self._typed_encoders
73+
base = self.default_typed_encoders()
74+
return ChainMap(i, base)
4375

4476
def register(self, function, type_=FUNCTIONAL):
4577
if type_ is self.FUNCTIONAL:
@@ -50,9 +82,9 @@ def register(self, function, type_=FUNCTIONAL):
5082
@classmethod
5183
def register_default_encoder(cls, function, type_=FUNCTIONAL):
5284
if type_ is cls.FUNCTIONAL:
53-
cls.default_functional_encoders.append(function)
85+
cls._default_functional_encoders.append(function)
5486
else:
55-
cls.default_typed_encoders[type_] = function
87+
cls._default_typed_encoders[type_] = function
5688

5789
def default(self, o) -> str:
5890
for base, encoder in self.typed_encoders.items():

tests/test_encoder.py

+12-7
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,34 @@ class CustomType:
1313

1414

1515
class JSONEncoderTest(JSONEncoderStar):
16-
default_typed_encoders = {}
17-
default_functional_encoders = []
16+
pass
1817

1918

2019
@pytest.fixture
2120
def encoder():
2221
return JSONEncoderTest()
2322

2423

24+
@pytest.fixture(autouse=True)
25+
def empty_encoder_class(monkeypatch):
26+
monkeypatch.setattr(JSONEncoderTest, "_default_functional_encoders", [])
27+
monkeypatch.setattr(JSONEncoderTest, "_default_typed_encoders", {})
28+
29+
2530
class TestTypedEncoders:
2631
def test_default_typed_encoders_are_used_when_nothing_else_is_registered(self, encoder):
27-
encoder.default_typed_encoders = {CustomType: lambda o: "CustomType default encoder"}
32+
encoder.__class__._default_typed_encoders = {CustomType: lambda o: "CustomType default encoder"}
2833

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

3136
def test_typed_encoders_have_precedence_over_default_type_encoders(self, encoder):
32-
encoder.default_typed_encoders = {CustomType: lambda o: "CustomType default encoder"}
37+
encoder.__class__._default_typed_encoders = {CustomType: lambda o: "CustomType default encoder"}
3338
encoder.register(lambda o: "CustomType encoder", CustomType)
3439

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

3742
def test_typed_encoders_have_precedence_over_functional_encoders(self, encoder):
38-
encoder.default_typed_encoders = {CustomType: lambda o: "CustomType default encoder"}
43+
encoder.__class__._default_typed_encoders = {CustomType: lambda o: "CustomType default encoder"}
3944
encoder.register(lambda o: "CustomType encoder", CustomType)
4045
encoder.register(lambda o: "Functional encoder")
4146

@@ -76,12 +81,12 @@ class Child(Mother, Father):
7681

7782
class TestFunctionalEncoders:
7883
def test_default_functional_encoders_are_used_when_nothing_else_is_registered(self, encoder):
79-
encoder.default_functional_encoders = [lambda o: "default functional encoder"]
84+
encoder.__class__._default_functional_encoders = [lambda o: "default functional encoder"]
8085

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

8388
def test_functional_encoders_have_precedence_over_default_functional_encoders(self, encoder):
84-
encoder.default_functional_encoders = [lambda o: "default functional encoder"]
89+
encoder.__class__._default_functional_encoders = [lambda o: "default functional encoder"]
8590
encoder.register(lambda o: "functional encoder")
8691

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

tests/test_register_defaults.py

+65-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ class CustomType:
1414
class TestDefaultEncoderRegistration:
1515
@pytest.fixture
1616
def empty_encoder(self, monkeypatch):
17-
monkeypatch.setattr(jsonstar.JSONEncoderStar, "default_functional_encoders", [])
18-
monkeypatch.setattr(jsonstar.JSONEncoderStar, "default_typed_encoders", {})
17+
monkeypatch.setattr(jsonstar.JSONEncoderStar, "_default_functional_encoders", [])
18+
monkeypatch.setattr(jsonstar.JSONEncoderStar, "_default_typed_encoders", {})
1919

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

3131
assert jsonstar.dumps(CustomType()) == '"typed default encoder"'
3232
assert jsonstar.dumps(object()) == '"functional default encoder"'
33+
34+
35+
class TestIsolateDefaultsFromEncoderClasses:
36+
def test_isolate_functional_defaults_from_different_encoder_classes(self):
37+
class EncoderA1(JSONEncoderStar):
38+
pass
39+
40+
class EncoderA2(EncoderA1):
41+
pass
42+
43+
class EncoderB1(JSONEncoderStar):
44+
pass
45+
46+
def a1(o):
47+
return o
48+
49+
def a2(o):
50+
return o
51+
52+
def b1(o):
53+
return o
54+
55+
default_functional_encoders = JSONEncoderStar._default_functional_encoders.copy()
56+
57+
EncoderA1.register_default_encoder(a1)
58+
EncoderA2.register_default_encoder(a2)
59+
EncoderB1.register_default_encoder(b1)
60+
61+
assert JSONEncoderStar.default_functional_encoders() == default_functional_encoders
62+
assert EncoderA1.default_functional_encoders() == [a1] + default_functional_encoders
63+
assert EncoderA2.default_functional_encoders() == [a2, a1] + default_functional_encoders
64+
assert EncoderB1.default_functional_encoders() == [b1] + default_functional_encoders
65+
66+
def test_isolate_typed_defaults_from_different_encoder_classes(self):
67+
class EncoderA1(JSONEncoderStar):
68+
pass
69+
70+
class EncoderA2(EncoderA1):
71+
pass
72+
73+
class EncoderB1(JSONEncoderStar):
74+
pass
75+
76+
def a1(o):
77+
return o
78+
79+
def a2(o):
80+
return o
81+
82+
def b1(o):
83+
return o
84+
85+
default_typed_encoders = JSONEncoderStar._default_typed_encoders.copy()
86+
87+
EncoderA1.register_default_encoder(a1, str)
88+
EncoderA1.register_default_encoder(a1, int)
89+
EncoderA2.register_default_encoder(a2, str)
90+
EncoderB1.register_default_encoder(b1, str)
91+
92+
assert JSONEncoderStar.default_typed_encoders() == default_typed_encoders
93+
assert EncoderA1.default_typed_encoders() == {**default_typed_encoders, str: a1, int: a1}
94+
assert EncoderA2.default_typed_encoders() == {**default_typed_encoders, str: a2, int: a1}
95+
assert EncoderB1.default_typed_encoders() == {**default_typed_encoders, str: b1}

0 commit comments

Comments
 (0)