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 Typeshare support for Python #169

Open
wants to merge 60 commits into
base: main
Choose a base branch
from

Conversation

hculea
Copy link
Member

@hculea hculea commented Apr 30, 2024

This PR is adding initial support for Python in Typeshare (gated by a feature flag, like Go).

See @MOmarMiraj 's comment for a high-level description of the design considerations behind this work.

This was made possible thanks to the prior work of @adriangb: #25.

@hculea hculea marked this pull request as draft April 30, 2024 13:24
@kareid kareid requested a review from Lucretiel May 1, 2024 15:04
@hculea hculea marked this pull request as ready for review May 28, 2024 15:01
core/Cargo.toml Outdated
@@ -14,6 +14,9 @@ thiserror = "1"
itertools = "0.12"
lazy_format = "2"
joinery = "2"
topological-sort = { version = "0.2.2"}
once_cell = { version = "1"}
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason to not use the built-in type? https://doc.rust-lang.org/stable/std/sync/struct.LazyLock.html

@Lucretiel
Copy link
Contributor

Doing some review, sorry for the delay. I'd like to ask for a high-level description in this MR for how this integration works, with especial focus on how structs are handled (in particular how the JSON <-> struct boundary is handled).

@MOmarMiraj MOmarMiraj force-pushed the hculea/add-python-support-in-typeshare branch from f4540ab to 85634d4 Compare July 31, 2024 19:42
@MOmarMiraj MOmarMiraj force-pushed the hculea/add-python-support-in-typeshare branch from 85634d4 to 48a8adb Compare July 31, 2024 19:42
@amandayu1 amandayu1 force-pushed the hculea/add-python-support-in-typeshare branch from fbfde61 to 5c88f19 Compare August 22, 2024 18:16
@amandayu1 amandayu1 force-pushed the hculea/add-python-support-in-typeshare branch 3 times, most recently from c901b1b to 9935aaf Compare August 22, 2024 20:19
@MOmarMiraj
Copy link
Contributor

Forgot to mention but we currently do not support generics as we have yet to figure out on an ideal/idiomatic way to handle them in Python so we left it out of this PR.

@hculea
Copy link
Member Author

hculea commented Nov 4, 2024

Thanks for the mention @MOmarMiraj 😄 That should be okay while Python support is still experimental (FWIW, Go doesn't support generics either)

@hculea
Copy link
Member Author

hculea commented Nov 4, 2024

@Lucretiel does @MOmarMiraj 's comment above answer to your high-level description request? Otherwise, we're happy to provide more context on the choices that were made here. Thanks!

@hculea hculea requested a review from Lucretiel November 4, 2024 10:37
Copy link
Contributor

@Lucretiel Lucretiel left a comment

Choose a reason for hiding this comment

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

Mostly this looks good. I had some concerns about algebraic enum representations, along with a few edge cases (mostly around serde renames) that appear to be handled incorrectly. Rust side of this looks fine, if quite allocation-heavy; I'll leave it be for now but there's a lot of low-hanging fruit for improvement if typeshare-python needs to be faster.

core/Cargo.toml Outdated
@@ -14,6 +14,9 @@ thiserror = "1"
itertools = "0.12"
lazy_format = "2"
joinery = "2"
topological-sort = { version = "0.2.2"}
once_cell = { version = "1"}
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason to not use the built-in type? https://doc.rust-lang.org/stable/std/sync/struct.LazyLock.html

core/data/tests/anonymous_struct_with_rename/output.py Outdated Show resolved Hide resolved
core/data/tests/anonymous_struct_with_rename/output.py Outdated Show resolved Hide resolved
core/data/tests/anonymous_struct_with_rename/output.py Outdated Show resolved Hide resolved
core/data/tests/can_apply_prefix_correctly/output.py Outdated Show resolved Hide resolved


class MyType(BaseModel):
field: str
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider constr to constrain the length here, since it's known to be a char https://docs.pydantic.dev/1.10/usage/types/#arguments-to-constr

Choose a reason for hiding this comment

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

Will be using Annotated with Field instead.

Comment on lines 9 to 17
OptionalU16 = int


OptionalU32 = int


class FooBar(BaseModel):
foo: OptionalU32
bar: OptionalU16
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks very wrong to me– the relevant rust type is Option<u32>

Choose a reason for hiding this comment

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

Fixed

"""
This is a comment.
"""
a: int
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: I don't feel nearly as strongly about this as I do about by other feedback, but if you wanted to be fancy, you could use conint to handle fixed-width integers with a min and max value: https://docs.pydantic.dev/1.10/usage/types/#arguments-to-conint

Choose a reason for hiding this comment

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

Will be using Annotated with Field instead, like so: Annotated[int, Field(strict=True, gt=0)]

// used by each individual field
// We do this by exploring each field's type and comparing against the generics used by the enum
// itself
fn collect_generics_for_variant(variant_type: &RustType, generics: &[String]) -> Vec<String> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: consider using a wrapper that pushes to an &mut Vec<String> here, to avoid the deep recursive frequent allocations and data copying between vectors. Also consider returning a HashSet or BTreeSet from this function.

Choose a reason for hiding this comment

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

This function is unused from the original contributor. This PR won't cover generics so I'm keeping the function around for future references.

I will add a comment a mark the as unused.

@Lucretiel
Copy link
Contributor

Follow-up: it looks like most of the pydantic con* types (conlist, constr, etc) are deprecated, in favor of Field and Annotated, so substitute all my recommendations as appropriate.

@Lucretiel
Copy link
Contributor

As a subsequent follow up, one idea for a way to handle unions would resemble this:

class MyEnumVariant1:
    type: Literal["Variant1"]
    content: MyEnumVariant1Content

...

MyEnum = Union[MyEnumVariant1, ...]

I'm still a novice around pydantic, so there could certainly be a better way that I'm not aware of. For all I know this pattern is fully built-in to pydantic somewhere and I just don't know where.

@CheatCod
Copy link

As a subsequent follow up, one idea for a way to handle unions would resemble this:

class MyEnumVariant1:
    type: Literal["Variant1"]
    content: MyEnumVariant1Content

...

MyEnum = Union[MyEnumVariant1, ...]

I'm still a novice around pydantic, so there could certainly be a better way that I'm not aware of. For all I know this pattern is fully built-in to pydantic somewhere and I just don't know where.

After some experimentation and discussion with @MOmarMiraj, I think this is the best approach for externally tagged enums. I've refactored the PR to do so.

KEBAB_CASE = "kebabCase"

class AnonymousStructWithRenameList(BaseModel):
type: AnonymousStructWithRenameTypes = AnonymousStructWithRenameTypes.LIST
Copy link
Contributor

Choose a reason for hiding this comment

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

It's possible I'm misunderstanding pydantic, but shouldn't this be a Literal parameter? I had understood this = to just set a default value.

https://docs.pydantic.dev/2.0/usage/types/unions/

Choose a reason for hiding this comment

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

Fixed

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.

6 participants