Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: lint
on:
pull_request:
workflow_dispatch:
push:
branches:
- master

jobs:
ruff:
name: Ruff
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
Comment on lines +14 to +16
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These versions are quite outdated.

with:
python-version: "3.12"
- name: Install lint tools
run: |
python -m pip install --upgrade pip
pip install ruff pre-commit
- name: Ruff check
run: |
ruff check .
ruff format --check .
Comment on lines +22 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't install ruff, run pre-commit (although, you could also skip GHA and use the official app). Personally, I wrap linting things with tox: https://github.com/cherrypy/cheroot/blob/662cd9dcf426253536f921c618b480e65a429cb1/.github/workflows/ci-cd.yml#L606-L710

13 changes: 13 additions & 0 deletions .pre-commit-config.yaml
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use native integration, not local and you won't need to install ruff externally + run it in https://pre-commit.ci. Pro tip: with a double run, you can normalize trailing commas nicely — https://github.com/cherrypy/cheroot/blob/662cd9dcf426253536f921c618b480e65a429cb1/.pre-commit-config.yaml#L62-L87

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
repos:
- repo: local
hooks:
- id: ruff-check
name: ruff-check
entry: ruff check .
language: system
types: [python]
- id: ruff-format
name: ruff-format
entry: ruff format --check .
language: system
types: [python]
7 changes: 7 additions & 0 deletions .ruff.toml
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
line-length = 88
target-version = "py39"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set requires-python in pyproject.toml instead.

Suggested change
target-version = "py39"

extend-exclude = ["src/duktape", "dukpy/jsmodules", "dukpy/jscore"]

[lint]
select = ["E", "F"]
ignore = ["E501"]
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
recursive-include src *.h
recursive-include src/quickjs *.c *.h VERSION
recursive-include dukpy/jscore *.js
recursive-include dukpy/jsmodules *.js
include LICENSE
6 changes: 5 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ dukpy
.. image:: https://img.shields.io/pypi/v/dukpy.svg
:target: https://pypi.org/p/dukpy

.. raw:: html

<img align="left" width="100px" src="dukpy_logo.png" alt="DukPy logo">


DukPy is a simple javascript interpreter for Python built on top of
duktape engine **without any external dependency**.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be nice to leave a historic reference explaining the project name.

QuickJS engine **without any external dependency**.
It comes with a bunch of common transpilers built-in for convenience:

- *CoffeeScript*
Expand Down
14 changes: 13 additions & 1 deletion dukpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,16 @@
from .coffee import coffee_compile
from .babel import babel_compile, jsx_compile
from .tsc import typescript_compile
from .lessc import less_compile
from .lessc import less_compile

__all__ = [
"JSInterpreter",
"JSRuntimeError",
"babel_compile",
"coffee_compile",
"evaljs",
"install_jspackage",
"jsx_compile",
"less_compile",
"typescript_compile",
]
26 changes: 15 additions & 11 deletions dukpy/babel.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
import os
from .evaljs import evaljs

BABEL_COMPILER = os.path.join(os.path.dirname(__file__), 'jsmodules', 'babel-6.26.0.min.js')
BABEL_COMPILER = os.path.join(
os.path.dirname(__file__), "jsmodules", "babel-6.26.0.min.js"
)


def babel_compile(source, **kwargs):
"""Compiles the given ``source`` from ES6 to ES5 using Babeljs"""
presets = kwargs.get('presets')
presets = kwargs.get("presets")
if not presets:
kwargs['presets'] = ["es2015"]
with open(BABEL_COMPILER, 'rb') as babel_js:
kwargs["presets"] = ["es2015"]
with open(BABEL_COMPILER, "rb") as babel_js:
return evaljs(
(babel_js.read().decode('utf-8'),
'var bres, res;'
'bres = Babel.transform(dukpy.es6code, dukpy.babel_options);',
'res = {map: bres.map, code: bres.code};'),
(
babel_js.read().decode("utf-8"),
"var bres, res;"
"bres = Babel.transform(dukpy.es6code, dukpy.babel_options);",
"res = {map: bres.map, code: bres.code};",
),
es6code=source,
babel_options=kwargs
babel_options=kwargs,
)


def jsx_compile(source, **kwargs):
kwargs['presets'] = ['es2015', 'react']
return babel_compile(source, **kwargs)['code']
kwargs["presets"] = ["es2015", "react"]
return babel_compile(source, **kwargs)["code"]
14 changes: 9 additions & 5 deletions dukpy/coffee.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import os
from .evaljs import evaljs

COFFEE_COMPILER = os.path.join(os.path.dirname(__file__), 'jsmodules', 'coffeescript.js')
COFFEE_COMPILER = os.path.join(
os.path.dirname(__file__), "jsmodules", "coffeescript.js"
)


def coffee_compile(source):
"""Compiles the given ``source`` from CoffeeScript to JavaScript"""
with open(COFFEE_COMPILER, 'rb') as coffeescript_js:
with open(COFFEE_COMPILER, "rb") as coffeescript_js:
return evaljs(
(coffeescript_js.read().decode('utf-8'),
'CoffeeScript.compile(dukpy.coffeecode)'),
coffeecode=source
(
coffeescript_js.read().decode("utf-8"),
"CoffeeScript.compile(dukpy.coffeecode)",
),
coffeecode=source,
)
124 changes: 75 additions & 49 deletions dukpy/evaljs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,20 @@
import json
import os
import logging
import posixpath

from dukpy.module_loader import JSModuleLoader
from . import _dukpy

try:
from collections.abc import Iterable
except ImportError: # pragma: no cover
from collections import Iterable
string_types = (bytes, str)

try: # pragma: no cover
unicode
string_types = (str, unicode)
except NameError: # pragma: no cover
string_types = (bytes, str)


log = logging.getLogger('dukpy.interpreter')
log = logging.getLogger("dukpy.interpreter")


class JSInterpreter(object):
"""JavaScript Interpreter"""

def __init__(self):
self._loader = JSModuleLoader()
self._ctx = _dukpy.create_context()
Expand All @@ -49,16 +42,16 @@ def evaljs(self, code, **kwargs):
jscode = self._adapt_code(code)

if not isinstance(jscode, bytes):
jscode = jscode.encode('utf-8')
jscode = jscode.encode("utf-8")

if not isinstance(jsvars, bytes):
jsvars = jsvars.encode('utf-8')
jsvars = jsvars.encode("utf-8")

res = _dukpy.eval_string(self, jscode, jsvars)
if res is None:
return None

return json.loads(res.decode('utf-8'))
return json.loads(res.decode("utf-8"))

def export_function(self, name, func):
"""Exports a python function to the javascript layer with the given name.
Expand All @@ -70,66 +63,99 @@ def export_function(self, name, func):
self._funcs[name] = func

def _check_exported_function_exists(self, func):
func = func.decode('ascii')
func = func.decode("ascii")
return func in self._funcs

def _call_python(self, func, json_args):
# Arguments came in reverse order from JS
func = func.decode('ascii')
json_args = json_args.decode('utf-8')
func = func.decode("ascii")
json_args = json_args.decode("utf-8")

args = list(reversed(json.loads(json_args)))
args = json.loads(json_args)
ret = self._funcs[func](*args)
if ret is not None:
return json.dumps(ret).encode('utf-8')
return json.dumps(ret).encode("utf-8")

def _init_process(self):
self.evaljs("process = {}; process.env = dukpy.environ", environ=dict(os.environ))
self.evaljs(
"process = {}; process.env = dukpy.environ", environ=dict(os.environ)
)

def _init_console(self):
self.export_function('dukpy.log.info', lambda *args: log.info(' '.join(args)))
self.export_function('dukpy.log.error', lambda *args: log.error(' '.join(args)))
self.export_function('dukpy.log.warn', lambda *args: log.warn(' '.join(args)))
self.export_function("dukpy.log.info", lambda *args: log.info(" ".join(args)))
self.export_function("dukpy.log.error", lambda *args: log.error(" ".join(args)))
self.export_function("dukpy.log.warn", lambda *args: log.warn(" ".join(args)))
self.evaljs("""
;console = {
log: function() {
call_python('dukpy.log.info', Array.prototype.join.call(arguments, ' '));
},
info: function() {
call_python('dukpy.log.info', Array.prototype.join.call(arguments, ' '));
},
warn: function() {
call_python('dukpy.log.warn', Array.prototype.join.call(arguments, ' '));
},
error: function() {
call_python('dukpy.log.error', Array.prototype.join.call(arguments, ' '));
}
};
;(function() {
globalThis.console = globalThis.console || {};
globalThis.console.log = function() {
globalThis.call_python('dukpy.log.info', Array.prototype.join.call(arguments, ' '));
};
globalThis.console.info = function() {
globalThis.call_python('dukpy.log.info', Array.prototype.join.call(arguments, ' '));
};
globalThis.console.warn = function() {
globalThis.call_python('dukpy.log.warn', Array.prototype.join.call(arguments, ' '));
};
globalThis.console.error = function() {
globalThis.call_python('dukpy.log.error', Array.prototype.join.call(arguments, ' '));
};
})();
""")

def _init_require(self):
self.export_function('dukpy.lookup_module', self._loader.load)
self.export_function("dukpy.load_module", self._load_module)
self.export_function("dukpy.normalize_module", self._normalize_module)
self.evaljs("""
;Duktape.modSearch = function (id, require, exports, module) {
var m = call_python('dukpy.lookup_module', id);
if (!m || !m[1]) {
throw new Error('cannot find module: ' + id);
;(function() {
var _dukpy_modules = {};
function _dukpy_make_require(base) {
function _dukpy_require(id) {
var resolved = call_python('dukpy.normalize_module', base || '', id) || id;
if (_dukpy_modules[resolved]) {
return _dukpy_modules[resolved].exports;
}
var m = call_python('dukpy.load_module', resolved);
if (!m || !m[1]) {
throw new Error('cannot find module: ' + id);
}
var module = { id: m[0], exports: {} };
_dukpy_modules[module.id] = module;
if (module.id !== resolved) {
_dukpy_modules[resolved] = module;
}
module.require = _dukpy_make_require(module.id);
var exports = module.exports;
var func = new Function('require', 'exports', 'module', m[1]);
func(module.require, exports, module);
return module.exports;
}
_dukpy_require.id = base || '';
return _dukpy_require;
}
_require_set_module_id(require, m[0]);
return m[1];
};
globalThis.require = _dukpy_make_require('');
})();
""")

def _normalize_module(self, base_name, module_name):
if module_name.startswith(".") and base_name:
base_dir = base_name.rsplit("/", 1)[0] if "/" in base_name else base_name
module_name = posixpath.normpath(posixpath.join(base_dir, module_name))
module_id, _ = self._loader.lookup(module_name)
return module_id or module_name

def _load_module(self, module_name):
return self._loader.load(module_name)

def _adapt_code(self, code):
def _read_files(f):
if hasattr(f, 'read'):
if hasattr(f, "read"):
return f.read()
else:
return f

code = _read_files(code)
if not isinstance(code, string_types) and hasattr(code, '__iter__'):
code = ';\n'.join(map(_read_files, code))
if not isinstance(code, string_types) and hasattr(code, "__iter__"):
code = ";\n".join(map(_read_files, code))
return code


Expand Down
Loading
Loading