reactpy icon indicating copy to clipboard operation
reactpy copied to clipboard

Review User API Type Hints

Open Archmonger opened this issue 3 years ago • 38 comments

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.

Archmonger avatar Jul 29 '22 21:07 Archmonger

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.

rmorshea avatar Jul 30 '22 00:07 rmorshea

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.

rmorshea avatar Jul 30 '22 06:07 rmorshea

Mypy 0981 includes support for generic NamedTuples.

rmorshea avatar Sep 27 '22 19:09 rmorshea

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

rmorshea avatar Sep 27 '22 19:09 rmorshea

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.

Archmonger avatar Oct 07 '22 02:10 Archmonger

VDOM constructor type hints currently don't like None.

Archmonger avatar Oct 19 '22 01:10 Archmonger

In the case a None child is passed?

rmorshea avatar Oct 19 '22 03:10 rmorshea

Yeah, for example a conditional to return a component or None within a div tag.

Archmonger avatar Oct 19 '22 04:10 Archmonger

It's also still very annoying that I can't directly return a component within a component without type hint errors.

Archmonger avatar Oct 19 '22 06:10 Archmonger

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.

rmorshea avatar Oct 19 '22 07:10 rmorshea

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()

Archmonger avatar Oct 19 '22 08:10 Archmonger

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.

rmorshea avatar Oct 19 '22 18:10 rmorshea

Yes those errors are pylance specific, using "basic" type checking.

I'll post the required vscode settings after work.

Archmonger avatar Oct 19 '22 21:10 Archmonger

In the vscode settings.json

{
    "python.analysis.typeCheckingMode": "basic",
    "python.languageServer": "Pylance",
}

Archmonger avatar Oct 20 '22 06:10 Archmonger

Did you ever replicate the issue with embedded component type hints?

Archmonger avatar Nov 02 '22 06:11 Archmonger

Whoops I linked to a comment here thinking it wouldn't close the issue.

rmorshea avatar Nov 02 '22 07:11 rmorshea

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)

Archmonger avatar Dec 01 '22 10:12 Archmonger

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?

rmorshea avatar Dec 01 '22 17:12 rmorshea

Actually there's already an annotation for this called ComponentConstructor

rmorshea avatar Dec 01 '22 17:12 rmorshea

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.

Archmonger avatar Dec 05 '22 01:12 Archmonger

Component is a concrete implementation of the abstract ComponentType. MyPy seems able to handle this so probably an issue with Pyright.

rmorshea avatar Dec 05 '22 18:12 rmorshea

In order for PyRight to handle this, Component would need to inherit from ComponentType.

Archmonger avatar Dec 05 '22 21:12 Archmonger

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.

rmorshea avatar Dec 05 '22 22:12 rmorshea

I think the generic tuple on use_state might be more frustrating to use than the original implementation.

Archmonger avatar Jan 10 '23 08:01 Archmonger

Yeah... Far too many annoying to fix type hint errors with the new use_state.

Archmonger avatar Jan 20 '23 13:01 Archmonger

Can you elaborate on what those issues are?

rmorshea avatar Jan 27 '23 03:01 rmorshea

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.

Archmonger avatar Jan 27 '23 03:01 Archmonger

I've been noticing some weird behavior with my MyPy cache. Does the error persist if you clear the cache first?

rmorshea avatar Jan 27 '23 04:01 rmorshea

Clearing cache fixed the issues.

How strange... Maybe we can see if sqlite_cache resolves this issue?

Archmonger avatar Jan 27 '23 04:01 Archmonger

Yeah, I have no idea what's going on. I've been meaning to try and write up a bug report.

rmorshea avatar Jan 27 '23 06:01 rmorshea