danielgtaylor / python-betterproto

Clean, modern, Python 3.6+ code generator & library for Protobuf 3 and async gRPC
MIT License
1.5k stars 210 forks source link

OneOf and empty Message issue #360

Open bw7715 opened 2 years ago

bw7715 commented 2 years ago

When try to use empty Message (Message without fields) in one_of group, I cannot convert it to dict or bool.

Code to reproduce:

syntax = "proto3";

message CommandGetInfo
{}

message CommandGetName
{}

message Test {
  oneof foo {
    bool on = 1;
    int32 count = 2;
    string name = 3;
    CommandGetInfo get_info = 4;
    CommandGetName get_name = 5;
  }
}
import betterproto
from oneof import Test, CommandGetInfo, CommandGetName

def print_one_of(msg: Test):
    def _to_bool(_value) -> bool:
        return True if _value else False
    msg_one_of = betterproto.which_one_of(msg, "foo")
    print(f"msg={msg}")
    print(f"msg_one_of={msg_one_of}")
    print(f"msg to dict={msg.to_dict()}")
    print(f"to bool test:")
    print(f"\tmsg.on={_to_bool(msg.on)}")
    print(f"\tmsg.count={_to_bool(msg.count)}")
    print(f"\tmsg.name={_to_bool(msg.name)}")
    print(f"\tmsg.get_info={_to_bool(msg.get_info)}")
    print(f"\tmsg.get_name={_to_bool(msg.get_name)}")
    print(f"to bytes: {bytes(msg)}")
    print("\n")

test = Test()
print_one_of(msg=test)

test.get_name = CommandGetName()
print_one_of(msg=test)

test.on = True
print_one_of(msg=test)

test.count = 57
print_one_of(msg=test)

test.name = "Name"
print_one_of(msg=test)

test.get_info = CommandGetInfo()
print_one_of(msg=test)

Output:

msg=Test(on=False, count=0, name='', get_info=CommandGetInfo(), get_name=CommandGetName())
msg_one_of=('', None)
msg to dict={}
to bool test:
    msg.on=False
    msg.count=False
    msg.name=False
    msg.get_info=True
    msg.get_name=True
to bytes: b''

msg=Test(on=False, count=0, name='', get_info=CommandGetInfo(), get_name=CommandGetName())
msg_one_of=('get_name', CommandGetName())
msg to dict={}
to bool test:
    msg.on=False
    msg.count=False
    msg.name=False
    msg.get_info=True
    msg.get_name=True
to bytes: b''

msg=Test(on=True, count=0, name='', get_info=CommandGetInfo(), get_name=CommandGetName())
msg_one_of=('on', True)
msg to dict={'on': True}
to bool test:
    msg.on=True
    msg.count=False
    msg.name=False
    msg.get_info=True
    msg.get_name=True
to bytes: b'\x08\x01'

msg=Test(on=False, count=57, name='', get_info=CommandGetInfo(), get_name=CommandGetName())
msg_one_of=('count', 57)
msg to dict={'count': 57}
to bool test:
    msg.on=False
    msg.count=True
    msg.name=False
    msg.get_info=True
    msg.get_name=True
to bytes: b'\x109'

msg=Test(on=False, count=0, name='Name', get_info=CommandGetInfo(), get_name=CommandGetName())
msg_one_of=('name', 'Name')
msg to dict={'name': 'Name'}
to bool test:
    msg.on=False
    msg.count=False
    msg.name=True
    msg.get_info=True
    msg.get_name=True
to bytes: b'\x1a\x04Name'

msg=Test(on=False, count=0, name='', get_info=CommandGetInfo(), get_name=CommandGetName())
msg_one_of=('get_info', CommandGetInfo())
msg to dict={}
to bool test:
    msg.on=False
    msg.count=False
    msg.name=False
    msg.get_info=True
    msg.get_name=True
to bytes: b''

We can see that bool values are always set to True for empty proto messages, serializing them to dictionary gives empty dictionary, serializing to bytes also gives empty value.

I tested on python versions 3.9 and 3.10. Betterproto: 1.2.5

Gobot1234 commented 2 years ago

Please can you test pip install betterproto --pre?

bw7715 commented 2 years ago

With betterproto version 2.0.0b4 to_dict works, but unlike version 1.2.4 the field as bool is always set to False for empty proto messages:

msg=Test(on=False, count=0, name='', get_info=CommandGetInfo(), get_name=CommandGetName())
msg_one_of=('', None)
msg to dict={}
to bool test:
    msg.on=False
    msg.count=False
    msg.name=False
    msg.get_info=False
    msg.get_name=False
to bytes: b''

msg=Test(on=False, count=0, name='', get_info=CommandGetInfo(), get_name=CommandGetName())
msg_one_of=('get_name', CommandGetName())
msg to dict={'getName': {}}
to bool test:
    msg.on=False
    msg.count=False
    msg.name=False
    msg.get_info=False
    msg.get_name=False
to bytes: b'*\x00'

msg=Test(on=True, count=0, name='', get_info=CommandGetInfo(), get_name=CommandGetName())
msg_one_of=('on', True)
msg to dict={'on': True}
to bool test:
    msg.on=True
    msg.count=False
    msg.name=False
    msg.get_info=False
    msg.get_name=False
to bytes: b'\x08\x01'

msg=Test(on=False, count=57, name='', get_info=CommandGetInfo(), get_name=CommandGetName())
msg_one_of=('count', 57)
msg to dict={'count': 57}
to bool test:
    msg.on=False
    msg.count=True
    msg.name=False
    msg.get_info=False
    msg.get_name=False
to bytes: b'\x109'

msg=Test(on=False, count=0, name='Name', get_info=CommandGetInfo(), get_name=CommandGetName())
msg_one_of=('name', 'Name')
msg to dict={'name': 'Name'}
to bool test:
    msg.on=False
    msg.count=False
    msg.name=True
    msg.get_info=False
    msg.get_name=False
to bytes: b'\x1a\x04Name'

msg=Test(on=False, count=0, name='', get_info=CommandGetInfo(), get_name=CommandGetName())
msg_one_of=('get_info', CommandGetInfo())
msg to dict={'getInfo': {}}
to bool test:
    msg.on=False
    msg.count=False
    msg.name=False
    msg.get_info=False
    msg.get_name=False
to bytes: b'"\x00'

Process finished with exit code 0
redbmk commented 1 year ago

I think this is fixed in 2.0.0b6, but appears to be broken with pydantic_dataclasses enabled. AFAICT it's doing some extra validation twice - once when creating the Message, and once when serializing the message. The first one works, but the second fails and throws an error.

jaceksan commented 3 weeks ago

I am affected as well. For now, I disabled the Pydantic data classes. I would like to enable them as soon as possible, they are really helpful. I haven't found a workaround solution yet.

My case:

message MetadataObjectProperties {
    message Empty {}

    message VisualizationObject {
        string content = 1;
    }
....
    oneof properties {
        Empty empty = 1;
        VisualizationObject visualization_object = 2;
....

I also tried to inject optional string empty = 1; into Empty, but it didn't help.