Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OneOf fields not typed as optional in constructor #580

Open
artificial-aidan opened this issue Mar 22, 2024 · 3 comments
Open

OneOf fields not typed as optional in constructor #580

artificial-aidan opened this issue Mar 22, 2024 · 3 comments

Comments

@artificial-aidan
Copy link
Contributor

Given this proto:

enum Enum {
    UNKNOWN=0;
    KNOWN=1;
}
message WithOneOfs {
    oneof optional_enum {
        Enum some_enum = 1;
    }
}

The constructor looks something like this:

    def __init__(
        self,
        *,
        some_enum: global___Enum.ValueType = ...,
    ) -> None: ...

But the OneOf block is effectively marking the some_enum field as optional, but type checkers error when you try and pass None to the constructor.

@nipunn1313
Copy link
Owner

nipunn1313 commented Mar 26, 2024

In the mypy-protobuf generator, we have this

for field in constructor_fields:
    field_type = self.python_type(field, generic_container=True)
    if self.fd.syntax == "proto3" and is_scalar(field) and field.label != d.FieldDescriptorProto.LABEL_REPEATED and not self.relax_strict_optional_primitives and not field.proto3_optional:
        wl(f"{field.name}: {field_type} = ...,")
    else:
        wl(f"{field.name}: {field_type} | None = ...,")

so it looks pretty explicit that we don't allow passing None in when the field is scalar.

We have some tests of this here - that show that protos give TypeError when you pass in None for a scalar field. Hence only the message fields (not scalar) are optional.
https://github.com/nipunn1313/mypy-protobuf/blob/main/test/test_generated_mypy.py#L421

I don't actually know what happens for enums inside oneof. We have such a thing in the tests, but not in the runtime test cases.
https://github.com/nipunn1313/mypy-protobuf/blob/main/proto/testproto/test3.proto#L39

I think step one is figuring out what the runtime behavior is for enums inside oneof - do they operate more like messages or like scalars. Can do this by adding a test case here
https://github.com/nipunn1313/mypy-protobuf/blob/main/test/test_generated_mypy.py#L421

Once we know how it should behave, we can decide how to proceed.

Thank you for the report!

@artificial-aidan
Copy link
Contributor Author

So here is some exploration I did. Interestingly I was able to pass None in for all of the fields, scalar or enum, or oneof. But as expected, if you set the enum in the oneof to None, then the oneof did not get set.

    x = SimpleProto3(a_string=None, outer_enum_in_oneof=None,  # type: ignore
                     inner_enum=None)  # type: ignore
    assert x.a_string == ''
    assert x.outer_enum_in_oneof == 0
    assert x.WhichOneof('a_oneof') is None
    assert x.inner_enum == 0
    assert x.inner_enum_in_oneof == 0

    x = SimpleProto3(a_string="hello", outer_enum_in_oneof=FOO3)
    assert x.a_string == 'hello'
    assert x.outer_enum_in_oneof == FOO3
    assert x.WhichOneof('a_oneof') == 'outer_enum_in_oneof'

I think technically anything inside a oneof can be set to None which results in the oneof having nothing set.

@nipunn1313
Copy link
Owner

ok interesting!

Cool find - seems like the behavior is slightly different when the scalar field is part of a oneof vs in the message.

Would take a fix along with a unit test that proves that the types match the runtime behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants