Skip to content

Commit

Permalink
src, test: more generic enum handling (#12)
Browse files Browse the repository at this point in the history
* src, test: more generic enum handling

Signed-off-by: William Woodruff <william@trailofbits.com>

* behave more like Python's json

Signed-off-by: William Woodruff <william@trailofbits.com>

* lintage

Signed-off-by: William Woodruff <william@trailofbits.com>

---------

Signed-off-by: William Woodruff <william@trailofbits.com>
  • Loading branch information
woodruffw authored Mar 21, 2024
1 parent 1796ae7 commit 5cda7f4
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 10 deletions.
19 changes: 10 additions & 9 deletions src/rfc8785/_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,10 @@
import typing
from io import BytesIO

_Scalar = typing.Union[bool, int, str, float, None]

_Value = typing.Union[
bool,
int,
str,
float,
None,
_Scalar,
typing.Sequence["_Value"],
typing.Tuple["_Value"],
typing.Mapping[str, "_Value"],
Expand Down Expand Up @@ -195,24 +193,26 @@ def dump(obj: _Value, sink: typing.IO[bytes]) -> None:
if obj is None:
sink.write(b"null")
elif isinstance(obj, bool):
obj = bool(obj)
if obj is True:
sink.write(b"true")
else:
sink.write(b"false")
elif isinstance(obj, int):
# Annoyance: int can be subclassed by types like IntEnum,
# which then break or change `int.__str__`. Rather than plugging
# these individually, we coerce back to `int`.
obj = int(obj)

if obj < _INT_MIN or obj > _INT_MAX:
raise IntegerDomainError(obj)
sink.write(str(obj).encode("utf-8"))
elif isinstance(obj, str):
# NOTE: We don't coerce with `str(...)`` here, since that will do
# the wrong thing for `(str, Enum)` subtypes where `__str__` is
# `Enum.__str__`.
_serialize_str(obj, sink)
elif isinstance(obj, float):
obj = float(obj)
_serialize_float(obj, sink)
elif isinstance(obj, (list, tuple)):
obj = list(obj)
if not obj:
# Optimization for empty lists.
sink.write(b"[]")
Expand All @@ -225,6 +225,7 @@ def dump(obj: _Value, sink: typing.IO[bytes]) -> None:
dump(elem, sink)
sink.write(b"]")
elif isinstance(obj, dict):
obj = dict(obj)
if not obj:
# Optimization for empty dicts.
sink.write(b"{}")
Expand Down
41 changes: 40 additions & 1 deletion test/test_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import json
import struct
import sys
from enum import IntEnum
from enum import Enum, IntEnum
from io import BytesIO

import pytest
Expand Down Expand Up @@ -129,6 +129,45 @@ class X(StrEnum):
assert json.loads(raw) == ["foo", "bar", "baz"]


def test_dumps_enum_multiple_inheritance():
# Manually inheriting str, Enum should also work.
class X(str, Enum):
A = "foo"
B = "bar"
C = "baz"

raw = impl.dumps([X.A, X.B, X.C])
assert json.loads(raw) == ["foo", "bar", "baz"]

# Same for other JSON-able enum types.
class Y(dict, Enum):
A = {"A": "foo"}
B = {"B": "bar"}
C = {"C": "baz"}

raw = impl.dumps([Y.A, Y.B, Y.C])
assert json.loads(raw) == [{"A": "foo"}, {"B": "bar"}, {"C": "baz"}]

class Z(int, Enum):
A = 1
B = 2
C = 3

raw = impl.dumps([Z.A, Z.B, Z.C])
assert json.loads(raw) == [1, 2, 3]


def test_dumps_bare_enum_fails():
class X(Enum):
A = "1"
B = 2
C = 3.0

# Python's json doesn't allow this, so we don't either.
with pytest.raises(impl.CanonicalizationError, match="unsupported type"):
impl.dumps([X.A, X.B, X.C])


def test_dumps_nonstring_key():
with pytest.raises(impl.CanonicalizationError, match="object keys must be strings"):
impl.dumps({1: 2, None: 3})

0 comments on commit 5cda7f4

Please sign in to comment.