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

wtforms also allows a str as a valid choice of a SelectField #12203

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

jkittner
Copy link

As far as I can see, choices of a SelectField also support an Iterable of just a strs, not only a tuple[Any, str].

SelectField(choices=["foo", "bar"])

This is apparently called "shortcut" in the tests

This is also tested:
https://github.com/wtforms/wtforms/blob/8211fc854f91aef184b6fc8766d7dad14b198387/tests/fields/test_select.py#L141-L152

...and as a return value of a callable

https://github.com/wtforms/wtforms/blob/8211fc854f91aef184b6fc8766d7dad14b198387/tests/fields/test_selectmultiple.py#L61

I am not 100% sure that's the correct fix? Happy to hear your feedback - I'm a newbie at typeshed

Copy link
Contributor

According to mypy_primer, this change has no effect on the checked open source code. 🤖🎉

@jkittner
Copy link
Author

Mhm, not sure why it does not like Iterable[_Choice | str]

This seems to work (explicitly defining unions of tuple[str, ...] | list[str]), but it is quite ugly...

(not formatted with black so the diff is easier to see)

diff --git a/stubs/WTForms/wtforms/fields/choices.pyi b/stubs/WTForms/wtforms/fields/choices.pyi
index acbb01dc9..0c802cc50 100644
--- a/stubs/WTForms/wtforms/fields/choices.pyi
+++ b/stubs/WTForms/wtforms/fields/choices.pyi
@@ -7,7 +7,7 @@ from wtforms.form import BaseForm
 from wtforms.meta import DefaultMeta, _SupportsGettextAndNgettext
 
 # technically this allows a list, but we're more strict for type safety
-_Choice: TypeAlias = tuple[Any, str] | tuple[Any, str, dict[str, Any]] | str
+_Choice: TypeAlias = tuple[Any, str] | tuple[Any, str, dict[str, Any]]
 # it's too difficult to get type safety here due to to nested partially invariant collections
 _GroupedChoices: TypeAlias = dict[str, Any]  # Any should be Collection[_Choice]
 _FullChoice: TypeAlias = tuple[Any, str, bool, dict[str, Any]]  # value, label, selected, render_kw
@@ -51,7 +51,7 @@ class SelectField(SelectFieldBase):
         label: str | None = None,
         validators: tuple[_Validator[_FormT, Self], ...] | list[Any] | None = None,
         coerce: Callable[[Any], Any] = ...,
-        choices: Iterable[_Choice] | _GroupedChoices | Callable[[], Iterable[_Choice] | _GroupedChoices] | None = None,
+        choices: Iterable[_Choice] | list[str] | tuple[str, ...] | _GroupedChoices | Callable[[], Iterable[_Choice] | tuple[str, ...] | list[str] | _GroupedChoices] | None = None,
         validate_choice: bool = True,
         *,
         filters: Sequence[_Filter] = (),

@srittau
Copy link
Collaborator

srittau commented Jun 25, 2024

It seems that due to the new union element, pyright is not able to statically determine the type of the empty dict in the tests anymore. I don't know know why that is.

@Daverball
Copy link
Contributor

pyright's behavior seems very broken to me. Why is it reporting a dict instead of a Callable as the outermost type? Why is the key type _Choice? That's not one of the possible options. @erictraut Can you figure out what's happening here?

@erictraut
Copy link
Contributor

Pyright's behavior looks correct here to me. The parameter type is Iterable[_Choice]. You appear to have strict-mode type checking enabled, so pyright's reportUnknownArgumentType check is enabled. Let's take a look at a specific case where this error is triggered:

SelectField(choices={"a": (("", "", {}),)})

Here the specified value is an Iterable[str], which is assignable to Iterable[_Choice], but the value type argument for the dict cannot be fully inferred from either the bidirectional inference context (Iterable[str]) or from the value expression ((("", "", {}),)). The {} subexpression evaluates to a type of dict[Unknown, Unknown], which makes the resulting argument type "partially unknown".

@Daverball
Copy link
Contributor

Daverball commented Jun 26, 2024

That makes sense, I hadn't considered that through the addition of str, dict[str, Any] would pass as Iterable[str] and in turn Iterable[_Choice], so type checkers would be able to infer the type of the lambda based on that, rather than _GroupedChoices/dict[str, Any]. One could make the argument that a type checker should still infer based on dict[str, Any] since it is closer to dict than Iterable[str], but that of course quickly gets very complex.

I knew about the str shortcut for WTForms and I decided against including it when first writing these stubs because it seemed like a recipe for disaster and it appears my intuition was at least partially correct. We could potentially make this work with a separate overload for the str case, but that makes subclassing these fields far more annoying and it also makes the type alias less useful, so I'm not fully convinced it is worth it, unless all type checkers can pass the regression tests with the current, more simple change.

@srittau
Copy link
Collaborator

srittau commented Jun 26, 2024

Is passing an empty dict something that happens in real code? If it isn't, we could just adapt our tests. Otherwise I think using overloads is the only solution I see (other than using Any), as string shortcuts are seemingly a desired feature.

@Daverball
Copy link
Contributor

Daverball commented Jun 26, 2024

Is passing an empty dict something that happens in real code? If it isn't, we could just adapt our tests.

That's a fair point, it's not really useful to pass an empty dict, since it's the same as omitting it in the first place. I'd be fine with adapting the tests. Iterable[str] does worry me a little for other reasons, like accepting str or preventing flagging things like (('', '')) as an error, since it was meant to be a single blank choice rather than two blank choices.

EDIT: It would also start accepting arbitrary Mapping[str, ...], which may lead to confusion, since you will get the individual group names as choices rather than grouped choices.

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.

None yet

4 participants