From 9a35f983417e20aee831c87426d0d2bd6aeadea7 Mon Sep 17 00:00:00 2001 From: Samuel Yvon Date: Wed, 3 Aug 2022 13:57:41 -0400 Subject: [PATCH 1/3] Pull down the `include_default_values` argument to `to_json` The `to_json` method does not include the `include_default_values` option from `to_dict`. While the implementation to `to_json` is almost litteraly only a call to `to_dict()` (and thus trivial to replace), it is more convenient for users to not have to reimplement it when the option is desired. --- src/betterproto/__init__.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/betterproto/__init__.py b/src/betterproto/__init__.py index 62056e385..bfd143044 100644 --- a/src/betterproto/__init__.py +++ b/src/betterproto/__init__.py @@ -1260,7 +1260,9 @@ def from_dict(self: T, value: Dict[str, Any]) -> T: setattr(self, field_name, v) return self - def to_json(self, indent: Union[None, int, str] = None) -> str: + def to_json( + self, indent: Union[None, int, str] = None, include_default_values: bool = False + ) -> str: """A helper function to parse the message instance into its JSON representation. @@ -1273,12 +1275,19 @@ def to_json(self, indent: Union[None, int, str] = None) -> str: indent: Optional[Union[:class:`int`, :class:`str`]] The indent to pass to :func:`json.dumps`. + include_default_values: :class:`bool` + If ``True`` will include the default values of fields. Default is ``False``. + E.g. an ``int32`` field will be included with a value of ``0`` if this is + set to ``True``, otherwise this would be ignored. + Returns -------- :class:`str` The JSON representation of the message. """ - return json.dumps(self.to_dict(), indent=indent) + return json.dumps( + self.to_dict(include_default_values=include_default_values), indent=indent + ) def from_json(self: T, value: Union[str, bytes]) -> T: """A helper function to return the message instance from its JSON From 8127732ac5dbbe1cc55a26f7f3e466056043dd68 Mon Sep 17 00:00:00 2001 From: Samuel Yvon Date: Wed, 3 Aug 2022 15:36:40 -0400 Subject: [PATCH 2/3] Also add the `casing` arg. --- src/betterproto/__init__.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/betterproto/__init__.py b/src/betterproto/__init__.py index bfd143044..3f36a489d 100644 --- a/src/betterproto/__init__.py +++ b/src/betterproto/__init__.py @@ -1261,7 +1261,10 @@ def from_dict(self: T, value: Dict[str, Any]) -> T: return self def to_json( - self, indent: Union[None, int, str] = None, include_default_values: bool = False + self, + indent: Union[None, int, str] = None, + include_default_values: bool = False, + casing: Casing = Casing.CAMEL, ) -> str: """A helper function to parse the message instance into its JSON representation. @@ -1280,13 +1283,18 @@ def to_json( E.g. an ``int32`` field will be included with a value of ``0`` if this is set to ``True``, otherwise this would be ignored. + casing: :class:`Casing` + The casing to use for key values. Default is :attr:`Casing.CAMEL` for + compatibility purposes. + Returns -------- :class:`str` The JSON representation of the message. """ return json.dumps( - self.to_dict(include_default_values=include_default_values), indent=indent + self.to_dict(include_default_values=include_default_values, casing=casing), + indent=indent, ) def from_json(self: T, value: Union[str, bytes]) -> T: From f58ba68a6057b879bc4d6fdee837c598e7eb62c7 Mon Sep 17 00:00:00 2001 From: Samuel Yvon Date: Thu, 4 Aug 2022 10:46:56 -0400 Subject: [PATCH 3/3] Add tests --- tests/test_features.py | 63 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/tests/test_features.py b/tests/test_features.py index b59bfe892..2deef2bab 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -1,3 +1,4 @@ +import json from copy import ( copy, deepcopy, @@ -190,6 +191,37 @@ class CasingTest(betterproto.Message): assert test == CasingTest(1, 2, 3, 4) + # Serializing should be strict. + assert json.loads(test.to_json()) == { + "pascalCase": 1, + "camelCase": 2, + "snakeCase": 3, + "kabobCase": 4, + } + + assert json.loads(test.to_json(casing=betterproto.Casing.SNAKE)) == { + "pascal_case": 1, + "camel_case": 2, + "snake_case": 3, + "kabob_case": 4, + } + + +def test_dict_casing(): + @dataclass + class CasingTest(betterproto.Message): + pascal_case: int = betterproto.int32_field(1) + camel_case: int = betterproto.int32_field(2) + snake_case: int = betterproto.int32_field(3) + kabob_case: int = betterproto.int32_field(4) + + # Parsing should accept almost any input + test = CasingTest().from_dict( + {"PascalCase": 1, "camelCase": 2, "snake_case": 3, "kabob-case": 4} + ) + + assert test == CasingTest(1, 2, 3, 4) + # Serializing should be strict. assert test.to_dict() == { "pascalCase": 1, @@ -233,6 +265,37 @@ class Request(betterproto.Message): assert Request().parse(b"\n\x00").flag is False +def test_to_json_default_values(): + @dataclass + class TestMessage(betterproto.Message): + some_int: int = betterproto.int32_field(1) + some_double: float = betterproto.double_field(2) + some_str: str = betterproto.string_field(3) + some_bool: bool = betterproto.bool_field(4) + + # Empty dict + test = TestMessage().from_dict({}) + + assert json.loads(test.to_json(include_default_values=True)) == { + "someInt": 0, + "someDouble": 0.0, + "someStr": "", + "someBool": False, + } + + # All default values + test = TestMessage().from_dict( + {"someInt": 0, "someDouble": 0.0, "someStr": "", "someBool": False} + ) + + assert json.loads(test.to_json(include_default_values=True)) == { + "someInt": 0, + "someDouble": 0.0, + "someStr": "", + "someBool": False, + } + + def test_to_dict_default_values(): @dataclass class TestMessage(betterproto.Message):