-
Notifications
You must be signed in to change notification settings - Fork 30
Closed
Description
First of all, thank you for providing the json5
package. It really does improve the configuration file readability. So far, I have not encountered any issues while parsing / loading and un-parsing / dumping.
However, I did notice a strange behavior when using the indent
setting during dumps(...)
. Please see this MWE below:
import json5
class Foo:
def __init__(self):
self.bar = Bar()
self.baz = "hello"
self.foobar = None
self.arr = [1, 2, 3]
self.qux = {"a": 1, "b": 2}
self.obj = Bar()
class Bar:
def __init__(self):
self.answer = 42
class Encoder(json5.JSON5Encoder):
def default(self, obj):
if isinstance(obj, Foo):
return {"__type": "Foo", **vars(obj)}
if isinstance(obj, Bar):
return {"__type": "Bar", "answer": obj.answer}
return super().default(obj)
print(
json5.dumps(
Foo(),
indent=4,
cls=Encoder,
),
)
It gives this semantically correct output:
{
__type: "Foo",
bar: {
__type: "Bar",
answer: 42,
},
baz: "hello",
foobar: null,
arr: [
1,
2,
3,
],
qux: {
a: 1,
b: 2,
},
obj: {
__type: "Bar",
answer: 42,
},
}
As you can see, the indentation gets applied twice for my custom objects (Foo and Bar) and their closing braces do not line up with the (semantically) corresponding opening braces.
This is due to an additional level + 1
in _encode_non_basic_type
.
In fact, overriding the method _encode_non_basic_type
in the custom Encoder
class will fix this issue:
# ...
class Encoder(json5.JSON5Encoder):
def default(self, obj):
if isinstance(obj, Foo):
return {"__type": "Foo", **vars(obj)}
if isinstance(obj, Bar):
return {"__type": "Bar", "answer": obj.answer}
return super().default(obj)
def _encode_non_basic_type(self, obj, seen: Set, level: int) -> str:
# Basic types can't be recursive so we only check for circularity
# on non-basic types. If for some reason the caller was using a
# subclass of a basic type and wanted to check circularity on it,
# it'd have to do so directly in a subclass of JSON5Encoder.
if self.check_circular:
i = id(obj)
if i in seen:
raise ValueError("Circular reference detected.")
seen.add(i)
# Ideally we'd use collections.abc.Mapping and collections.abc.Sequence
# here, but for backwards-compatibility with potential old callers,
# we only check for the two attributes we need in each case.
if hasattr(obj, "keys") and hasattr(obj, "__getitem__"):
s = self._encode_dict(obj, seen, level + 1)
elif hasattr(obj, "__getitem__") and hasattr(obj, "__iter__"):
s = self._encode_array(obj, seen, level + 1)
else:
s = self.encode(self.default(obj), seen, level, as_key=False) # <---- this line is the culprit
assert s is not None
if self.check_circular:
seen.remove(i) # type: ignore
return s
# ...
Output:
{
__type: "Foo",
bar: {
__type: "Bar",
answer: 42,
},
baz: "hello",
foobar: null,
arr: [
1,
2,
3,
],
qux: {
a: 1,
b: 2,
},
obj: {
__type: "Bar",
answer: 42,
},
}
Metadata
Metadata
Assignees
Labels
No labels