reactpy
reactpy copied to clipboard
Review User API Type Hints
Current Situation
ReactPy can generate type hinting errors generated (from Pylance).
For example, the fact that reactpy.component can ingest a ComponentType, but can only return a Component. Or the fact that use_state can be set to None initially and changed later.
Proposed Actions
Review user API type hints and fix where needed.
I think idom.component is typed correctly. It's input function is a render function which may return ComponentType | VdomDict | None and its output is a constructor for a Component object. The distinction between Component and ComponentType is that the latter is a protocol, an abstract interface. Component on the other hand is a concrete implementation of ComponentType. ContextProvider is another example of a ComponentType.
For use_state, if its value is initially None but may also be some other type (e.g. a str) then you must explicitly declare this upfront.
initial_value: str | None = None
value, set_value = use_state(initial_value)
# Or cast initial value
from typing import cast
value, set_value = use_state(cast(str | None, None))
# Or declare state, set_state upfront
value: str | None
set_value: Callable[[str | None], None]
value, set_value = use_state(None)
Admittedly, while these solutions work, I think they all feel a bit awkward. Here are two alternatives:
# Use a named tuple
state: State[str | None] = use_state(None)
value, set_value = state
state.value
state.set_value
# Or do some weird typing magic to make this possible
value, set_value = use_state[str | None](None)
I haven't seen the latter pattern used very many places so it might surprise people, but it looks nice.
On thinking further, use_state is complicated by the fact that Python 3.9 broke generic NamedTuples which we'd need to get this working. The best we'd be able to do is a generic tuple till this is fixed in Python 3.11.
Mypy 0981 includes support for generic NamedTuples.
Also, presently the @component hides the type annotations of the underlying rendering function. This is because @component adds the special key parameter that is not consumed by the rendering function. Typing this correctly is blocked on concatenating keyword-only arguments with ParamSpec
Currently getting type hint errors on the component decorator, if str() is directly returned within a component. Or if a component is directly returned within a component.
VDOM constructor type hints currently don't like None.
In the case a None child is passed?
Yeah, for example a conditional to return a component or None within a div tag.
It's also still very annoying that I can't directly return a component within a component without type hint errors.
a conditional to return a component or None within a div tag.
Can you share an example. I'm not quite sure I understand what you're describing.
I can't directly return a component within a component
Can you share the error you're getting? The type hint for the allowable return type for component functions should allow for this.
Argument of type "None" cannot be assigned to parameter "attributes_and_children" of type "VdomAttributesAndChildren" in function "__call__"
Type "None" cannot be assigned to type "VdomAttributesAndChildren"
Type "None" cannot be assigned to type "Mapping[str, Any]"
"__iter__" is not present
"key" is not present
"type" is not present
"render" is not present
"should_render" is not present
Type "None" cannot be assigned to type "VdomDict"
...Pylance[reportGeneralTypeIssues](https://github.com/microsoft/pylance-release/blob/main/DIAGNOSTIC_SEVERITY_RULES.md#diagnostic-severity-rules)
@component
def component_1():
return html.div(
None
)
Argument of type "(*args: Unknown, **kwargs: Unknown) -> (Component | Unknown)" cannot be assigned to parameter "function" of type "(...) -> (ComponentType | VdomDict | None)" in function "component"
Type "(*args: Unknown, **kwargs: Unknown) -> (Component | Unknown)" cannot be assigned to type "(...) -> (ComponentType | VdomDict | None)"
Function return type "Component | Unknown" is incompatible with type "ComponentType | VdomDict | None"
Type "Component | Unknown" cannot be assigned to type "ComponentType | VdomDict | None"
Type "Component" cannot be assigned to type "ComponentType | VdomDict | None"
"Component" is incompatible with protocol "ComponentType"
"Component" is incompatible with "VdomDict"
Type cannot be assigned to type "None"Pylance[reportGeneralTypeIssues](https://github.com/microsoft/pylance-release/blob/main/DIAGNOSTIC_SEVERITY_RULES.md#diagnostic-severity-rules)
@component
def component_2():
return html.div()
@component
def component_1():
return component_2()
For the first case, it looks like the behavior of React is to simply exclude children which are None. Code changes will need to happen in two places:
- https://github.com/idom-team/idom/blob/main/src/idom/core/vdom.py#L247-L252
- https://github.com/idom-team/idom/blob/main/src/idom/core/types.py#L83
For the second, I don't observe any problems when I lint with MyPy. What does your VSCode configuration look like? I can't seem to figure out how to reproduce the errors your seeing.
Yes those errors are pylance specific, using "basic" type checking.
I'll post the required vscode settings after work.
In the vscode settings.json
{
"python.analysis.typeCheckingMode": "basic",
"python.languageServer": "Pylance",
}
Did you ever replicate the issue with embedded component type hints?
Whoops I linked to a comment here thinking it wouldn't close the issue.
Object of type ComponentType is not callable is a common type hint issue I get when trying to generically define a component.
For example
@dataclass
class Example():
my_component: ComponentType
...
Example.my_component(params)
Technically this is true. The component object itself is not actually callable. You should annotate this as Callable[... ComponentType]. We can create a new type alias for this since it seems generally useful. Maybe ComponentFunc?
Actually there's already an annotation for this called ComponentConstructor
There is a type hint mismatch causing some annoyance on my side
The @component decorator returns (...) -> Component while ComponentConstructor returns (...) -> ComponentType, making them incompatible in the eyes of Pylance.
Component is a concrete implementation of the abstract ComponentType. MyPy seems able to handle this so probably an issue with Pyright.
In order for PyRight to handle this, Component would need to inherit from ComponentType.
Traditionally, subtyping is based on inheritance hierarchies, but ComponentType is a protocol which uses something called structural subtyping. That is, if A is a protocol, then B can be said to be a sub-type of A if B has all, compatibly annotated, attributes and methods of A.
I think the generic tuple on use_state might be more frustrating to use than the original implementation.
Yeah... Far too many annoying to fix type hint errors with the new use_state.
Can you elaborate on what those issues are?
Seems like the main MyPy issue is _Type is not correctly inferred for state and set_state.
Basically every usage of state/set_state result errors similar to
Argument 1 has incompatible type "bool"; expected "Union[_Type, Callable[[_Type], _Type]]"
or
Argument 2 to "Mutation" has incompatible type "_Type"; expected "bool"
You can see this in django_idom.hooks.
I've been noticing some weird behavior with my MyPy cache. Does the error persist if you clear the cache first?
Clearing cache fixed the issues.
How strange... Maybe we can see if sqlite_cache resolves this issue?
Yeah, I have no idea what's going on. I've been meaning to try and write up a bug report.