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

fix: 'Bill Type: Invalid Choice: could not coerce' error #1294

Merged
merged 1 commit into from
Mar 28, 2024

Conversation

rriski
Copy link
Contributor

@rriski rriski commented Mar 23, 2024

Error introduced in #1290. Fixes #1293. WTForms needs to be bumped to >=2.3.2
as it includes a fix to SelectField which is required for this change to work.

See:

@rriski rriski changed the title fix: 'Bill Type: Invalid Choice: could not coerce.' error fix: 'Bill Type: Invalid Choice: could not coerce' error Mar 23, 2024
Comment on lines 54 to 55

class BillType(Enum):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is a FormEnum helper here

class FormEnum(Enum):
"""Extend builtin Enum class to be seamlessly compatible with WTForms"""
@classmethod
def choices(cls):
return [(choice, choice.name) for choice in cls]
@classmethod
def coerce(cls, item):
"""Coerce a str or int representation into an Enum object"""
if isinstance(item, cls):
return item
# If item is not already a Enum object then it must be
# a string or int corresponding to an ID (e.g. '0' or 1)
# Either int() or cls() will correctly throw a TypeError if this
# is not the case
return cls(int(item))
def __str__(self):
return str(self.value)
but we can't use it since it expects int values for the extending class.

Copy link
Contributor

Choose a reason for hiding this comment

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

Indeed I also found this while adding the enum, but I wanted to have an Enum with string values to easily show a "nice" string to the user.

@classmethod
def coerce(cls, item):
try:
return item if isinstance(item, BillType) else BillType[item.upper()]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Need item.upper() because the API expects that bill_type is passed as the enum value (i.e. "Expense" or "Reimbursement") but item is "EXPENSE" or "REIMBURSEMENT" when its passed as string to coerce.

@TomRoussel
Copy link
Contributor

I've been playing around with the changes in this PR. To me it seems like BillType.coerce in unnecessary. We can just use the enum constructor for this. BillType("Expense") correctly returns BillType.Expense.

I don't really understand why, but I think the key change that fixes the error in the form is adding BillType.__str__?

@TomRoussel
Copy link
Contributor

I quickly pushed my change to explain what I mean: See this commit.

When I run the server with this change, I also don't get the error message.

@TomRoussel
Copy link
Contributor

Okay, I figured out what's going on. The WTForms SelectField sends the different choices to the browser as strings (documented here. These are specified by BillType.choices() as a list of (value, label) tuples. The value should be the actual enum instance, and label would then be what the user sees. Since WTForms encodes everything as a string, it also sends this value to the browser as str(value). For an ordinary enum, this becomes "BillType.EXPENSE" for example. When the browser then sends the post request, it uses that string, but that's not understood by the coerce function.

That's why adding an explicit BillType.__str__ fixes the problem. It's actually the equivalent as modifying choices as follows:

    @classmethod
    def choices(cls):
        return [(choice.value, choice.value) for choice in cls]

@rriski
Copy link
Contributor Author

rriski commented Mar 24, 2024

Okay, I figured out what's going on. The WTForms SelectField sends the different choices to the browser as strings (documented here. These are specified by BillType.choices() as a list of (value, label) tuples. The value should be the actual enum instance, and label would then be what the user sees. Since WTForms encodes everything as a string, it also sends this value to the browser as str(value). For an ordinary enum, this becomes "BillType.EXPENSE" for example. When the browser then sends the post request, it uses that string, but that's not understood by the coerce function.

That's why adding an explicit BillType.__str__ fixes the problem. It's actually the equivalent as modifying choices as follows:

    @classmethod
    def choices(cls):
        return [(choice.value, choice.value) for choice in cls]

Thanks, I have updated the PR to use:

@classmethod
    def choices(cls):
        return [(choice.value, choice.value) for choice in cls]

@TomRoussel
Copy link
Contributor

That looks good to me! Ah nice, you also caught the duplicate tests, I just saw those today.

@zorun
Copy link
Collaborator

zorun commented Mar 25, 2024

Thanks for the fix.

Our CI was failing, which means that unformatted code went through in the last PR. I have fixed the issue in #1296 and hopefully reorganized the CI to avoid this in the future.

It does mean that you have to rebase and possibly fix conflicts, sorry about that. But we should then get a clearer view of the CI.

@rriski rriski force-pushed the fix-billtype branch 2 times, most recently from fb0111c to f7bbe83 Compare March 26, 2024 13:59
@zorun
Copy link
Collaborator

zorun commented Mar 26, 2024

The testcases with the lower versions for dependency are failing with HTTP 400 errors. It means that the approach doesn't work with older version of some libraries (here, most probably wtforms)

Does your solution rely on a undocumented behaviour in wtforms? If so, it's preferable to find another solution that will be less fragile over time.

If there is really no other workable approach, you can try bumping the minimum version of wtforms.

@TomRoussel
Copy link
Contributor

Very strange, as far as I can tell, this solution is not using anything exotic from wtforms. All api calls are following the documented way of working for 2.3.x of wtforms, which should correspond to the minimal version. What I think is strange is that this test is breaking now, but the minimal version checks in #1290 were fine.

I can try replicating the problem when I have time later this week.

@TomRoussel
Copy link
Contributor

I found the culprit I think. I believe the problem is related to a bug fixed in a PR of WTForms here. The changelog for 2.3.2 mentions that they fixed a bug in the SelectField.choices field. Using WTForms 2.3.1 fails, but 2.3.2 succeeds. Reading the PR description, I'm really not sure why our implementation here doesn't work. The only reason I can think of is that their validation logic for 2.3.1 somehow breaks if the choices tuples are identical values?

Is see 2 solutions to this problem, either we do a minor version bump for WTForms from 2.3.1 --> 2.3.2, or we use the following workaround which also seems to do the trick:

class BillType(Enum):
    EXPENSE = "Expense"
    REIMBURSEMENT = "Reimbursement"

    def __str__(self):
        return self.value

    @classmethod
    def choices(cls):
        return [(choice, choice.value) for choice in cls]

Using a BillType instance as the first member of the choice tuple seems to not trigger the validation bug, while the str method makes sure flasks sends a sensible value to the browser which can then be coerced back to the enum.

By the way, sorry @rriski that you got involved in this weird can of worms from my implementation!

@zorun
Copy link
Collaborator

zorun commented Mar 27, 2024

Thanks for looking at it!

If it's a bug in wtforms, then let's use the simplest solution: @rriski , can you bump the wtforms dependency to >=2.3.3 ?

Error introduced in spiral-project#1290. Fixes spiral-project#1293. WTForms needs to be bumped to >=2.3.2
as it includes a fix to `SelectField` which is required for this change to work.

See:
  - https://wtforms.readthedocs.io/en/3.1.x/changes/#version-2-3-2
  - wtforms/wtforms#598
@rriski
Copy link
Contributor Author

rriski commented Mar 28, 2024

Thanks for looking at it!

If it's a bug in wtforms, then let's use the simplest solution: @rriski , can you bump the wtforms dependency to >=2.3.3 ?

Sure, updated PR now. Thanks @TomRoussel for figuring out the issue.

@zorun zorun merged commit a3d4e42 into spiral-project:master Mar 28, 2024
21 checks passed
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.

Bill Type: Invalid Choice: could not coerce.
3 participants