Skip to content

Conversation

@sinisaos
Copy link
Member

An alternative approach to #434 . It uses Piccolo choices without complex parsing definitions in the Vue frontend.

@codecov-commenter
Copy link

codecov-commenter commented Jan 25, 2025

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.24%. Comparing base (1157c12) to head (3cb1f9a).
⚠️ Report is 47 commits behind head on master.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #436      +/-   ##
==========================================
+ Coverage   93.42%   94.24%   +0.81%     
==========================================
  Files           5        6       +1     
  Lines         365      417      +52     
==========================================
+ Hits          341      393      +52     
  Misses         24       24              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sinisaos sinisaos requested a review from dantownsend January 27, 2025 07:00
@sinisaos
Copy link
Member Author

@Skelmis I can't add you as a reviewer, but I'm interested in your thoughts on this approach of using Python enums in custom forms. Thanks in advance.

@Skelmis
Copy link
Contributor

Skelmis commented Jan 28, 2025

It looks like a much simpler implementation which I love. I'll try test it out within a week

@sinisaos
Copy link
Member Author

@Skelmis Great. Thanks

Copy link
Contributor

@Skelmis Skelmis left a comment

Choose a reason for hiding this comment

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

Tested and it works as is, my only issue is that using the example we are accessing an untyped field.

I.e.
data: NewStaffModel yet we need data.permissions which doesnt exist

Users could instead do something like this:

class NewStaffFormModel(BaseModel):
    username: str
    email: EmailStr
    superuser: bool

class NewStaffModel(NewStaffFormModel):
    permissions: Permission

def new_staff_endpoint(request: Request, data: NewStaffModel) -> str:
    print(type(data.permissions))
    return "A new staff member has been successfully created."


FORM = FormConfig(
    name="Enum form",
    pydantic_model=NewStaffFormModel,
    endpoint=new_staff_endpoint,
    description="Make a enum form.",
    choices={"permissions": Permission},
    form_group="Text forms",
)

But even in that case type(data.permissions) is str instead of Permissions.

Do you think it'd be possible to try return Enum items as the relevant enum in this case? Or should users do the enum conversion themselves

@sinisaos
Copy link
Member Author

sinisaos commented Feb 1, 2025

@Skelmis Thank you for your time to try this out.

Do you think it'd be possible to try return Enum items as the relevant enum in this case? Or should users do the enum conversion themselves

Yes, you are right. I think the easiest way, as you say, is for users to do the enum conversion themselves. Something like this

def new_staff_endpoint(request: Request, data: NewStaffModel) -> str:
    # data.permissions = Permission(int(data.permissions)) # for int enum
    data.permissions = Permission(data.permissions) # for str enum
    print(type(data.permissions)) # prints correct type <enum 'Permission'>
    return "A new staff member has been successfully created."

@Skelmis
Copy link
Contributor

Skelmis commented Feb 1, 2025

Seems reasonable for me. Thoughts @dantownsend ?

@Skelmis
Copy link
Contributor

Skelmis commented Mar 26, 2025

Hrmm perhaps just realised we should likely add docs haha. Will see if I find time sometime to try write some up

@sinisaos
Copy link
Member Author

@Skelmis Yes, you are right, but I don't see the point of writing docs because PR is not reviewed or accepted. We can add a usage example to the docs later, if PR is accepted.

@Skelmis
Copy link
Contributor

Skelmis commented Mar 26, 2025

Fair enough

@Skelmis
Copy link
Contributor

Skelmis commented Apr 4, 2025

@dantownsend what's your view on this pr? Docs before pr, or pr then docs on approved content?

Either way keen to get this moving, would love this feature

@github-actions
Copy link

github-actions bot commented May 7, 2025

This PR has been marked as stale because it has been open for 30 days with no activity. Are there any blockers, or should this be closed?

@github-actions github-actions bot added the Stale label May 7, 2025
@Skelmis
Copy link
Contributor

Skelmis commented May 7, 2025

@dantownsend what's your view on this pr? Docs before pr, or pr then docs on approved content?

Either way keen to get this moving, would love this feature

@dantownsend waiting on your response to move this PR forward

@github-actions github-actions bot removed the Stale label May 10, 2025
@dantownsend
Copy link
Member

Sorry for the delay on this. I was torn about which approach to take:

  1. Should we make our front end understand the refs in the OpenAPI schema.
  2. Should we add the extra attributes (like this in this PR).

There was also some discussion about not using OpenAPI schemas any more to drive the front end (because Litestar doesn't allow the extra attributes).

This PR might be the best approach for now though.

t.Union[FormResponse, t.Coroutine[None, None, FormResponse]],
],
description: t.Optional[str] = None,
choices: t.Optional[t.Dict[str, t.Any]] = None,
Copy link
Member

Choose a reason for hiding this comment

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

Rather than passing the choices in, can't we just look at the enum value on the pydantic model itself (i.e. if the default value for the field is an enum then use it to generate the choices).

Copy link
Member Author

Choose a reason for hiding this comment

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

Do you think something like this?

Code changes
def __init__(
      self,
      name: str,
      pydantic_model: t.Type[PydanticModel],
      endpoint: t.Callable[
          [Request, PydanticModel],
          t.Union[FormResponse, t.Coroutine[None, None, FormResponse]],
      ],
      description: t.Optional[str] = None,
      form_group: t.Optional[str] = None,
  ):
      self.name = name
      self.pydantic_model = pydantic_model
      self.endpoint = endpoint
      self.description = description
      self.form_group = form_group
      self.slug = self.name.replace(" ", "-").lower()
      for (
          field_name,
          field_value,
      ) in self.pydantic_model.model_fields.items():
          if isinstance(field_value.annotation, enum.EnumType):
              # update model_fields, field annotation and
              # rebuild the model for the changes to take effect
              pydantic_model.model_fields[field_name] = Field(
                  json_schema_extra={
                      "extra": {
                          "choices": convert_enum_to_choices(
                              field_value.annotation
                          )
                      }
                  },
              )
              pydantic_model.model_fields[field_name].annotation = str
              pydantic_model.model_rebuild(force=True)

Usage would be

Usage example
# An example of using Python enum in custom forms
class Permission(str, enum.Enum):
    admissions = "admissions"
    gallery = "gallery"
    notices = "notices"
    uploads = "uploads"


class NewStaffModel(BaseModel):
    username: str
    email: EmailStr
    superuser: bool
    permissions: Permission


def new_staff_endpoint(request: Request, data: NewStaffModel) -> str:
    # We need to do the enum type conversion ourselves like this:
    # data.permissions = Permission(int(data.permissions)) # for int enum
    # data.permissions = Permission(data.permissions)  # for str enum
    print(data)
    return "A new staff member has been successfully created."


FORM = FormConfig(
    name="Enum form",
    pydantic_model=NewStaffModel,
    endpoint=new_staff_endpoint,
    description="Make a enum form.",
    form_group="Text forms",
)

field_name,
field_value,
) in self.pydantic_model.model_fields.items():
if isinstance(field_value.annotation, ENUMTYPE):
Copy link
Member

Choose a reason for hiding this comment

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

Here can you use issubclass(field_value.annotation, enum.Enum) instead?

Copy link
Member Author

Choose a reason for hiding this comment

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

Note that the field value annotation is of type EnumType (before Python 3.11 it was EnumMeta), not of type enum.Enum and we need to compare the object with ENUMTYPE not enum.Enum. It's practically the same to use issubclass(type(field_value.annotation), ENUMTYPE) or if isinstance(field_value.annotation, ENUMTYPE), but I find the isinstance option a bit cleaner.

Copy link
Member

Choose a reason for hiding this comment

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

OK, I see what you're saying.

I think my preference is still this:

inspect.isclass(field_value.annotation) and issubclass(field_value.annotation, enum.Enum)

I find it a bit more intuitive, but now I understand yours I can see that it works fine.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, it works. Great.

}
},
)
pydantic_model.model_fields[field_name].annotation = str
Copy link
Member

Choose a reason for hiding this comment

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

Can't we just leave the annotation as what it was originally?

Copy link
Member Author

Choose a reason for hiding this comment

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

We need to reassign the annotation type because if we don't, that annotation is None and that causes a form validation error. I'm using the string type because if we reassign annotation to the Enum, frontend don't work (working with strings because choices display_value and value are strings, and choices does not work with Enum). In the whole PR I tried to use Piccolo choices so that there would be as few changes as possible on the frontend. If you don't like the whole concept, feel free to delete this PR.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, yes - I forgot that the front end would get confused by the open api schema is we leave the enum in there.

language_name="FARSI",
language_code="fa",
translations = {
translations={
Copy link
Member

Choose a reason for hiding this comment

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

Thanks for fixing these - I'm going to pull it out into a separate PR.

@github-actions
Copy link

This PR has been marked as stale because it has been open for 30 days with no activity. Are there any blockers, or should this be closed?

@github-actions github-actions bot added the Stale label Jul 10, 2025
@github-actions github-actions bot removed the Stale label Jul 28, 2025
@github-actions
Copy link

This PR has been marked as stale because it has been open for 30 days with no activity. Are there any blockers, or should this be closed?

@github-actions github-actions bot added the Stale label Aug 31, 2025
@Skelmis
Copy link
Contributor

Skelmis commented Sep 1, 2025

@dantownsend do you have any updates regarding this PR? Is it blocked as a result of technical basis or more so just personal time constraints

@github-actions github-actions bot removed the Stale label Sep 4, 2025
@github-actions
Copy link

github-actions bot commented Oct 7, 2025

This PR has been marked as stale because it has been open for 30 days with no activity. Are there any blockers, or should this be closed?

@github-actions github-actions bot added the Stale label Oct 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants