PiotrDabkowski / Js2Py

JavaScript to Python Translator & JavaScript interpreter written in 100% pure Python🚀 Try it online:
http://piter.io/projects/js2py
MIT License
2.47k stars 261 forks source link

How to enforce parameter types when calling pyimport functions? #87

Open amv opened 7 years ago

amv commented 7 years ago

For example the webapp2 framework expects to get it's parameters in this format:

app = webapp2.WSGIApplication([
    ('/', MainPage),
], debug=True)

If I pyimport webapp2, how do I create variables in javascript to cause the parameters to be in the right format when entering the function call (one or more tuples inside a list + a named attribute) ?

PiotrDabkowski commented 7 years ago

Hey, I am afraid the named parameters and tuples are not supported directly.

Both tuples and lists are converted to Arrays when entering Js2Py and Array. When leaving Js2Py the Array is converted back to list. So tuple will be converted to list during this process. In most cases in Python there is not a big difference between lists and tuples and in most cases they can be used interchangeably.

Keyword arguments and *args are not supported directly yet.

However, you can solve these problems by creating a wrapper function in Python.

def wsgi_application_wrapper(handlers, debug):
    import webapp2
    return webapp2.WSGIApplication(map(tuple, handlers), debug=debug)

Later you add this function to the execution context as usual.

ex = js2py.EvalJs({'wsgi_application_wrapper': wsgi_application_wrapper})
ex.eval("wsgi_application_wrapper([['/', 1],], true)")

Otherwise, you can create a new python file with wrappers and use a pyimport to import it. I understand this is not ideal and maybe I will solve it in the future.

amv commented 7 years ago

Ok. I figured so much that by making Python wrappers, I could make things happen as I wanted, but as you said it is not ideal, and I have for the time being put my project on hold as infeasible.

Maybe a bit of context will help on what I was trying to do, and whether you want to even try to support this use case with the project in the future:

My aim was to create some Google App Engine Standard environment deployable projects with Javascript, but as GAE standard does not support JS, I was inspecting the possibility to use Js2Py .translate_file as a part of my build process to generate Python files which GAE could then run.

The initial aim was to write code using ES6, transpile it to ES5 with Babel in my build scripts, and then Js2Py the files to Python 2.7 files for GAE Standard.

Even from the rudimentary "Hello world" -tests I found the following things to pose difficulty:

1) Some kind of support for "require" would be necessary to allow using NPM modules as part of a natural JS development process. 2) It is difficult to get non-javascript syntax, specifically pyimport to pass through Babel 3) GAE requires an app entry point to be configured in the form of mymodule.variable, which Python code then looks for, and I was not easily able to introduce this kind of top level module variable with Js2Py generated Python module. 4) Some level of interaction with Python native functions is required in the GAE environment, and Js2Py does not allow JS code to specify the exact types of Python variables one wants to pass in to those Python function calls (without writing a Python wrapper for each of them). 5) Some level of interaction with native Python variables is required, which is not possible to generate from Js2Py generated code (without writing a Python wrapper for each of them).

A specific example of the last one is that I get a "headers" dict from Python to my request handler and I am supposed to set a key to that dict to add headers, but setting self.request.headers["Content-Type"] in JS code does not actually add the key to the underlying dict, but adds it to the JsObjectWrapper instead.

In the end I did get a Hello World example handler working, but in order to do that I had to manually write Python wrappers for:

1) the module variable 2) creating Python subclasses (which were required as parameters) 3) calling Python native functions with correct parameters 4) setting the dict key value properly 5) and to have the dict value be of type str instead of unicode

.. and this was without ES6 transpiling and without the use of any NPM modules, so all in all I concluded that for now there are too many obstacles to continue.

PiotrDabkowski commented 7 years ago

Interesting project, but I agree it may be slightly hard to do with Js2Py.

  1. require is not really a problem because if you can use Babel to compile everything to the single file. Just like you compile ES6 to ES5.
  2. Again not a big problem because you can simply define and use the pyimport function instead of the statement.
  3. All the variables are stored in the variable called var. You can retrieve any object from it using var.to_python().name.
  4. Yes, I agree this is a problem, especially with confusing types like unicode vs str and tuple vs list. Also, keyword arguments are not supported in JS. No easy fix for this.
  5. This could be a bug, I will check it out.

Yeah, compiling NPM modules may be sometimes a bit complicated because some of them depend on node host environment. For example on object like process or Buffer which is not implemented by Js2Py. It is possible to make everything work, but that would involve much more work on the Js2Py project. And I dont think its worth it just for a single interesting project.

amv commented 7 years ago

On 1: After thinking about this a bit in the morning, I too concluded that using a packager like Webpack, which creates a single file meant for web browser usage, would probably help with the require bit. I am not sure if they actually bundle some polyfills of the node environment, but at least those modules which are supposed to work in the browser with Webpack would also be supposed to work with Js2Py, right?

I am starting to think that this would actually be less of a problem in my specific use case where the App Engine environment itself is very limited in regards to the access to the native environment anyway.

On 2: So there is a pyimport function available for JS? I did not find mention of it in the documentation, which only points to a "statement" which does not look like Javascript. Changing the statements to pyimport('webapp2'); results in translation error: TypeError: 'bool' object is not callable, so I guess I am doing it wrong?

On 3: I actually did already use the the var variable to get my code working, by pointing the App Engine entry point to mainwrapper.app and then providing the following file:

# mainwrapper.py
import main;
app = main.var.get(u'app').to_python();

.. but it would be nice to have a javascript function for setting a python module variable so I would not need to have a wrapper for this.

On 4: I think you are right that there are no ways to write javascript that would execute "the same", but since we are talking about a situation which is specific to interfacing with pure Python functions, maybe it would be possible to just provide some simple functions to the JS scope that would allow specifying the type that is passed in? I am talking about something like the following:

my_python_function.call( [ py_str( "hello" ), py_tuple( [1,2] ) ], { "name": var } )

.. or:

py_call( mymodule.myfunction, [ py_str( "hello" ), py_tuple( [1,2] ) ], { "name": var } ) ]

As Python does not allow providing unnamed variables after named variables, and Python seems to allow providing a mix of unnamed and named arguments to function calls dynamically, maybe the implementation could be as simple as:

def py_call( actual_python_function, args=[], named_args={} ):
  if type( args ) == 'dict':
    actual_python_function( **args )
  else:
    actual_python_function( *args, **named_args )

Currently it is not possible to do this in a Js2Py translated code, because even if py_str would be an imported Python function that would return a Python string, the return value of the function would always be converted to unicode before being passed again into the Python function as an argument. The same goes for tuples and arrays as showcased in #86

But just adding some special casting functions for the JS code to use, which would bypass the encapsulation and simply pass the variable on as is, might be enough to make this possible.

amv commented 7 years ago

I dabbled a bit more with this, and the result is here:

https://github.com/amv/gae-js2py-es2015-example

Some notes based on that work:

Babel and Browserify seemed to work well together to translate ES6 code and package NPM modules for Js2Py.

However using Browserify made it harder to set top level module variables, as Browserify also wraps the entry file in it's own namespace.

From reading the Browserify generated code, it turned out that setting a window = {} object before running the bundled code and using global.myvar = .. in the handlers helped to get the variables out to Python (using vars.get(u'global').get(u'myvar'). From the perspective of Browserify, global variable should work the same as window variable, but I got some weird errors when running it through Js2Py, which were fixed by switching to window.

amv commented 7 years ago

Also I not added the pyimport directives after the Babel compilation step, because I did not find a way to do it from the JS file so that Babel would not have choked on them.. Trying eval("pyimort time") got me some weird warning messages with the GAE environment, so I veered away from it.