From d36a0528ea34283be57651e95e57f52fb8ffac04 Mon Sep 17 00:00:00 2001 From: DarkCat09 Date: Sun, 25 Dec 2022 17:45:22 +0400 Subject: [PATCH] Web server for node.js interpreter, added docstrings, unittest --- python_aternos/atjsparse.py | 82 ++++++++++++++++++++++++----- python_aternos/data/server.js | 30 +++++++++++ setup.py | 1 + tests/{test_js.py => test_js2py.py} | 6 +-- tests/test_jsnode.py | 24 +++++++++ 5 files changed, 127 insertions(+), 16 deletions(-) create mode 100644 python_aternos/data/server.js rename tests/{test_js.py => test_js2py.py} (93%) create mode 100644 tests/test_jsnode.py diff --git a/python_aternos/atjsparse.py b/python_aternos/atjsparse.py index 929fe9f..8e8b39a 100644 --- a/python_aternos/atjsparse.py +++ b/python_aternos/atjsparse.py @@ -1,15 +1,20 @@ """Parsing and executing JavaScript code""" import abc + +import json import base64 + +import time import subprocess from pathlib import Path -from typing import Optional, Union, Any -from typing import Type +from typing import Optional, Union +from typing import Type, Any import regex import js2py +import requests js: Optional['Interpreter'] = None @@ -18,9 +23,19 @@ class Interpreter(abc.ABC): """Base JS interpreter class""" def __init__(self) -> None: + """Base JS interpreter class""" pass 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) @abc.abstractmethod @@ -48,24 +63,47 @@ class Interpreter(abc.ABC): 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__() + + file_dir = Path(__file__).absolute().parent + server_js = file_dir / 'data' / 'server.js' + + self.url = f'http://{host}:{port}' + self.proc = subprocess.Popen( - node, - stdout=subprocess.PIPE, + args=[ + node, server_js, + f'{port}', host, + ], ) + time.sleep(0.1) 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: - assert self.proc.stdout is not None - self.proc.stdout.read() - self.proc.communicate(name.encode('utf-8')) - return self.proc.stdout.read().decode('utf-8') + resp = requests.post(self.url, data=name) + resp.raise_for_status() + return json.loads(resp.content) def __del__(self) -> None: self.proc.terminate() + self.proc.communicate() class Js2PyInterpreter(Interpreter): @@ -128,7 +166,7 @@ class Js2PyInterpreter(Interpreter): def atob(s: str) -> str: """Wrapper for the built-in library function. - Decodes base64 string + Decodes a base64 string Args: s (str): Encoded data @@ -140,8 +178,26 @@ def atob(s: str) -> str: 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 + + # create if none if js is None: - js = create() + js = create(*args, **kwargs) + + # and return return js diff --git a/python_aternos/data/server.js b/python_aternos/data/server.js new file mode 100644 index 0000000..6eb2d44 --- /dev/null +++ b/python_aternos/data/server.js @@ -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) diff --git a/setup.py b/setup.py index 9064f1b..607f24c 100644 --- a/setup.py +++ b/setup.py @@ -42,4 +42,5 @@ setuptools.setup( ], packages=['python_aternos'], python_requires=">=3.7", + include_package_data=True, ) diff --git a/tests/test_js.py b/tests/test_js2py.py similarity index 93% rename from tests/test_js.py rename to tests/test_js2py.py index f077739..cd2ec21 100644 --- a/tests/test_js.py +++ b/tests/test_js2py.py @@ -51,10 +51,10 @@ class TestJs2Py(unittest.TestCase): def test_exec(self) -> None: - for i, f in enumerate(self.tests): - self.js.exec_js(f) + for func, exp in zip(self.tests, self.results): + self.js.exec_js(func) res = self.js['AJAX_TOKEN'] - self.assertEqual(res, self.results[i]) + self.assertEqual(res, exp) if __name__ == '__main__': diff --git a/tests/test_jsnode.py b/tests/test_jsnode.py new file mode 100644 index 0000000..b676cb8 --- /dev/null +++ b/tests/test_jsnode.py @@ -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()