Skip to content

How should JS values bind into Python? #808

Closed

Description

@coolreader18

This does not require knowledge of the WASM crate's internals, only a high level knowledge of Python and JS objects.

A couple questions about adding a JsValue class to WASM:

  • When passing in a JS value into python, where should the line be between JsValue and builtin Python type? e.g. JS strings and numbers should be converted to their corresponding Python primitives, but what about arrays/lists? TypedArrays/bytes/bytearrays?
  • Should functions be JsValues too? Should JsValue have a __call__ method?
  • Should we have a __getattr__ method on JsValue?
    • Should we throw an error when a JsValue doesn't have an attribute, or just return None?
  • How should we transfer kwargs to JS functions? Right now it provides them as the this variable, but should we change that?
  • How should errors transfer/convert between the two languages?

Not all of these questions are necessarily hard to answer, but it's good to have them on the table.

Activity

  1. Ryex commented on Apr 9, 2019

    @Ryex
    Contributor

    Here are my thoughts

    1. Simple values have simple mappings to related python types (int | float) => number, null => None etc. The question of JS's more complex types is actually rather interesting and I think should be tackled on a case by case basis.

      • TypedArray's actually have an equivalent mapping in Python's standard library array. I think it might be worthwhile to explicitly map these types together as they seem to be mostly mirrors outside of JS supporting 8 byte 64bit Bigints alternatively reducing to bytearray in python might be more universal
    2. in general, my position is that serialisation boundaries should be strict in at least one direction (namely Javascript into python's VM), allowing only a list of types with well-defined transformations to pass. moving out of python objects can be mapped rather transparently into JS Objects so we can be less strict with the types we don't have defined transformations for.

    3. would there be any benefit to allowing function objects to pass through the wasm layer? I wouldn't think so unless there was a reliable way to pass them back in or call them from javascript in such a way that they keep their VM bindings intact.

    4. this question confuses me. it implies that we can call functions across what I assumed would be a serialization boundary between JS and the WASM layer if not also between the WASM and python VM layer. or is this being asked form the python side for a __getattr__ for the properties of an object passed from JS? In either case, I feel like the ability to pass arbitrary objects between the two environments is a bad idea so a __getattr__ would be rendered moot.

    5. so, I'm writing this as I read over the questions so forgive me if this seems confused compared to the rest of it. This last question about how to pass **kwargs can be passed into the Python VM and properly called. I wasn't even aware this could be done with WASM in general because I was under the impression there was a serialization layer between JS and the WASM VM. I'd need to see a concrete example but if a consistent call signature then binding them to a known name in the this object seems appropriate

    6. Errors should probably be passed as JS Error's and either raised explicitly or returned in the tuple (result, err) pattern.

    My conclusion: I feel I actually do need an understanding of at least some of the finer points how calls between the two environments work to properly participate. reading the current WASM code on the master branch can only do so much. perhaps this discussion can coincide with an effort to better document the WASM module?

  2. coolreader18 commented on Apr 9, 2019

    @coolreader18
    MemberAuthor

    Oh, my apologies, I guess I assumed some knowledge about the WASM crate. There are currently function bindings, both ways, between JS and Python. When you call a JS function from Python, it transforms the kwargs into a JS object and supplies it as the this argument. When you call a Python function from JS, it takes an object and an array, for kwargs and args respectively. (The two arguments are a limit of wasm-bindgen, a closure can only have so many arguments, no varargs.) The idea for a JsValue class would be so we wouldn't have to manually provide bindings for every possible JS object someone might want to interact with in Python, e.g. an Event someone might get from an event listener. So, the idea with __getattr__ was: Should we let someone access a property on a JS object like obj.prop or keep it to __getitem__ (obj['prop']).

  3. Ryex commented on Apr 9, 2019

    @Ryex
    Contributor

    Ah, thank you that clears up my confusion. in that case, my position is this:

    1. types form JS should be mapped to Python types only when there is a clear, unambiguous mapping from one language to another, eg. Number to float (we're going to want to be careful about size there), etc. Otherwise, it should map to a JsValue class as suggested.

      • my opinion is that JsValue should store the properties of the JS object including its type (if that can be reliably determined) as _ attributes. ie. _props, _js_type
      • and a __getattr__ should be defined along the semantics of:
      def __getattr__(self, attr):
          if attr in self._props:
              return self._props[attr]
          raise AttributeError("JsValue object has no property {}".format(attr))
      • a __getitem__ I feel should not be used for properties as it would break pythonicness
    2. In the case of JS functions passed into python, I think these should be mapped as a JsValuCallable class which implements an appropriate __call__ function.

      • __call__ should be something along the lines of def __call__(self, *args, **kwargs): where *args is used to populate the positional args of the JS callable, and attaching **kwargs as properties of this in JS as you said WASM does seem the most reasonable option.
      • information from the JS function's prototype should be pulled into attributes of the JSValueCallable. name and length are the important ones, but any other information that is gleanable should be used.
      • a TypeError should be raised if there are too many or too few *args to __call__, I know technically javascript won't care because the extra will be lost, and the not passed ones will be undefined but I feel every effort should be made to make python feel pythonic even in the browser dealing with JS values.
    3. going the other direction of python callables in js, it should probably be made as painless a possible so py_func_in_js(args: any[] [, kwargs: object]) works about as well as one could wish`

    4. Id say that the best case is that Errors get converted with a proper stack trace. we may need to wait for wasm-bindgen#1173. Ideally, we would get an intelligible stack trace out of unwinding a python error caused by a Js exception and vise versa. any solution that allows that with no extra work for user code is the way to go.

  4. coolreader18 commented on Apr 9, 2019

    @coolreader18
    MemberAuthor

    a TypeError should be raised if there are too many or too few *args to call

    The problem with that is that JS didn't used to have the concept of optional args (e.g. function(a = 1){}), so some old or backwards-compatible code might do it like function(a) { if (a === undefined) a = 1; }, which has the same functionality except for making func.length 1 where it was 0 for ES6 default args. I think we should prioritize usability of a JS function with any kind of signature over making things more Pythonic, and perhaps consumers of a JS library could write wrapper libraries over more "low-level" JS libraries, á la CPython with _io vs io.

  5. Ryex commented on Apr 10, 2019

    @Ryex
    Contributor

    some old or backwards-compatible code might do it like function(a) { if (a === undefined) a = 1; }, which has the same functionality except for making func.length 1 where it was 0 for ES6 default args.

    darn, I completely forgot about default args with that suggestion. I feel there should at least be a warning injection when the arg number's don't match with perhaps an option to turn it off. but that's just me being idealistic.

  6. adrian17 commented on Apr 10, 2019

    @adrian17
    Contributor

    I think we should prioritize usability of a JS function with any kind of signature over making things more Pythonic

    At the same time, as you said, they do have a concept of optional args now, so why not enforce more "strict" code? (In fact, being idealistic, it could even be possible to generate both typescript and mypy type definitions to enforce both sides from developer's side.)

  7. OddCoincidence commented on Apr 11, 2019

    @OddCoincidence
    Contributor

    What exactly is the use case for a JsValue class? The point of JsValue iirc is to emulate dynamic typing in rust, so I'm failing to see how it even makes sense to directly expose it to python.

  8. coolreader18 commented on Apr 11, 2019

    @coolreader18
    MemberAuthor

    So that when we pass a JS value into Python, Python code can manipulate it without us needing to define a specific class, e.g. an Event from an EventListener.

    The point of JsValue iirc is to emulate dynamic typing in rust

    No, that's std::any::Any. JsValue is a representation of a JavaScript value that can be manipulated from Rust, and the same would go for a Python JsValue.

  9. OddCoincidence commented on Apr 20, 2019

    @OddCoincidence
    Contributor

    So that when we pass a JS value into Python, Python code can manipulate it without us needing to define a specific class, e.g. an Event from an EventListener.

    Given that python primitives would map to js primitives, it seems like what you want is a wrapper around js_sys::Object, not JsValue.

    No, that's std::any::Any. JsValue is a representation of a JavaScript value that can be manipulated from Rust, and the same would go for a Python JsValue.

    I guess I phrased that poorly - I didn't mean emulate dynamic typing in general, but rather that exposing a value from a dynamically typed language like JS needs a type like JsValue to be usable from rust. Exposing that directly to Python seems nonsensical to me. What makes sense is to expose each variant (ideally using the js_sys wrappers).

  10. coolreader18 commented on Apr 20, 2019

    @coolreader18
    MemberAuthor

    Exposing that directly to Python seems nonsensical to me. What makes sense is to expose each variant (ideally using the js_sys wrappers).

    The thing is that Python is also dynamically typed, so there's not really a big ergonomics difference between using a general JSObject and a hand-crafted wrapper to map to a js_sys type. I'm also sorta opposed to individual wrapper types because it seems like a lot of manual labor for what I see as not a ton of gain.

  11. coolreader18 commented on Apr 20, 2019

    @coolreader18
    MemberAuthor

    I think for some things it would definitely make sense to make a custom wrapper type for, but just not everything.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions