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

Add support for pydantic dataclasses #406

Merged
merged 30 commits into from
Feb 13, 2023
Merged

Add support for pydantic dataclasses #406

merged 30 commits into from
Feb 13, 2023

Conversation

SamuelYvon
Copy link
Contributor

Pydantic dataclasses are a drop-in replacement for builtin dataclasses
to offer a stronger type validation. This PR adds support for
generating pydantic dataclasses instead of the builtin variant.

This enable usage of protobuf classes in API endpoint (like FastAPI)
which supports parsing pydantic objects but not builtin dataclasses.

The `to_json` method does not include the `include_default_values`
option from `to_dict`. While the implementation to `to_json` is
almost litteraly only a call to `to_dict()` (and thus trivial to
replace), it is more convenient for users to not have to reimplement it
when the option is desired.
@SamuelYvon
Copy link
Contributor Author

SamuelYvon commented Aug 5, 2022

I am pitching this as an additional feature, feel free to let me know what you think. I will maintain my own fork otherwise; I do feel like this is a trivial improvement to implement.

edit
It was rather foolish to think it was trivial, it is not;

@SamuelYvon SamuelYvon marked this pull request as draft August 5, 2022 20:13
@SamuelYvon
Copy link
Contributor Author

SamuelYvon commented Aug 5, 2022

I'll put as draft for now. Some issues to note:

  • Does not play well with oneOf
  • ForwardRefs need to be updated

Right now the imports are done through the template, but it might be cleaner to do the pydantic imports through the builtin import system.

ForwardRefs are an easy fix, oneOf can be partially achieved through validators and overriding __post_init__ to set to None the fields that aren't set (otherwise a validation error from pydantic occurs)

@SamuelYvon
Copy link
Contributor Author

SamuelYvon commented Aug 5, 2022

Plan for the oneOf (with pydantic dataclasses):

  • Set each field part of a group to Optional
  • Set the message_field optional attribute to True
  • Add a pydantic validator (for the entire dataclass) that checks that at least one of the field is defined
    @root_validator()
    def check_oneof(cls, values):
        meta = cls._betterproto_meta.oneof_field_by_group

        for group, field_set in meta.items():
            if not any([values[field.name] is not None for field in field_set]):
                raise ValueError(f"Group {group} has no value; all fields are None")

        return values

@SamuelYvon
Copy link
Contributor Author

... that checks that at least one of the field is defined

Changed the validator to ensure that exactly one field is defined.

@SamuelYvon
Copy link
Contributor Author

SamuelYvon commented Aug 8, 2022

Further tasks

  • Only add the call to update the forward refs if the message has other messages as fields
  • Add tests

@SamuelYvon SamuelYvon marked this pull request as ready for review October 18, 2022 18:36
@SamuelYvon
Copy link
Contributor Author

Hey @danielgtaylor. I've completed these changes a while back. This code has been in production at Garda World for well over two months now without any specific incident. I just completed the unit test portion for windows (generation part). Let me know if this is something you are interested in having in the mainline version of python-betterproto. I did not add any specific test to protobuf but instead relied on use-cases; all previous tests were also ported so they are running on the protobuf version (insuring no regression)

cetanu
cetanu previously approved these changes Oct 19, 2022
Copy link
Collaborator

@cetanu cetanu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fine to me and seems like a safe addition, since the generation is simply controlled by an option.

Probably want to hear from another member to see what they think

@Kludex
Copy link

Kludex commented Feb 10, 2023

Hi there 👋

Anything I can do to help here? 🙏

@Gobot1234
Copy link
Collaborator

I'll try and take a look at this over the weekend

Copy link
Collaborator

@Gobot1234 Gobot1234 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good so far, it'd be nice if there was some documentation for this in the README.md instead of having to source dive for this

src/betterproto/plugin/models.py Outdated Show resolved Hide resolved
src/betterproto/plugin/models.py Outdated Show resolved Hide resolved
src/betterproto/templates/template.py.j2 Outdated Show resolved Hide resolved
src/betterproto/templates/template.py.j2 Outdated Show resolved Hide resolved
tests/util.py Outdated Show resolved Hide resolved
@Kludex
Copy link

Kludex commented Feb 13, 2023

@SamuelYvon are you still interested in this? If not, I can take over.

@SamuelYvon
Copy link
Contributor Author

@Kludex Ill finish the requested changes today :) We are actively using this at Garda so I have allocated time to finish this feature.

@Kludex
Copy link

Kludex commented Feb 13, 2023

Thinking a bit ahead... Would there be a way to create validators and not lose them when regenerating again?

@SamuelYvon
Copy link
Contributor Author

Thinking a bit ahead... Would there be a way to create validators and not lose them when regenerating again?

Ah! Great question. I personally think this is out of scope for a project like this, since the goal is to translate protobuf to python. The validation is enforced by the underlying protobuf model since this is a 1:1 translation (mainly to enforce types and groups). If I had other business logic to validate, I would add methods dynamically in a __init__.py file to perform the pydantic validation.

@SamuelYvon
Copy link
Contributor Author

@Gobot1234 Fixed all suggestions and added text in the README. Double checking that everything still works since I merged w/master.

Copy link
Collaborator

@Gobot1234 Gobot1234 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks pretty much ready to ship

README.md Outdated Show resolved Hide resolved
src/betterproto/plugin/models.py Outdated Show resolved Hide resolved
src/betterproto/templates/template.py.j2 Outdated Show resolved Hide resolved
src/betterproto/templates/template.py.j2 Outdated Show resolved Hide resolved
SamuelYvon and others added 5 commits February 13, 2023 10:22
Co-authored-by: James Hilton-Balfe <gobot1234yt@gmail.com>
Co-authored-by: James Hilton-Balfe <gobot1234yt@gmail.com>
Co-authored-by: James Hilton-Balfe <gobot1234yt@gmail.com>
@SamuelYvon
Copy link
Contributor Author

SamuelYvon commented Feb 13, 2023

@Gobot1234 All is good on my side; the merge w/master did not affect anything. If this ever need maintenance or issues pop-up, feel free to @ me and I'll be right there.

@Gobot1234
Copy link
Collaborator

Thanks for all your work on this

@Gobot1234 Gobot1234 merged commit 13d6565 into danielgtaylor:master Feb 13, 2023
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

Successfully merging this pull request may close these issues.

4 participants