SelectField backward compatibility and minor QoL improvements#923
SelectField backward compatibility and minor QoL improvements#923azmeuk wants to merge 9 commits into
Conversation
|
Couldn't double check everything, but if we were extending a Field now we have to change from |
Daverball
left a comment
There was a problem hiding this comment.
Thanks for working on this, this looks pretty good and alleviates my concerns. I will try to take another deep dive later, to see if there is anything else you or I missed.
I did spot one small issue with the new callback, but that's an easy fix, it just adds a little bit more code.
No, you keep overriding iter_choices. _iter_choices_normalized is just a private helper that normalizes the iter_choices output to keep backward compatibility (and raise deprecation warnings). |
Daverball
left a comment
There was a problem hiding this comment.
Upon closer review I found a few additional subtle issues, but other than that I think we're good.
Daverball
left a comment
There was a problem hiding this comment.
I stand corrected, found a couple more things in the widget code.
Add Field._form and BaseForm._parent_form so fields and nested forms can reach the enclosing form. FieldList is transparent in the chain: a FormField nested in a FieldList points to the form that owns the list, not the list. Also fix FieldList entries which previously got _form=None.
Add Field.post_process() and BaseForm.post_process() hooks, invoked at the end of BaseForm.process() on the root form. FormField and FieldList propagate the hook to their nested form or entries, so every nested field's hook runs exactly once.
ea8013a to
7de39bd
Compare
9009bb2 to
6f91c47
Compare
|
I think I adressed most of your feedback. I went back to dataclasses with iter for the tuple compatibility, because this is hard to handle default values and inheritance with them. I tested on several dowstream projects and the unit test suites pass. |
The choices callable may optionally accept (form, field) as positional arguments, mirroring the validator signature. Resolved from post_process so it can read processed data from any field on the form.
DataList callable choices follow the same contract as SelectField: the callable accepts (form, field) or no arguments and is invoked once per form processing cycle from post_process.
SelectChoice / DataListChoice declare options for SelectField / DataList; Choice is the shape yielded by iter_choices and iter_groups. All three are dataclasses with __iter__ preserving the 3.2 tuple-unpacking contract.
Pipe iter_choices and iter_groups yields through _normalize_iter_choice with a DeprecationWarning so subclasses yielding 3.2-shaped tuples keep working until 4.0. The Select widget consumes the normalized iterators in both flat and grouped paths.
Don't coerce choices to SelectChoice at __init__; keep the user-supplied shape so subclasses iterating self.choices directly keep working. iter_choices still coerces per render. Legacy dict and raw tuple shapes emit a one-shot DeprecationWarning via _warn_legacy_choices.
Detect render_option overrides using the WTForms 3.2 signature (cls, value, label, selected, **kwargs) via _dispatch_render_option, adapt them to receive the new (cls, choice, **kwargs) signature, and emit a DeprecationWarning.
{value: label} for flat options, {label: {value: label}} for optgroups,
both forms mixable at the top level. _warn_legacy_choices treats this
shorthand as non-deprecated.
Thanks so much, I'll take another closer look later, but there's one thing I can already say now. Using dataclasses for class Choice(NamedTuple):
"""
A rendered option yielded by
:meth:`SelectFieldBase.iter_choices` and
:meth:`SelectFieldBase.iter_groups`.
``selected`` is computed against the field's current data. To
declare options on a :class:`SelectField`, use
:class:`SelectChoice` instead.
:param value:
The value that will be sent in the request.
:param label:
The label of the option.
:param selected:
Whether the option is currently selected. Set by ``iter_choices``;
you rarely set this yourself.
:param render_kw:
A dict containing HTML attributes that will be rendered
with the option.
"""
value: str
label: str
selected: bool
render_kw: dictThe main reason to use a I.e. compare for value, label, selected, render_kw in field.iter_choices():
do_something_with(value, label, selected)to for choice in field.iter_choices():
do_something_with(choice.value, choice.label, choice.selected)Neither code is obviously better than the other. Forcing people to switch their code to the latter, just to preserve good type checking experience seems like unnecessary downstream churn for no real benefit to anyone, when a |
I addressed most of the points raised in #922
post_processstep which happens right afterprocessformandfieldargs toSelectFieldandDataListchoices callbacks. Callbacks are resolved during thepost_processstep, so it can access the other fields.dataChoiceinherit fromNamedTupleinstead ofDataclass. An additional private_Choiceclass is needed to pass default values to the tuple members since NamedTuple forbids overriding newhas_groupsanditer_groupsiter_choicesreturning tuples, forrender_optionstakingvalue, label, selectedinstead ofchoice.The compatibility layer makes the code bigger and harder to read, but hopefully things goes better when we delete the deprecations in 3.4/4.0.
I have tested those modification in downstream projects (wtf-peewee, wtforms-alchemy, starlette-wtf, flask-appbuilder) and it solves all the issues related to SelectField.
/cc @Daverball