Make ValueElement and ValueChangeEventArguments generic#5785
Make ValueElement and ValueChangeEventArguments generic#5785evnchn wants to merge 26 commits intozauberzeug:mainfrom
Conversation
Add Generic[V] to ValueElement so type checkers can infer the correct type for .value on every element subclass (e.g. Input.value -> str, Slider.value -> float | None). Parameterize all ~35 leaf element subclasses and propagate V through ValidationElement and ChoiceElement. Ref: zauberzeug#2677
Add Generic[ValueT] to ValueChangeEventArguments so type checkers can infer the correct type for e.value and e.previous_value in change handler callbacks. Update all Handler[ValueChangeEventArguments] signatures across ~35 element files to use the parameterized form.
Why
|
| Factor | Assessment |
|---|---|
| Conceptual complexity | Very high — "descriptor + owner generic" inference is unsolved |
| Python type system support | Insufficient — no PEP covers this |
| Type checker support | Partial (pyright has heuristics, mypy doesn't) |
| Scope if attempted | 15+ mixin classes with their own BindableProperty |
| Benefit over current workaround | Marginal — TYPE_CHECKING override already works |
Recommendation
Don't pursue. A proper solution requires a future PEP for descriptor-generic inference. The same TYPE_CHECKING pattern could optionally be applied to other BindableProperty usages (text: str, label: str, visible: bool, enabled: bool, etc.) in a follow-up PR for completeness.
|
I'll sit until 3.9 until getting around to fix the mypy. If anyone want to take a shot, feel free to do so (open a PR against my branch, I suppose) |
* Initial plan * Fix mypy errors in value_element, selectable_element, and drawer Co-authored-by: evnchn <37951241+evnchn@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: evnchn <37951241+evnchn@users.noreply.github.com>
|
Yeah indeed this is OK for review. I forgot that I asked copilot to fix the mypy errors. |
falkoschindler
left a comment
There was a problem hiding this comment.
Thanks for the pull request, @evnchn! The overall approach is sound, but replacing Any with concrete types is deceptively subtle — each element's actual runtime value type must be carefully verified, not just inferred from its __init__ signature. Here are the cases that need attention. (Note that Claude found none of them, but - of course - helped to write them down.)
-
Some value types don't account for
Nonefrom QuasarElements like
Input,ColorInput,DateInput,TimeInputare typed asValueElement[str], but at runtime Quasar sendsNonewhen a clearable input is cleared. So.valuebecomesNoneeven though the type saysstr.For example,
i = ui.input().props('clearable')— after clearing,type(i.value)isNoneType. The type parameter should bestr | Noneto reflect reality, or the element needs to coerceNoneback to''in_event_args_to_value.This applies to any element where the Vue component can emit
nullbut the type parameter claims otherwise. Worth auditing all leaf elements to ensure the type parameter matches what actually happens at runtime, not just what__init__accepts. -
Elements like
Carouselshould usestr | None, notAnyCarousel,Stepper,Tabs, andTabPanelsuseValueElement[Any]but their.valueis alwaysstr | Noneat runtime (element references get converted to name strings in_value_to_model_value). They should beValueElement[str | None]withset_valueoverridden to also accept the element type, e.g.set_value(self, value: str | CarouselSlide | None). This keeps.valuecorrectly typed while still allowing element references as input. -
TreeandTablecallbacks useAnybut have known concrete typesTreeandTabledon't extendValueElement— they manage their own handlers and constructValueChangeEventArgumentsdirectly. Each callback has a distinct, known value type:Tree.on_select:str | None(single node key)Tree.on_expand:list[str](expanded node keys)Tree.on_tick:list[str](ticked node keys)Table.on_pagination_change:dict(pagination object)
All currently use
ValueChangeEventArguments[Any]. They should use their concrete types. -
Default value for inputs:
''vsNoneCurrently
ui.input()defaults tovalue=''. ButNonewould be more semantically correct — "no value yet" is different from "user entered an empty string." It also aligns with how Quasar already treats cleared inputs (sendsnull). WithNoneas default, the type naturally becomesstr | Noneand clearable behavior is no longer a special case.This is a breaking change (code like
ui.input().value.upper()would need a None check), so it belongs in NiceGUI 4.0. -
Line length violations
Some files like
selectable_element.pyhave lines that exceed the project's line length limit. Since the changes were likely made with AI tooling, VS Code's auto-formatter didn't run on save. Either all changed files need to be opened and saved, or a pre-commit hook should enforce formatting automatically. -
Rename
VtoValueTThe TypeVar
Vinvalue_element.pyis imported from other files likevalidation_element.py. Since it crosses module boundaries, a more descriptive name likeValueTwould be clearer — and consistent with theValueTalready used inevents.py.
) * Address review: rename V to ValueT, fix None coercion, concrete types for Tree/Table - Rename TypeVar V to ValueT in value_element.py, import from events.py for consistency - Update validation_element.py to use ValueT - Carousel/Stepper/Tabs/TabPanels: use ValueElement[str | None] with set_value overrides - Tree: concrete types for on_select (str | None), on_expand/on_tick (list[str]) - Table: concrete type for on_pagination_change (dict) - Input/ColorInput/DateInput/TimeInput/Editor: coerce None to empty string in _event_args_to_value Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix: use str | None type instead of coercing None for clearable inputs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: evnchn <evnchn@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ateInput, TimeInput Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nt[str | None] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ue_to_event_value returns ValueT Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
None is accepted as input but coerced to [] before storing, so the actual .value is always list[str]. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Accept element objects (CarouselSlide, Step, Tab, TabPanel) in the ValueElement type parameter so .value typing is honest. This removes the need for set_value overrides and __init__ pre-conversions since _value_to_model_value handles all conversion to strings for the frontend. Split CarouselSlide, Step, StepperNavigation, Tab, TabPanel, and TabPanels into separate files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The value type depends on runtime props (range, multiple), similar to how ChoiceElement values depend on runtime options. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Motivation
Closes #2677.
Type checkers currently infer
Anyfor.valueon everyValueElementsubclass and fore.value/e.previous_valuein change handler callbacks. This makes static analysis ineffective for the most commonly accessed property in NiceGUI.This PR makes
ValueElementgeneric (ValueElement[V]) andValueChangeEventArgumentsgeneric (ValueChangeEventArguments[ValueT]) so that type checkers can properly infer value types — e.g.ui.input().value→str,ui.slider().value→float | None,ui.switch().value→bool | None.Implementation
Commit 1 — Make
ValueElementgeneric with TypeVarV:V = TypeVar('V')andGeneric[V]base class toValueElementTYPE_CHECKING-onlyvalue: Vannotation to override theBindablePropertydescriptor for type checkers (the descriptor continues to work at runtime)set_value,_handle_value_change,_event_args_to_value,bind_value_to/from, etc.) to useVValidationElementpropagateV(ValidationElement(ValueElement[V]))ChoiceElementtoAnysince choice values are runtime-dependentCommit 2 — Make
ValueChangeEventArgumentsgeneric with TypeVarValueT:ValueT = TypeVar('ValueT')andGeneric[ValueT]to the dataclassvalueandprevious_valuefields asValueTHandler[ValueChangeEventArguments]annotations across all elements to carry the correct type parameterKey design decisions:
BindablePropertyis not made generic — Python's descriptor protocol cannot infer the return type from the owner's generic parameter. TheTYPE_CHECKINGannotation override is the standard workaround.Date,Tabs,Carousel,Stepper,Select, etc.) useAnyas their type parameter, which preserves the current behavior without making incorrect type claims.Generic[V]has no runtime effect, and theTYPE_CHECKINGblock is never executed.Progress