pyodide / pyodide

Pyodide is a Python distribution for the browser and Node.js based on WebAssembly
https://pyodide.org/en/stable/
Mozilla Public License 2.0
12.08k stars 819 forks source link

Improve generic typing for `to_js` and `JsProxy` #4921

Closed CNSeniorious000 closed 2 months ago

CNSeniorious000 commented 3 months ago

🚀 Feature

I think JsProxy can also be a generic type. Like:

class JsProxy[T](...):
    def to_py(self) -> T: ...

And to_js(...).to_py() should returns the same type as the input (if it is a simple type).

Motivation

In webtypy, all kinds of options are TypedDicts, but they should be converted to a JsProxy before sending it to the JavaScript function. I think we can change the type there to JsProxy[<the original TypedDict>] instead, which needs the JsProxy to be a generic type.

Supporting this may make webtypy even more useful.

Pitch

Add more @overload in ffi.py

Alternatives

Additional context

I think in the JavaScript side, we can consider make PyProxy a generic type too. Like:

class PyProxy<T extends any> {
  toJs(): T
}
hoodmane commented 3 months ago

Sounds generally reasonable to me. But mypy requires you provide a T to JsProxy[T] and not everything does something reasonable when you call to_py() so I'm not sure what to do about it. Can we make a type parameter optional?

CNSeniorious000 commented 3 months ago

Can we make a type parameter optional?

I think this is possible in python 3.13. In python 3.13 PEP 696 is implemented. So we can provide a default argument to TypeVar. I think mypy will support this then.

I think we can explicitly specify the type in every usage of JsProxy. And use JsProxy[Any] for cases when we can't infer the type. For downstream developers, they may need to static type every JsProxy too or at least use JsProxy[Any] if they want to support mypy. (Or they can migrate to pyright because pyright treats them as JsProxy[unknown] in "standard" mode. Its "strict" mode is the same as mypy)

Another way is to use a type-only TypedJsProxy. The implementation may belike:

class TypedJsProxy[T](JsProxy):
    def to_py(self) -> T: ...

@overload
def to_js(value: T) -> TypedJsProxy[T]: ...
# ... other to_js overloads ...
def to_js(value: Any) -> TypedJsProxy[Any]: ...
CNSeniorious000 commented 2 months ago

After trying a bit, I think this is impossible and worthless. If we want to pursue absolute type correctness, we may need to make too many changes.

Firstly, we need to add a TypedJsProxy here, which is a fake class for type hinting only. Then, we need to change every option type in webtypy where TypedDict is used as a parameter to TypedJsProxy... like this:

- def fetch(input: str, init: RequestInit | None = {}) -> Awaitable[Response]:
+ def fetch(input: str, init: TypedJsProxy[RequestInit] | None = to_js({})) -> Awaitable[Response]:

I think this is not a good solution.

On the other hand, when we use webtypy to type check javascript function usages, we can simply make a fake to_js to have the right type checking:

if TYPE_CHECKING:
    def to_js[T](obj: T) -> T: ...
else:
    from pyodide.ffi import to_js

Then use to_js in calls to JavaScript functions:

from js import fetch

await fetch("/", to_js({"headers": {}, "method": "POST", "body": "123"))

will have the right behavior both during runtime and type-checking.

So I am closing this issue.