Add support for recursive messages (#130)

Changes message initialization (`__post_init__`) so that default values
are no longer eagerly created to prevent infinite recursion when
initializing recursive messages.

As a result, `PLACEHOLDER` will be present in the message for any
uninitialized fields.  So, an implementation of `__get_attribute__` is
added that checks for `PLACEHOLDER` and lazily creates and stores
default field values.

And, because `PLACEHOLDER` values don't compare equal with zero values,
a custom implementation of `__eq__` is provided, and the code generation
template is updated so that messages generate with `@dataclass(eq=False)`.

Also add new Message __repr__ implementation that skips PLACEHOLDER 
values and orders keys by number from the proto.

Co-authored-by: Christopher Chambers <chris@peanutcode.com>
Co-authored-by: nat <n@natn.me>
Co-authored-by: James <50501825+Gobot1234@users.noreply.github.com>
This commit is contained in:
Chris Chambers
2020-08-30 15:04:36 -04:00
committed by GitHub
parent ca16b6ed34
commit 034e2e7da0
5 changed files with 125 additions and 10 deletions

View File

@@ -0,0 +1,12 @@
{
"name": "Zues",
"child": {
"name": "Hercules"
},
"intermediate": {
"child": {
"name": "Douglas Adams"
},
"number": 42
}
}

View File

@@ -0,0 +1,13 @@
syntax = "proto3";
message Test {
string name = 1;
Test child = 2;
Intermediate intermediate = 3;
}
message Intermediate {
int32 number = 1;
Test child = 2;
}

View File

@@ -317,3 +317,51 @@ def test_oneof_default_value_set_causes_writes_wire():
== betterproto.which_one_of(_round_trip_serialization(foo3), "group1")
== ("", None)
)
def test_recursive_message():
from tests.output_betterproto.recursivemessage import Test as RecursiveMessage
msg = RecursiveMessage()
assert msg.child == RecursiveMessage()
# Lazily-created zero-value children must not affect equality.
assert msg == RecursiveMessage()
# Lazily-created zero-value children must not affect serialization.
assert bytes(msg) == b""
def test_recursive_message_defaults():
from tests.output_betterproto.recursivemessage import (
Test as RecursiveMessage,
Intermediate,
)
msg = RecursiveMessage(name="bob", intermediate=Intermediate(42))
# set values are as expected
assert msg == RecursiveMessage(name="bob", intermediate=Intermediate(42))
# lazy initialized works modifies the message
assert msg != RecursiveMessage(
name="bob", intermediate=Intermediate(42), child=RecursiveMessage(name="jude")
)
msg.child.child.name = "jude"
assert msg == RecursiveMessage(
name="bob",
intermediate=Intermediate(42),
child=RecursiveMessage(child=RecursiveMessage(name="jude")),
)
# lazily initialization recurses as needed
assert msg.child.child.child.child.child.child.child == RecursiveMessage()
assert msg.intermediate.child.intermediate == Intermediate()
def test_message_repr():
from tests.output_betterproto.recursivemessage import Test
assert repr(Test(name="Loki")) == "Test(name='Loki')"
assert repr(Test(child=Test(), name="Loki")) == "Test(name='Loki', child=Test())"