You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
462 lines
15 KiB
462 lines
15 KiB
""" |
|
|
|
Python Builtins |
|
--------------- |
|
|
|
Most builtin functions (that make sense in JS) are automatically |
|
translated to JavaScript: isinstance, issubclass, callable, hasattr, |
|
getattr, setattr, delattr, print, len, max, min, chr, ord, dict, list, |
|
tuple, range, pow, sum, round, int, float, str, bool, abs, divmod, all, |
|
any, enumerate, zip, reversed, sorted, filter, map. |
|
|
|
Further all methods for list, dict and str are implemented (except str |
|
methods: encode, decode, format_map, isprintable, maketrans). |
|
|
|
.. pscript_example:: |
|
|
|
# "self" is replaced with "this" |
|
self.foo |
|
|
|
# Printing just works |
|
print('some test') |
|
print(a, b, c, sep='-') |
|
|
|
# Getting the length of a string or array |
|
len(foo) |
|
|
|
# Rounding and abs |
|
round(foo) # round to nearest integer |
|
int(foo) # round towards 0 as in Python |
|
abs(foo) |
|
|
|
# min and max |
|
min(foo) |
|
min(a, b, c) |
|
max(foo) |
|
max(a, b, c) |
|
|
|
# divmod |
|
a, b = divmod(100, 7) # -> 14, 2 |
|
|
|
# Aggregation |
|
sum(foo) |
|
all(foo) |
|
any(foo) |
|
|
|
# Turning things into numbers, bools and strings |
|
str(s) |
|
float(x) |
|
bool(y) |
|
int(z) # this rounds towards zero like in Python |
|
chr(65) # -> 'A' |
|
ord('A') # -> 65 |
|
|
|
# Turning things into lists and dicts |
|
dict([['foo', 1], ['bar', 2]]) # -> {'foo': 1, 'bar': 2} |
|
list('abc') # -> ['a', 'b', 'c'] |
|
dict(other_dict) # make a copy |
|
list(other_list) # make copy |
|
|
|
|
|
The isinstance function (and friends) |
|
------------------------------------- |
|
|
|
The ``isinstance()`` function works for all JS primitive types, but also |
|
for user-defined classes. |
|
|
|
.. pscript_example:: |
|
|
|
# Basic types |
|
isinstance(3, float) # in JS there are no ints |
|
isinstance('', str) |
|
isinstance([], list) |
|
isinstance({}, dict) |
|
isinstance(foo, types.FunctionType) |
|
|
|
# Can also use JS strings |
|
isinstance(3, 'number') |
|
isinstance('', 'string') |
|
isinstance([], 'array') |
|
isinstance({}, 'object') |
|
isinstance(foo, 'function') |
|
|
|
# You can use it on your own types too ... |
|
isinstance(x, MyClass) |
|
isinstance(x, 'MyClass') # equivalent |
|
isinstance(x, 'Object') # also yields true (subclass of Object) |
|
|
|
# issubclass works too |
|
issubclass(Foo, Bar) |
|
|
|
# As well as callable |
|
callable(foo) |
|
|
|
|
|
hasattr, getattr, setattr and delattr |
|
------------------------------------- |
|
|
|
.. pscript_example:: |
|
|
|
a = {'foo': 1, 'bar': 2} |
|
|
|
hasattr(a, 'foo') # -> True |
|
hasattr(a, 'fooo') # -> False |
|
hasattr(null, 'foo') # -> False |
|
|
|
getattr(a, 'foo') # -> 1 |
|
getattr(a, 'fooo') # -> raise AttributeError |
|
getattr(a, 'fooo', 3) # -> 3 |
|
getattr(null, 'foo', 3) # -> 3 |
|
|
|
setattr(a, 'foo', 2) |
|
|
|
delattr(a, 'foo') |
|
|
|
|
|
Creating sequences |
|
------------------ |
|
|
|
.. pscript_example:: |
|
|
|
range(10) |
|
range(2, 10, 2) |
|
range(100, 0, -1) |
|
|
|
reversed(foo) |
|
sorted(foo) |
|
enumerate(foo) |
|
zip(foo, bar) |
|
|
|
filter(func, foo) |
|
map(func, foo) |
|
|
|
|
|
List methods |
|
------------ |
|
|
|
.. pscript_example:: |
|
|
|
# Call a.append() if it exists, otherwise a.push() |
|
a.append(x) |
|
|
|
# Similar for remove() |
|
a.remove(x) |
|
|
|
|
|
Dict methods |
|
------------ |
|
|
|
.. pscript_example:: |
|
|
|
a = {'foo': 3} |
|
a['foo'] |
|
a.get('foo', 0) |
|
a.get('foo') |
|
a.keys() |
|
|
|
|
|
Str methods |
|
----------- |
|
|
|
.. pscript_example:: |
|
|
|
"foobar".startswith('foo') |
|
"foobar".replace('foo', 'bar') |
|
"foobar".upper() |
|
|
|
|
|
Using JS specific functionality |
|
------------------------------- |
|
|
|
When writing PScript inside Python modules, we recommend that where |
|
specific JavaScript functionality is used, that the references are |
|
prefixed with ``window.`` Where ``window`` represents the global JS |
|
namespace. All global JavaScript objects, functions, and variables |
|
automatically become members of the ``window`` object. This helps |
|
make it clear that the functionality is specific to JS, and also |
|
helps static code analysis tools like flake8. |
|
|
|
.. pscript_example:: |
|
|
|
from pscript import window # this is a stub |
|
def foo(a): |
|
return window.Math.cos(a) |
|
|
|
Aside from ``window``, ``pscript`` also provides ``undefined``, |
|
``Inifinity``, and ``NaN``. |
|
|
|
""" |
|
|
|
from . import commonast as ast |
|
from . import stdlib |
|
from .parser2 import Parser2, JSError, unify # noqa |
|
from .stubs import RawJS |
|
|
|
|
|
# This class has several `function_foo()` and `method_bar()` methods |
|
# to implement corresponding functionality. Most of these are |
|
# auto-generated from the stdlib. However, some methods need explicit |
|
# implementation, e.g. to parse keyword arguments, or are inlined rather |
|
# than implemented via the stlib. |
|
# |
|
# Note that when the number of arguments does not match, almost all |
|
# functions raise a compile-time error. The methods, however, will |
|
# bypass the stdlib in this case, because it is assumed that the user |
|
# intended to call a special method on the object. |
|
|
|
|
|
class Parser3(Parser2): |
|
""" Parser to transcompile Python to JS, allowing more Pythonic |
|
code, like ``self``, ``print()``, ``len()``, list methods, etc. |
|
""" |
|
|
|
def function_this_is_js(self, node): |
|
# Note that we handle this_is_js() shortcuts in the if-statement |
|
# directly. This replacement with a string is when this_is_js() |
|
# is used outside an if statement. |
|
if len(node.arg_nodes) != 0: |
|
raise JSError('this_is_js() expects zero arguments.') |
|
return ('"this_is_js()"') |
|
|
|
def function_RawJS(self, node): |
|
if len(node.arg_nodes) == 1: |
|
if not isinstance(node.arg_nodes[0], ast.Str): |
|
raise JSError('RawJS needs a verbatim string (use multiple ' |
|
'args to bypass PScript\'s RawJS).') |
|
lines = RawJS._str2lines(node.arg_nodes[0].value.strip()) |
|
nl = '\n' + (self._indent * 4) * ' ' |
|
return nl.join(lines) |
|
else: |
|
return None # maybe RawJS is a thing |
|
|
|
## Python builtin functions |
|
|
|
|
|
def function_isinstance(self, node): |
|
if len(node.arg_nodes) != 2: |
|
raise JSError('isinstance() expects two arguments.') |
|
|
|
ob = unify(self.parse(node.arg_nodes[0])) |
|
cls = unify(self.parse(node.arg_nodes[1])) |
|
if cls[0] in '"\'': |
|
cls = cls[1:-1] # remove quotes |
|
|
|
BASIC_TYPES = ('number', 'boolean', 'string', 'function', 'array', |
|
'object', 'null', 'undefined') |
|
|
|
MAP = {'[int, float]': 'number', '[float, int]': 'number', 'float': 'number', |
|
'str': 'string', 'basestring': 'string', 'string_types': 'string', |
|
'bool': 'boolean', |
|
'FunctionType': 'function', 'types.FunctionType': 'function', |
|
'list': 'array', 'tuple': 'array', |
|
'[list, tuple]': 'array', '[tuple, list]': 'array', |
|
'dict': 'object', |
|
} |
|
|
|
cmp = MAP.get(cls, cls) |
|
|
|
if cmp == 'array': |
|
return ['Array.isArray(', ob, ')'] |
|
elif cmp.lower() in BASIC_TYPES: |
|
# Basic type, use Object.prototype.toString |
|
return ["Object.prototype.toString.call(", ob , |
|
").slice(8,-1).toLowerCase() === '%s'" % cmp.lower()] |
|
# In http://stackoverflow.com/questions/11108877 the following is |
|
# proposed, which might be better in theory, but is > 50% slower |
|
return ["({}).toString.call(", |
|
ob, |
|
r").match(/\s([a-zA-Z]+)/)[1].toLowerCase() === ", |
|
"'%s'" % cmp.lower() |
|
] |
|
else: |
|
# User defined type, use instanceof |
|
# http://tobyho.com/2011/01/28/checking-types-in-javascript/ |
|
cmp = unify(cls) |
|
if cmp[0] == '(': |
|
raise JSError('isinstance() can only compare to simple types') |
|
return ob, " instanceof ", cmp |
|
|
|
def function_issubclass(self, node): |
|
# issubclass only needs to work on custom classes |
|
if len(node.arg_nodes) != 2: |
|
raise JSError('issubclass() expects two arguments.') |
|
|
|
cls1 = unify(self.parse(node.arg_nodes[0])) |
|
cls2 = unify(self.parse(node.arg_nodes[1])) |
|
if cls2 == 'object': |
|
cls2 = 'Object' |
|
return '(%s.prototype instanceof %s)' % (cls1, cls2) |
|
|
|
def function_print(self, node): |
|
# Process keywords |
|
sep, end = '" "', '' |
|
for kw in node.kwarg_nodes: |
|
if kw.name == 'sep': |
|
sep = ''.join(self.parse(kw.value_node)) |
|
elif kw.name == 'end': |
|
end = ''.join(self.parse(kw.value_node)) |
|
elif kw.name in ('file', 'flush'): |
|
raise JSError('print() file and flush args not supported') |
|
else: |
|
raise JSError('Invalid argument for print(): %r' % kw.name) |
|
|
|
# Combine args |
|
args = [unify(self.parse(arg)) for arg in node.arg_nodes] |
|
end = (" + %s" % end) if (args and end and end != '\n') else '' |
|
combiner = ' + %s + ' % sep |
|
args_concat = combiner.join(args) or '""' |
|
return 'console.log(' + args_concat + end + ')' |
|
|
|
def function_len(self, node): |
|
if len(node.arg_nodes) == 1: |
|
return unify(self.parse(node.arg_nodes[0])), '.length' |
|
else: |
|
return None # don't apply this feature |
|
|
|
def function_max(self, node): |
|
if len(node.arg_nodes) == 0: |
|
raise JSError('max() needs at least one argument') |
|
elif len(node.arg_nodes) == 1: |
|
arg = ''.join(self.parse(node.arg_nodes[0])) |
|
return 'Math.max.apply(null, ', arg, ')' |
|
else: |
|
args = ', '.join([unify(self.parse(arg)) for arg in node.arg_nodes]) |
|
return 'Math.max(', args, ')' |
|
|
|
def function_min(self, node): |
|
if len(node.arg_nodes) == 0: |
|
raise JSError('min() needs at least one argument') |
|
elif len(node.arg_nodes) == 1: |
|
arg = ''.join(self.parse(node.arg_nodes[0])) |
|
return 'Math.min.apply(null, ', arg, ')' |
|
else: |
|
args = ', '.join([unify(self.parse(arg)) for arg in node.arg_nodes]) |
|
return 'Math.min(', args, ')' |
|
|
|
def function_callable(self, node): |
|
if len(node.arg_nodes) == 1: |
|
arg = unify(self.parse(node.arg_nodes[0])) |
|
return '(typeof %s === "function")' % arg |
|
else: |
|
raise JSError('callable() needs at least one argument') |
|
|
|
def function_chr(self, node): |
|
if len(node.arg_nodes) == 1: |
|
arg = ''.join(self.parse(node.arg_nodes[0])) |
|
return 'String.fromCharCode(%s)' % arg |
|
else: |
|
raise JSError('chr() needs at least one argument') |
|
|
|
def function_ord(self, node): |
|
if len(node.arg_nodes) == 1: |
|
arg = ''.join(self.parse(node.arg_nodes[0])) |
|
return '%s.charCodeAt(0)' % arg |
|
else: |
|
raise JSError('ord() needs at least one argument') |
|
|
|
def function_dict(self, node): |
|
if len(node.arg_nodes) == 0: |
|
kwargs = ['%s:%s' % (arg.name, unify(self.parse(arg.value_node))) |
|
for arg in node.kwarg_nodes] |
|
return '{%s}' % ', '.join(kwargs) |
|
if len(node.arg_nodes) == 1: |
|
return self.use_std_function('dict', node.arg_nodes) |
|
else: |
|
raise JSError('dict() needs at least one argument') |
|
|
|
def function_list(self, node): |
|
if len(node.arg_nodes) == 0: |
|
return '[]' |
|
if len(node.arg_nodes) == 1: |
|
return self.use_std_function('list', node.arg_nodes) |
|
else: |
|
raise JSError('list() needs at least one argument') |
|
|
|
def function_tuple(self, node): |
|
return self.function_list(node) |
|
|
|
def function_range(self, node): |
|
if len(node.arg_nodes) == 1: |
|
args = ast.Num(0), node.arg_nodes[0], ast.Num(1) |
|
return self.use_std_function('range', args) |
|
elif len(node.arg_nodes) == 2: |
|
args = node.arg_nodes[0], node.arg_nodes[1], ast.Num(1) |
|
return self.use_std_function('range', args) |
|
elif len(node.arg_nodes) == 3: |
|
return self.use_std_function('range', node.arg_nodes) |
|
else: |
|
raise JSError('range() needs 1, 2 or 3 arguments') |
|
|
|
def function_sorted(self, node): |
|
if len(node.arg_nodes) == 1: |
|
key, reverse = ast.Name('undefined'), ast.NameConstant(False) |
|
for kw in node.kwarg_nodes: |
|
if kw.name == 'key': |
|
key = kw.value_node |
|
elif kw.name == 'reverse': |
|
reverse = kw.value_node |
|
else: |
|
raise JSError('Invalid keyword argument for sorted: %r' % kw.name) |
|
return self.use_std_function('sorted', [node.arg_nodes[0], key, reverse]) |
|
else: |
|
raise JSError('sorted() needs one argument') |
|
|
|
## Methods of list/dict/str |
|
|
|
def method_sort(self, node, base): |
|
if len(node.arg_nodes) == 0: # sorts args are keyword-only |
|
key, reverse = ast.Name('undefined'), ast.NameConstant(False) |
|
for kw in node.kwarg_nodes: |
|
if kw.name == 'key': |
|
key = kw.value_node |
|
elif kw.name == 'reverse': |
|
reverse = kw.value_node |
|
else: |
|
raise JSError('Invalid keyword argument for sort: %r' % kw.name) |
|
return self.use_std_method(base, 'sort', [key, reverse]) |
|
|
|
def method_format(self, node, base): |
|
if node.kwarg_nodes: |
|
raise JSError('Method format() does not support keyword args.') |
|
return self.use_std_method(base, 'format', node.arg_nodes) |
|
|
|
|
|
# Add functions and methods to the class, using the stdib functions ... |
|
|
|
def make_function(name, nargs, function_deps, method_deps): |
|
def function_X(self, node): |
|
if node.kwarg_nodes: |
|
raise JSError('Function %s does not support keyword args.' % name) |
|
if len(node.arg_nodes) not in nargs: |
|
raise JSError('Function %s needs #args in %r.' % (name, nargs)) |
|
for dep in function_deps: |
|
self.use_std_function(dep, []) |
|
for dep in method_deps: |
|
self.use_std_method('x', dep, []) |
|
return self.use_std_function(name, node.arg_nodes) |
|
return function_X |
|
|
|
def make_method(name, nargs, function_deps, method_deps): |
|
def method_X(self, node, base): |
|
if node.kwarg_nodes: |
|
raise JSError('Method %s does not support keyword args.' % name) |
|
if len(node.arg_nodes) not in nargs: |
|
return None # call as-is, don't use our variant |
|
for dep in function_deps: |
|
self.use_std_function(dep, []) |
|
for dep in method_deps: |
|
self.use_std_method('x', dep, []) |
|
return self.use_std_method(base, name, node.arg_nodes) |
|
return method_X |
|
|
|
for name, code in stdlib.METHODS.items(): |
|
nargs, function_deps, method_deps = stdlib.get_std_info(code) |
|
if nargs and not hasattr(Parser3, 'method_' + name): |
|
m = make_method(name, tuple(nargs), function_deps, method_deps) |
|
setattr(Parser3, 'method_' + name, m) |
|
|
|
for name, code in stdlib.FUNCTIONS.items(): |
|
nargs, function_deps, method_deps = stdlib.get_std_info(code) |
|
if nargs and not hasattr(Parser3, 'function_' + name): |
|
m = make_function(name, tuple(nargs), function_deps, method_deps) |
|
setattr(Parser3, 'function_' + name, m)
|
|
|