Skip to content

Commit d755623

Browse files
Make from_dict and from_json class methods (#92)
1 parent 0ae2e78 commit d755623

File tree

7 files changed

+17
-85
lines changed

7 files changed

+17
-85
lines changed

src/betterproto2/__init__.py

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from .casing import camel_case, safe_snake_case, snake_case
2929
from .enum import Enum as Enum
3030
from .grpc.grpclib_client import ServiceStub as ServiceStub
31-
from .utils import classproperty, hybridmethod
31+
from .utils import classproperty
3232

3333
if TYPE_CHECKING:
3434
from _typeshed import SupportsRead, SupportsWrite
@@ -1087,8 +1087,8 @@ def _from_dict_init(cls, mapping: Mapping[str, Any] | Any) -> Mapping[str, Any]:
10871087
init_kwargs[field_name] = value
10881088
return init_kwargs
10891089

1090-
@hybridmethod
1091-
def from_dict(cls: type[Self], value: Mapping[str, Any] | Any) -> Self: # type: ignore
1090+
@classmethod
1091+
def from_dict(cls: type[Self], value: Mapping[str, Any] | Any) -> Self:
10921092
"""
10931093
Parse the key/value pairs into the a new message instance.
10941094
@@ -1107,26 +1107,6 @@ def from_dict(cls: type[Self], value: Mapping[str, Any] | Any) -> Self: # type:
11071107

11081108
return cls(**cls._from_dict_init(value))
11091109

1110-
@from_dict.instancemethod
1111-
def from_dict(self, value: Mapping[str, Any] | Any) -> Self:
1112-
"""
1113-
Parse the key/value pairs into the current message instance. This returns the
1114-
instance itself and is therefore assignable and chainable.
1115-
1116-
Parameters
1117-
-----------
1118-
value: Dict[:class:`str`, Any]
1119-
The dictionary to parse from.
1120-
1121-
Returns
1122-
--------
1123-
:class:`Message`
1124-
The initialized message.
1125-
"""
1126-
for field, value in self._from_dict_init(value).items():
1127-
setattr(self, field, value)
1128-
return self
1129-
11301110
def to_json(
11311111
self,
11321112
indent: None | int | str = None,
@@ -1164,7 +1144,8 @@ def to_json(
11641144
indent=indent,
11651145
)
11661146

1167-
def from_json(self: T, value: str | bytes) -> T:
1147+
@classmethod
1148+
def from_json(cls, value: str | bytes) -> Self:
11681149
"""A helper function to return the message instance from its JSON
11691150
representation. This returns the instance itself and is therefore assignable
11701151
and chainable.
@@ -1183,7 +1164,7 @@ def from_json(self: T, value: str | bytes) -> T:
11831164
:class:`Message`
11841165
The initialized message.
11851166
"""
1186-
return self.from_dict(json.loads(value))
1167+
return cls.from_dict(json.loads(value))
11871168

11881169
def is_set(self, name: str) -> bool:
11891170
"""

src/betterproto2/utils.py

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,7 @@
11
from __future__ import annotations
22

33
from collections.abc import Callable
4-
from typing import (
5-
Any,
6-
Concatenate,
7-
Generic,
8-
TypeVar,
9-
)
10-
11-
from typing_extensions import (
12-
ParamSpec,
13-
Self,
14-
)
15-
16-
SelfT = TypeVar("SelfT")
17-
P = ParamSpec("P")
18-
HybridT = TypeVar("HybridT", covariant=True)
19-
20-
21-
class hybridmethod(Generic[SelfT, P, HybridT]):
22-
def __init__(
23-
self,
24-
func: Callable[Concatenate[type[SelfT], P], HybridT], # Must be the classmethod version
25-
):
26-
self.cls_func = func
27-
self.__doc__ = func.__doc__
28-
29-
def instancemethod(self, func: Callable[Concatenate[SelfT, P], HybridT]) -> Self:
30-
self.instance_func = func
31-
return self
32-
33-
def __get__(self, instance: SelfT | None, owner: type[SelfT]) -> Callable[P, HybridT]:
34-
if instance is None or self.instance_func is None:
35-
# either bound to the class, or no instance method available
36-
return self.cls_func.__get__(owner, None)
37-
return self.instance_func.__get__(instance, owner)
38-
4+
from typing import Any, Generic, TypeVar
395

406
T_co = TypeVar("T_co")
417
TT_co = TypeVar("TT_co", bound="type[Any]")

tests/inputs/oneof/test_oneof.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,14 @@
55
def test_which_count():
66
from tests.output_betterproto.oneof import Test
77

8-
message = Test()
9-
message.from_json(get_test_case_json_data("oneof")[0].json)
8+
message = Test.from_json(get_test_case_json_data("oneof")[0].json)
109
assert betterproto2.which_one_of(message, "foo") == ("pitied", 100)
1110

1211

1312
def test_which_name():
1413
from tests.output_betterproto.oneof import Test
1514

16-
message = Test()
17-
message.from_json(get_test_case_json_data("oneof", "oneof_name.json")[0].json)
15+
message = Test.from_json(get_test_case_json_data("oneof", "oneof_name.json")[0].json)
1816
assert betterproto2.which_one_of(message, "foo") == ("pitier", "Mr. T")
1917

2018

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
11
import betterproto2
2-
from tests.output_betterproto.oneof_enum import (
3-
Move,
4-
Signal,
5-
Test,
6-
)
2+
from tests.output_betterproto.oneof_enum import Move, Signal, Test
73
from tests.util import get_test_case_json_data
84

95

106
def test_which_one_of_returns_enum_with_default_value():
117
"""
128
returns first field when it is enum and set with default value
139
"""
14-
message = Test()
15-
message.from_json(get_test_case_json_data("oneof_enum", "oneof_enum-enum-0.json")[0].json)
10+
message = Test.from_json(get_test_case_json_data("oneof_enum", "oneof_enum-enum-0.json")[0].json)
1611

1712
assert message.move is None
1813
assert message.signal == Signal.PASS
@@ -23,17 +18,15 @@ def test_which_one_of_returns_enum_with_non_default_value():
2318
"""
2419
returns first field when it is enum and set with non default value
2520
"""
26-
message = Test()
27-
message.from_json(get_test_case_json_data("oneof_enum", "oneof_enum-enum-1.json")[0].json)
21+
message = Test.from_json(get_test_case_json_data("oneof_enum", "oneof_enum-enum-1.json")[0].json)
2822

2923
assert message.move is None
3024
assert message.signal == Signal.RESIGN
3125
assert betterproto2.which_one_of(message, "action") == ("signal", Signal.RESIGN)
3226

3327

3428
def test_which_one_of_returns_second_field_when_set():
35-
message = Test()
36-
message.from_json(get_test_case_json_data("oneof_enum")[0].json)
29+
message = Test.from_json(get_test_case_json_data("oneof_enum")[0].json)
3730
assert message.move == Move(x=2, y=3)
3831
assert message.signal is None
3932
assert betterproto2.which_one_of(message, "action") == ("move", Move(x=2, y=3))

tests/inputs/timestamp_dict_encode/test_timestamp_dict_encode.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ def test_datetime_dict_encode(tz: timezone):
2222
original_message = Test()
2323
original_message.ts = original_time
2424
encoded = original_message.to_dict()
25-
decoded_message = Test()
26-
decoded_message.from_dict(encoded)
25+
decoded_message = Test.from_dict(encoded)
2726

2827
# check that the timestamps are equal after decoding from dict
2928
assert original_message.ts.tzinfo is not None
@@ -37,8 +36,7 @@ def test_json_serialize(tz: timezone):
3736
original_message = Test()
3837
original_message.ts = original_time
3938
json_serialized = original_message.to_json()
40-
decoded_message = Test()
41-
decoded_message.from_json(json_serialized)
39+
decoded_message = Test.from_json(json_serialized)
4240

4341
# check that the timestamps are equal after decoding from dict
4442
assert original_message.ts.tzinfo is not None

tests/test_features.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -382,10 +382,8 @@ def test_bool():
382382
def test_iso_datetime():
383383
from tests.output_betterproto.features import TimeMsg
384384

385-
msg = TimeMsg()
386-
387385
for _, candidate in enumerate(iso_candidates):
388-
msg.from_dict({"timestamp": candidate})
386+
msg = TimeMsg.from_dict({"timestamp": candidate})
389387
assert isinstance(msg.timestamp, datetime)
390388

391389

tests/test_inputs.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,7 @@ def test_message_json(test_data: TestData) -> None:
169169
if sample.belongs_to(test_input_config.non_symmetrical_json):
170170
continue
171171

172-
message: betterproto2.Message = plugin_module.Test()
173-
174-
message.from_json(sample.json)
172+
message: betterproto2.Message = plugin_module.Test.from_json(sample.json)
175173
message_json = message.to_json(indent=0)
176174

177175
assert dict_replace_nans(json.loads(message_json)) == dict_replace_nans(json.loads(sample.json))

0 commit comments

Comments
 (0)