浏览代码

Fix typing for ui.refreshable/ui.refreshable_method (#4510)

If you try to use an `async` refreshable method, the fact that it is
`async` is lost by the type checker.

For example (A bigger, full example is below)
```python
from nicegui import ui
from typing_extensions import reveal_type

@ui.refreshable
def sync() -> None:
    return

@ui.refreshable
async def a_sync() -> None:
    return

reveal_type(sync) # refreshable[(), None]
reveal_type(a_sync) # refreshable[(), None]
```

The typing for the input function is `Callable[_P, Union[_T,
Awaitable[_T]]]` and this PR removes the `Awaitable[_T]` from the union.
The type checker will know the return type of the method is `Coroutine`
and not lose that information.

---

Full Example:
```python
from nicegui import ui
from typing_extensions import reveal_type

@ui.refreshable
def sync() -> None:
    return

@ui.refreshable
async def a_sync() -> None:
    return

class Foo:

    @ui.refreshable_method
    def sync(self) -> None:
        return

    @ui.refreshable_method
    async def a_sync(self) -> None:
        return

reveal_type(sync)
reveal_type(a_sync)

async def func():

    foo = Foo()

    sync()
    sync.refresh()

    await a_sync()
    a_sync.refresh()

    foo.sync()
    foo.sync.refresh()

    await foo.a_sync()
    foo.a_sync.refresh()
```

In the current version of nicegui, you get type errors on the `await`
lines and both the reveal types are `refreshable[(), None]`. In my
version, you get no type errors and the second reveal type is
`refreshable[(), Coroutine[Any, Any, None]]`

---

I had to add a `type: ignore` on `refreshable.py:45` . I tried
converting `is_coroutine_function` to a `TypeGuard`, but some typing
information (the actual return value) is lost, so you'll have to add
`type: ignore` or `cast` elsewhere. _I think_ this can be fixed by
narrowing the type of the input of `is_coroutine_function` from `Any` to
`Callable` with some overloads, but that would cause type errors
elsewhere. If someone does figure out a "better" typing of
`is_coroutine_function`, then the `assert isinstance(result,
Awaitable)`s can go away (I assume they were there for the type
checker).
Daniel Kramer 1 月之前
父节点
当前提交
4a5ede2df5
共有 1 个文件被更改,包括 6 次插入6 次删除
  1. 6 6
      nicegui/functions/refreshable.py

+ 6 - 6
nicegui/functions/refreshable.py

@@ -1,7 +1,7 @@
 from __future__ import annotations
 
 from dataclasses import dataclass, field
-from typing import Any, Awaitable, Callable, ClassVar, Dict, Generic, List, Optional, Tuple, TypeVar, Union, cast
+from typing import Any, Awaitable, Callable, ClassVar, Dict, Generic, List, Optional, Tuple, TypeVar, cast
 
 from typing_extensions import Concatenate, ParamSpec, Self
 
@@ -28,7 +28,7 @@ class RefreshableTarget:
     locals: List[Any] = field(default_factory=list)
     next_index: int = 0
 
-    def run(self, func: Callable[..., Union[_T, Awaitable[_T]]]) -> Union[_T, Awaitable[_T]]:
+    def run(self, func: Callable[..., _T]) -> _T:
         """Run the function and return the result."""
         RefreshableTarget.current_target = self
         self.next_index = 0
@@ -42,7 +42,7 @@ class RefreshableTarget:
                         result = func(self.instance, *self.args, **self.kwargs)
                     assert isinstance(result, Awaitable)
                     return await result
-            return wait_for_result()
+            return wait_for_result()  # type: ignore
         else:
             with self.container:
                 if self.instance is None:
@@ -57,7 +57,7 @@ class RefreshableContainer(Element, component='refreshable.js'):
 
 class refreshable(Generic[_P, _T]):
 
-    def __init__(self, func: Callable[_P, Union[_T, Awaitable[_T]]]) -> None:
+    def __init__(self, func: Callable[_P, _T]) -> None:
         """Refreshable UI functions
 
         The ``@ui.refreshable`` decorator allows you to create functions that have a ``refresh`` method.
@@ -83,7 +83,7 @@ class refreshable(Generic[_P, _T]):
             return refresh
         return attribute
 
-    def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> Union[_T, Awaitable[_T]]:
+    def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _T:
         self.prune()
         target = RefreshableTarget(container=RefreshableContainer(), refreshable=self, instance=self.instance,
                                    args=args, kwargs=kwargs)
@@ -133,7 +133,7 @@ class refreshable(Generic[_P, _T]):
 
 class refreshable_method(Generic[_S, _P, _T], refreshable[_P, _T]):
 
-    def __init__(self, func: Callable[Concatenate[_S, _P], Union[_T, Awaitable[_T]]]) -> None:
+    def __init__(self, func: Callable[Concatenate[_S, _P], _T]) -> None:
         """Refreshable UI methods
 
         The `@ui.refreshable_method` decorator allows you to create methods that have a `refresh` method.