Web server for node.js interpreter, added docstrings, unittest
This commit is contained in:
parent
a770df7334
commit
d36a0528ea
5 changed files with 127 additions and 16 deletions
|
@ -1,15 +1,20 @@
|
||||||
"""Parsing and executing JavaScript code"""
|
"""Parsing and executing JavaScript code"""
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
|
|
||||||
|
import json
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
|
import time
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Union, Any
|
from typing import Optional, Union
|
||||||
from typing import Type
|
from typing import Type, Any
|
||||||
|
|
||||||
import regex
|
import regex
|
||||||
import js2py
|
import js2py
|
||||||
|
import requests
|
||||||
|
|
||||||
js: Optional['Interpreter'] = None
|
js: Optional['Interpreter'] = None
|
||||||
|
|
||||||
|
@ -18,9 +23,19 @@ class Interpreter(abc.ABC):
|
||||||
"""Base JS interpreter class"""
|
"""Base JS interpreter class"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
"""Base JS interpreter class"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def __getitem__(self, name: str) -> Any:
|
def __getitem__(self, name: str) -> Any:
|
||||||
|
"""Support for `js[name]` syntax
|
||||||
|
instead of `js.get_var(name)`
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): Variable name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Variable value
|
||||||
|
"""
|
||||||
return self.get_var(name)
|
return self.get_var(name)
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
|
@ -48,24 +63,47 @@ class Interpreter(abc.ABC):
|
||||||
|
|
||||||
class NodeInterpreter(Interpreter):
|
class NodeInterpreter(Interpreter):
|
||||||
|
|
||||||
def __init__(self, node: Union[str, Path]) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
node: Union[str, Path] = 'node',
|
||||||
|
host: str = 'localhost',
|
||||||
|
port: int = 8001) -> None:
|
||||||
|
"""Node.JS interpreter wrapper,
|
||||||
|
starts a simple web server in background
|
||||||
|
|
||||||
|
Args:
|
||||||
|
node (Union[str, Path], optional): Path to `node` executable
|
||||||
|
host (str, optional): Hostname for the web server
|
||||||
|
port (int, optional): Port for the web server
|
||||||
|
"""
|
||||||
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
file_dir = Path(__file__).absolute().parent
|
||||||
|
server_js = file_dir / 'data' / 'server.js'
|
||||||
|
|
||||||
|
self.url = f'http://{host}:{port}'
|
||||||
|
|
||||||
self.proc = subprocess.Popen(
|
self.proc = subprocess.Popen(
|
||||||
node,
|
args=[
|
||||||
stdout=subprocess.PIPE,
|
node, server_js,
|
||||||
|
f'{port}', host,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
def exec_js(self, func: str) -> None:
|
def exec_js(self, func: str) -> None:
|
||||||
self.proc.communicate(func.encode('utf-8'))
|
resp = requests.post(self.url, data=func)
|
||||||
|
resp.raise_for_status()
|
||||||
|
|
||||||
def get_var(self, name: str) -> Any:
|
def get_var(self, name: str) -> Any:
|
||||||
assert self.proc.stdout is not None
|
resp = requests.post(self.url, data=name)
|
||||||
self.proc.stdout.read()
|
resp.raise_for_status()
|
||||||
self.proc.communicate(name.encode('utf-8'))
|
return json.loads(resp.content)
|
||||||
return self.proc.stdout.read().decode('utf-8')
|
|
||||||
|
|
||||||
def __del__(self) -> None:
|
def __del__(self) -> None:
|
||||||
self.proc.terminate()
|
self.proc.terminate()
|
||||||
|
self.proc.communicate()
|
||||||
|
|
||||||
|
|
||||||
class Js2PyInterpreter(Interpreter):
|
class Js2PyInterpreter(Interpreter):
|
||||||
|
@ -128,7 +166,7 @@ class Js2PyInterpreter(Interpreter):
|
||||||
|
|
||||||
def atob(s: str) -> str:
|
def atob(s: str) -> str:
|
||||||
"""Wrapper for the built-in library function.
|
"""Wrapper for the built-in library function.
|
||||||
Decodes base64 string
|
Decodes a base64 string
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
s (str): Encoded data
|
s (str): Encoded data
|
||||||
|
@ -140,8 +178,26 @@ def atob(s: str) -> str:
|
||||||
return base64.standard_b64decode(str(s)).decode('utf-8')
|
return base64.standard_b64decode(str(s)).decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
def get_interpreter(create: Type[Interpreter] = Js2PyInterpreter) -> 'Interpreter':
|
def get_interpreter(
|
||||||
|
create: Type[Interpreter] = Js2PyInterpreter,
|
||||||
|
*args, **kwargs) -> 'Interpreter':
|
||||||
|
"""Get or create a JS interpreter.
|
||||||
|
`*args` and `**kwargs` will be passed
|
||||||
|
directly to JS interpreter `__init__`
|
||||||
|
(when creating it)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
create (Type[Interpreter], optional): Preferred interpreter
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JS interpreter instance
|
||||||
|
"""
|
||||||
|
|
||||||
global js
|
global js
|
||||||
|
|
||||||
|
# create if none
|
||||||
if js is None:
|
if js is None:
|
||||||
js = create()
|
js = create(*args, **kwargs)
|
||||||
|
|
||||||
|
# and return
|
||||||
return js
|
return js
|
||||||
|
|
30
python_aternos/data/server.js
Normal file
30
python_aternos/data/server.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
const http = require('http')
|
||||||
|
const process = require('process')
|
||||||
|
|
||||||
|
args = process.argv.slice(2)
|
||||||
|
|
||||||
|
const port = args[0] || 8000
|
||||||
|
const host = args[1] || 'localhost'
|
||||||
|
|
||||||
|
const listener = (req, res) => {
|
||||||
|
|
||||||
|
if (req.method != 'POST')
|
||||||
|
res.writeHead(405) & res.end()
|
||||||
|
|
||||||
|
let body = ''
|
||||||
|
req.on('data', chunk => (body += chunk))
|
||||||
|
|
||||||
|
req.on('end', () => {
|
||||||
|
let resp
|
||||||
|
try { resp = JSON.stringify(eval(body)) }
|
||||||
|
catch (ex) { resp = ex.message }
|
||||||
|
res.writeHead(200)
|
||||||
|
res.end(resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
window = global
|
||||||
|
document = window.document || {}
|
||||||
|
|
||||||
|
const server = http.createServer(listener)
|
||||||
|
server.listen(port, host)
|
1
setup.py
1
setup.py
|
@ -42,4 +42,5 @@ setuptools.setup(
|
||||||
],
|
],
|
||||||
packages=['python_aternos'],
|
packages=['python_aternos'],
|
||||||
python_requires=">=3.7",
|
python_requires=">=3.7",
|
||||||
|
include_package_data=True,
|
||||||
)
|
)
|
||||||
|
|
|
@ -51,10 +51,10 @@ class TestJs2Py(unittest.TestCase):
|
||||||
|
|
||||||
def test_exec(self) -> None:
|
def test_exec(self) -> None:
|
||||||
|
|
||||||
for i, f in enumerate(self.tests):
|
for func, exp in zip(self.tests, self.results):
|
||||||
self.js.exec_js(f)
|
self.js.exec_js(func)
|
||||||
res = self.js['AJAX_TOKEN']
|
res = self.js['AJAX_TOKEN']
|
||||||
self.assertEqual(res, self.results[i])
|
self.assertEqual(res, exp)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
24
tests/test_jsnode.py
Normal file
24
tests/test_jsnode.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from python_aternos import atjsparse
|
||||||
|
from tests import files
|
||||||
|
|
||||||
|
|
||||||
|
class TestJsNode(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
|
||||||
|
self.tests = files.read_sample('token_input.txt')
|
||||||
|
self.results = files.read_sample('token_output.txt')
|
||||||
|
self.js = atjsparse.NodeInterpreter()
|
||||||
|
|
||||||
|
def test_exec(self) -> None:
|
||||||
|
|
||||||
|
for func, exp in zip(self.tests, self.results):
|
||||||
|
self.js.exec_js(func)
|
||||||
|
res = self.js['AJAX_TOKEN']
|
||||||
|
self.assertEqual(res, exp)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
Reference in a new issue