python-betterproto icon indicating copy to clipboard operation
python-betterproto copied to clipboard

OneOf and empty Message issue

Open bw7715 opened this issue 3 years ago • 4 comments

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

bw7715 avatar Mar 28 '22 09:03 bw7715

Please can you test pip install betterproto --pre?

Gobot1234 avatar Mar 28 '22 11:03 Gobot1234

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

bw7715 avatar Mar 28 '22 11:03 bw7715

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.

redbmk avatar Aug 04 '23 19:08 redbmk