Skip to content

Commit 21dd293

Browse files
Isolate the default encoders of subclasses. (#2)
* Update poetry and lock. * Replace default encoder lambdas with named functions to facilitate debugging. * Allow JSONEncoder subclasses to have isolated namespaces to compose with their base class without impacting other subclasses. * Revert last 3 commits that were wrongly pushed into main. Revert "Update poetry and lock." This reverts commit 44d33c6. Revert "Replace default encoder lambdas with named functions to facilitate debugging." This reverts commit 83ac429. Revert "Allow JSONEncoder subclasses to have isolated namespaces to compose with their base class without impacting other subclasses." This reverts commit 9e66fac. * Update poetry and lock. (cherry picked from commit 44d33c6) * Replace default encoder lambdas with named functions to facilitate debugging. (cherry picked from commit 83ac429) * Allow JSONEncoder subclasses to have isolated namespaces to compose with their base class without impacting other subclasses. (cherry picked from commit 9e66fac) * Remove debug code. * Revert "Update poetry and lock." This reverts commit 73f6cef. * Fix the order of the typed registry.
1 parent 22de2fa commit 21dd293

File tree

4 files changed

+124
-19
lines changed

4 files changed

+124
-19
lines changed

jsonstar/default_encoders.py

+13-3
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@
1818
try:
1919
from pydantic import BaseModel
2020

21+
def pydantic_dict(o):
22+
return o.dict()
23+
2124
PYDANTIC_TYPED_ENCODERS = {
22-
BaseModel: lambda o: o.dict(),
25+
BaseModel: pydantic_dict,
2326
}
2427
except ImportError:
2528
PYDANTIC_TYPED_ENCODERS = {}
@@ -28,7 +31,10 @@
2831
try:
2932
import attrs
3033

31-
ATTRS_FUNCTIONAL_ENCODERS = [lambda o: attrs.asdict(o)]
34+
def attrs_dict(o):
35+
return attrs.asdict(o)
36+
37+
ATTRS_FUNCTIONAL_ENCODERS = [attrs_dict]
3238
except ImportError:
3339
ATTRS_FUNCTIONAL_ENCODERS = ()
3440

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

4652

53+
def dataclasses_asdict(o):
54+
return dataclasses.asdict(o)
55+
56+
4757
DEFAULT_FUNCTIONAL_ENCODERS = [
48-
lambda o: dataclasses.asdict(o),
58+
dataclasses_asdict,
4959
*ATTRS_FUNCTIONAL_ENCODERS,
5060
]
5161

jsonstar/encoder.py

+34-7
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,53 @@ 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+
return TypedEncoderRegistry(ChainMap(cls._default_typed_encoders, cls.__base__.default_typed_encoders()))
60+
3661
@property
3762
def functional_encoders(self):
38-
return chain(self._functional_encoders, self.default_functional_encoders)
63+
return chain(self._functional_encoders, self.default_functional_encoders())
3964

4065
@property
4166
def typed_encoders(self):
42-
return ChainMap(self._typed_encoders, self.default_typed_encoders)
67+
i = self._typed_encoders
68+
base = self.default_typed_encoders()
69+
return ChainMap(i, base)
4370

4471
def register(self, function, type_=FUNCTIONAL):
4572
if type_ is self.FUNCTIONAL:
@@ -50,9 +77,9 @@ def register(self, function, type_=FUNCTIONAL):
5077
@classmethod
5178
def register_default_encoder(cls, function, type_=FUNCTIONAL):
5279
if type_ is cls.FUNCTIONAL:
53-
cls.default_functional_encoders.append(function)
80+
cls._default_functional_encoders.append(function)
5481
else:
55-
cls.default_typed_encoders[type_] = function
82+
cls._default_typed_encoders[type_] = function
5683

5784
def default(self, o) -> str:
5885
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)