SickGear/lib/js2py/internals/base.py

931 lines
32 KiB
Python
Raw Permalink Normal View History

from __future__ import unicode_literals
import re
import datetime
from .desc import *
from .simplex import *
from .conversions import *
from pyjsparser import PyJsParser
import six
if six.PY2:
from itertools import izip
else:
izip = zip
def Type(obj):
return obj.TYPE
# 8.6.2
class PyJs(object):
TYPE = 'Object'
IS_CONSTRUCTOR = False
prototype = None
Class = None
extensible = True
value = None
own = {}
def get_member(self, unconverted_prop):
return self.get(to_string(unconverted_prop))
def put_member(self, unconverted_prop, val):
return self.put(to_string(unconverted_prop), val)
def get(self, prop):
assert type(prop) == unicode
cand = self.get_property(prop)
if cand is None:
return undefined
if is_data_descriptor(cand):
return cand['value']
if is_undefined(cand['get']):
return undefined
return cand['get'].call(self)
def get_own_property(self, prop):
assert type(prop) == unicode
# takes py returns py
return self.own.get(prop)
def get_property(self, prop):
assert type(prop) == unicode
# take py returns py
cand = self.get_own_property(prop)
if cand:
return cand
if self.prototype is not None:
return self.prototype.get_property(prop)
def put(self, prop, val, throw=False):
assert type(prop) == unicode
# takes py, returns none
if not self.can_put(prop):
if throw:
raise MakeError('TypeError', 'Could not define own property')
return
own_desc = self.get_own_property(prop)
if is_data_descriptor(own_desc):
self.own[prop]['value'] = val
return
desc = self.get_property(prop)
if is_accessor_descriptor(desc):
desc['set'].call(
self, (val, )) # calling setter on own or inherited element
else: # new property
self.own[prop] = {
'value': val,
'writable': True,
'configurable': True,
'enumerable': True
}
def can_put(self, prop): # to check
assert type(prop) == unicode, type(prop)
# takes py returns py
desc = self.get_own_property(prop)
if desc: # if we have this property
if is_accessor_descriptor(desc):
return is_callable(
desc['set']) # Check if setter method is defined
else: # data desc
return desc['writable']
if self.prototype is None:
return self.extensible
inherited = self.prototype.get_property(prop)
if inherited is None:
return self.extensible
if is_accessor_descriptor(inherited):
return not is_undefined(inherited['set'])
elif self.extensible:
return inherited['writable'] # weird...
return False
def has_property(self, prop):
assert type(prop) == unicode
# takes py returns Py
return self.get_property(prop) is not None
def delete(self, prop, throw=False):
assert type(prop) == unicode
# takes py, returns py
desc = self.get_own_property(prop)
if desc is None:
return True
if desc['configurable']:
del self.own[prop]
return True
if throw:
raise MakeError('TypeError', 'Could not define own property')
return False
def default_value(self, hint=None):
order = ('valueOf', 'toString')
if hint == 'String' or (hint is None and self.Class == 'Date'):
order = ('toString', 'valueOf')
for meth_name in order:
method = self.get(meth_name)
if method is not None and is_callable(method):
cand = method.call(self, ())
if is_primitive(cand):
return cand
raise MakeError('TypeError',
'Cannot convert object to primitive value')
def define_own_property(
self, prop, desc,
throw): # Internal use only. External through Object
assert type(prop) == unicode
# takes Py, returns Py
# prop must be a Py string. Desc is either a descriptor or accessor.
# Messy method - raw translation from Ecma spec to prevent any bugs. # todo check this
current = self.get_own_property(prop)
extensible = self.extensible
if not current: # We are creating a new OWN property
if not extensible:
if throw:
raise MakeError('TypeError',
'Could not define own property')
return False
# extensible must be True
if is_data_descriptor(desc) or is_generic_descriptor(desc):
DEFAULT_DATA_DESC = {
'value': undefined, # undefined
'writable': False,
'enumerable': False,
'configurable': False
}
DEFAULT_DATA_DESC.update(desc)
self.own[prop] = DEFAULT_DATA_DESC
else:
DEFAULT_ACCESSOR_DESC = {
'get': undefined, # undefined
'set': undefined, # undefined
'enumerable': False,
'configurable': False
}
DEFAULT_ACCESSOR_DESC.update(desc)
self.own[prop] = DEFAULT_ACCESSOR_DESC
return True
# therefore current exists!
if not desc or desc == current: # We don't need to change anything.
return True
configurable = current['configurable']
if not configurable: # Prevent changing params
if desc.get('configurable'):
if throw:
raise MakeError('TypeError',
'Could not define own property')
return False
if 'enumerable' in desc and desc['enumerable'] != current[
'enumerable']:
if throw:
raise MakeError('TypeError',
'Could not define own property')
return False
if is_generic_descriptor(desc):
pass
elif is_data_descriptor(current) != is_data_descriptor(desc):
# we want to change the current type of property
if not configurable:
if throw:
raise MakeError('TypeError',
'Could not define own property')
return False
if is_data_descriptor(current): # from data to setter
del current['value']
del current['writable']
current['set'] = undefined # undefined
current['get'] = undefined # undefined
else: # from setter to data
del current['set']
del current['get']
current['value'] = undefined # undefined
current['writable'] = False
elif is_data_descriptor(current) and is_data_descriptor(desc):
if not configurable:
if not current['writable'] and desc.get('writable'):
if throw:
raise MakeError('TypeError',
'Could not define own property')
return False
if not current['writable'] and 'value' in desc and current[
'value'] != desc['value']:
if throw:
raise MakeError('TypeError',
'Could not define own property')
return False
elif is_accessor_descriptor(current) and is_accessor_descriptor(desc):
if not configurable:
if 'set' in desc and desc['set'] != current['set']:
if throw:
raise MakeError('TypeError',
'Could not define own property')
return False
if 'get' in desc and desc['get'] != current['get']:
if throw:
raise MakeError('TypeError',
'Could not define own property')
return False
current.update(desc)
return True
def create(self, args, space):
'''Generally not a constructor, raise an error'''
raise MakeError('TypeError', '%s is not a constructor' % self.Class)
def get_member(
self, prop, space
): # general member getter, prop has to be unconverted prop. it is it can be any value
typ = type(self)
if typ not in PRIMITIVES: # most likely getter for object
return self.get_member(
prop
) # <- object can implement this to support faster prop getting. ex array.
elif typ == unicode: # then probably a String
if type(prop) == float and is_finite(prop):
index = int(prop)
if index == prop and 0 <= index < len(self):
return self[index]
s_prop = to_string(prop)
if s_prop == 'length':
return float(len(self))
elif s_prop.isdigit():
index = int(s_prop)
if 0 <= index < len(self):
return self[index]
# use standard string prototype
return space.StringPrototype.get(s_prop)
# maybe an index
elif typ == float:
# use standard number prototype
return space.NumberPrototype.get(to_string(prop))
elif typ == bool:
return space.BooleanPrototype.get(to_string(prop))
elif typ is UNDEFINED_TYPE:
raise MakeError('TypeError',
"Cannot read property '%s' of undefined" % prop)
elif typ is NULL_TYPE:
raise MakeError('TypeError',
"Cannot read property '%s' of null" % prop)
else:
raise RuntimeError('Unknown type! - ' + repr(typ))
def get_member_dot(self, prop, space):
# dot member getter, prop has to be unicode
typ = type(self)
if typ not in PRIMITIVES: # most likely getter for object
return self.get(prop)
elif typ == unicode: # then probably a String
if prop == 'length':
return float(len(self))
elif prop.isdigit():
index = int(prop)
if 0 <= index < len(self):
return self[index]
else:
# use standard string prototype
return space.StringPrototype.get(prop)
# maybe an index
elif typ == float:
# use standard number prototype
return space.NumberPrototype.get(prop)
elif typ == bool:
return space.BooleanPrototype.get(prop)
elif typ in (UNDEFINED_TYPE, NULL_TYPE):
raise MakeError('TypeError',
"Cannot read property '%s' of undefined" % prop)
else:
raise RuntimeError('Unknown type! - ' + repr(typ))
# Object
class PyJsObject(PyJs):
TYPE = 'Object'
Class = 'Object'
def __init__(self, prototype=None):
self.prototype = prototype
self.own = {}
def _init(self, props, vals):
i = 0
for prop, kind in props:
if prop in self.own: # just check... probably will not happen very often.
if is_data_descriptor(self.own[prop]):
if kind != 'i':
raise MakeError(
'SyntaxError',
'Invalid object initializer! Duplicate property name "%s"'
% prop)
else:
if kind == 'i' or (kind == 'g' and 'get' in self.own[prop]
) or (kind == 's'
and 'set' in self.own[prop]):
raise MakeError(
'SyntaxError',
'Invalid object initializer! Duplicate setter/getter of prop: "%s"'
% prop)
if kind == 'i': # init
self.own[prop] = {
'value': vals[i],
'writable': True,
'enumerable': True,
'configurable': True
}
elif kind == 'g': # get
self.define_own_property(prop, {
'get': vals[i],
'enumerable': True,
'configurable': True
}, False)
elif kind == 's': # get
self.define_own_property(prop, {
'get': vals[i],
'enumerable': True,
'configurable': True
}, False)
else:
raise RuntimeError(
'Invalid property kind - %s. Expected one of i, g, s.' %
repr(kind))
i += 1
def _set_props(self, prop_descs):
for prop, desc in six.iteritems(prop_descs):
self.define_own_property(prop, desc)
# Array
# todo Optimise Array - extremely slow due to index conversions from str to int and back etc.
# solution - make get and put methods callable with any type of prop and handle conversions from inside
# if not array then use to_string(prop). In array if prop is integer then just use it
# also consider removal of these stupid writable, enumerable etc for ints.
class PyJsArray(PyJs):
Class = 'Array'
def __init__(self, length, prototype=None):
self.prototype = prototype
self.own = {
'length': {
'value': float(length),
'writable': True,
'enumerable': False,
'configurable': False
}
}
def _init(self, elements):
for i, ele in enumerate(elements):
if ele is None: continue
self.own[unicode(i)] = {
'value': ele,
'writable': True,
'enumerable': True,
'configurable': True
}
def put(self, prop, val, throw=False):
assert type(val) != int
# takes py, returns none
if not self.can_put(prop):
if throw:
raise MakeError('TypeError', 'Could not define own property')
return
own_desc = self.get_own_property(prop)
if is_data_descriptor(own_desc):
self.define_own_property(prop, {'value': val}, False)
return
desc = self.get_property(prop)
if is_accessor_descriptor(desc):
desc['set'].call(
self, (val, )) # calling setter on own or inherited element
else: # new property
self.define_own_property(
prop, {
'value': val,
'writable': True,
'configurable': True,
'enumerable': True
}, False)
def define_own_property(self, prop, desc, throw):
assert type(desc.get('value')) != int
old_len_desc = self.get_own_property('length')
old_len = old_len_desc['value'] # value is js type so convert to py.
if prop == 'length':
if 'value' not in desc:
return PyJs.define_own_property(self, prop, desc, False)
new_len = to_uint32(desc['value'])
if new_len != to_number(desc['value']):
raise MakeError('RangeError', 'Invalid range!')
new_desc = dict((k, v) for k, v in six.iteritems(desc))
new_desc['value'] = float(new_len)
if new_len >= old_len:
return PyJs.define_own_property(self, prop, new_desc, False)
if not old_len_desc['writable']:
return False
if 'writable' not in new_desc or new_desc['writable'] == True:
new_writable = True
else:
new_writable = False
new_desc['writable'] = True
if not PyJs.define_own_property(self, prop, new_desc, False):
return False
if new_len < old_len:
# not very efficient for sparse arrays, so using different method for sparse:
if old_len > 30 * len(self.own):
for ele in self.own.keys():
if ele.isdigit() and int(ele) >= new_len:
if not self.delete(
ele
): # if failed to delete set len to current len and reject.
new_desc['value'] = old_len + 1.
if not new_writable:
new_desc['writable'] = False
PyJs.define_own_property(
self, prop, new_desc, False)
return False
old_len = new_len
else: # standard method
while new_len < old_len:
old_len -= 1
if not self.delete(
unicode(int(old_len))
): # if failed to delete set len to current len and reject.
new_desc['value'] = old_len + 1.
if not new_writable:
new_desc['writable'] = False
PyJs.define_own_property(self, prop, new_desc,
False)
return False
if not new_writable:
self.own['length']['writable'] = False
return True
elif prop.isdigit():
index = to_uint32(prop)
if index >= old_len and not old_len_desc['writable']:
return False
if not PyJs.define_own_property(self, prop, desc, False):
return False
if index >= old_len:
old_len_desc['value'] = index + 1.
return True
else:
return PyJs.define_own_property(self, prop, desc, False)
def to_list(self):
return [
self.get(str(e)) for e in xrange(self.get('length').to_uint32())
]
# database with compiled patterns. Js pattern -> Py pattern.
REGEXP_DB = {}
class PyJsRegExp(PyJs):
Class = 'RegExp'
def __init__(self, body, flags, prototype=None):
self.prototype = prototype
self.glob = True if 'g' in flags else False
self.ignore_case = re.IGNORECASE if 'i' in flags else 0
self.multiline = re.MULTILINE if 'm' in flags else 0
self.value = body
if (body, flags) in REGEXP_DB:
self.pat = REGEXP_DB[body, flags]
else:
comp = None
try:
# converting JS regexp pattern to Py pattern.
possible_fixes = [(u'[]', u'[\0]'), (u'[^]', u'[^\0]'),
(u'nofix1791', u'nofix1791')]
reg = self.value
for fix, rep in possible_fixes:
comp = PyJsParser()._interpret_regexp(reg, flags)
#print 'reg -> comp', reg, '->', comp
try:
self.pat = re.compile(
comp, self.ignore_case | self.multiline)
#print reg, '->', comp
break
except:
reg = reg.replace(fix, rep)
# print 'Fix', fix, '->', rep, '=', reg
else:
raise Exception()
REGEXP_DB[body, flags] = self.pat
except:
#print 'Invalid pattern...', self.value, comp
raise MakeError(
'SyntaxError',
'Invalid RegExp pattern: %s -> %s' % (repr(self.value),
repr(comp)))
# now set own properties:
self.own = {
'source': {
'value': self.value,
'enumerable': False,
'writable': False,
'configurable': False
},
'global': {
'value': self.glob,
'enumerable': False,
'writable': False,
'configurable': False
},
'ignoreCase': {
'value': bool(self.ignore_case),
'enumerable': False,
'writable': False,
'configurable': False
},
'multiline': {
'value': bool(self.multiline),
'enumerable': False,
'writable': False,
'configurable': False
},
'lastIndex': {
'value': 0.,
'enumerable': False,
'writable': True,
'configurable': False
}
}
def match(self, string, pos):
'''string is of course a py string'''
return self.pat.match(string, int(pos))
class PyJsError(PyJs):
Class = 'Error'
extensible = True
def __init__(self, message=None, prototype=None):
self.prototype = prototype
self.own = {}
if message is not None:
self.put('message', to_string(message))
self.own['message']['enumerable'] = False
class PyJsDate(PyJs):
Class = 'Date'
UTCToLocal = None # todo UTC to local should be imported!
def __init__(self, value, prototype=None):
self.value = value
self.own = {}
self.prototype = prototype
# todo fix this problematic datetime part
def to_local_dt(self):
return datetime.datetime(1970, 1, 1) + datetime.timedelta(
seconds=self.UTCToLocal(self.value) // 1000)
def to_utc_dt(self):
return datetime.datetime(1970, 1, 1) + datetime.timedelta(
seconds=self.value // 1000)
def local_strftime(self, pattern):
if self.value is NaN:
return 'Invalid Date'
try:
dt = self.to_local_dt()
except:
raise MakeError(
'TypeError',
'unsupported date range. Will fix in future versions')
try:
return dt.strftime(pattern)
except:
raise MakeError(
'TypeError',
'Could not generate date string from this date (limitations of python.datetime)'
)
def utc_strftime(self, pattern):
if self.value is NaN:
return 'Invalid Date'
try:
dt = self.to_utc_dt()
except:
raise MakeError(
'TypeError',
'unsupported date range. Will fix in future versions')
try:
return dt.strftime(pattern)
except:
raise MakeError(
'TypeError',
'Could not generate date string from this date (limitations of python.datetime)'
)
# Scope class it will hold all the variables accessible to user
class Scope(PyJs):
Class = 'Global'
extensible = True
IS_CHILD_SCOPE = True
THIS_BINDING = None
space = None
exe = None
# todo speed up!
# in order to speed up this very important class the top scope should behave differently than
# child scopes, child scope should not have this property descriptor thing because they cant be changed anyway
# they are all confugurable= False
def __init__(self, scope, space, parent=None):
"""Doc"""
self.space = space
self.prototype = parent
if type(scope) is not dict:
assert parent is not None, 'You initialised the WITH_SCOPE without a parent scope.'
self.own = scope
self.is_with_scope = True
else:
self.is_with_scope = False
if parent is None:
# global, top level scope
self.own = {}
for k, v in six.iteritems(scope):
# set all the global items
self.define_own_property(
k, {
'value': v,
'configurable': False,
'writable': False,
'enumerable': False
}, False)
else:
# not global, less powerful but faster closure.
self.own = scope # simple dictionary which maps name directly to js object.
self.par = super(Scope, self)
self.stack = []
def register(self, var):
# registered keeps only global registered variables
if self.prototype is None:
# define in global scope
if var in self.own:
self.own[var]['configurable'] = False
else:
self.define_own_property(
var, {
'value': undefined,
'configurable': False,
'writable': True,
'enumerable': True
}, False)
elif var not in self.own:
# define in local scope since it has not been defined yet
self.own[var] = undefined # default value
def registers(self, vars):
"""register multiple variables"""
for var in vars:
self.register(var)
def put(self, var, val, throw=False):
if self.prototype is None:
desc = self.own.get(var) # global scope
if desc is None:
self.par.put(var, val, False)
else:
if desc['writable']: # todo consider getters/setters
desc['value'] = val
else:
if self.is_with_scope:
if self.own.has_property(var):
return self.own.put(var, val, throw=throw)
else:
return self.prototype.put(var, val)
# trying to put in local scope
# we dont know yet in which scope we should place this var
elif var in self.own:
self.own[var] = val
return val
else:
# try to put in the lower scope since we cant put in this one (var wasn't registered)
return self.prototype.put(var, val)
def get(self, var, throw=False):
if self.prototype is not None:
if self.is_with_scope:
cand = None if not self.own.has_property(
var) else self.own.get(var)
else:
# fast local scope
cand = self.own.get(var)
if cand is None:
return self.prototype.get(var, throw)
return cand
# slow, global scope
if var not in self.own:
# try in ObjectPrototype...
if var in self.space.ObjectPrototype.own:
return self.space.ObjectPrototype.get(var)
if throw:
raise MakeError('ReferenceError', '%s is not defined' % var)
return undefined
cand = self.own[var].get('value')
return cand if cand is not None else self.own[var]['get'].call(self)
def delete(self, var, throw=False):
if self.prototype is not None:
if self.is_with_scope:
if self.own.has_property(var):
return self.own.delete(var)
elif var in self.own:
return False
return self.prototype.delete(var)
# we are in global scope here. Must exist and be configurable to delete
if var not in self.own:
# this var does not exist, why do you want to delete it???
return True
if self.own[var]['configurable']:
del self.own[var]
return True
# not configurable, cant delete
return False
def get_new_arguments_obj(args, space):
obj = space.NewObject()
obj.Class = 'Arguments'
obj.define_own_property(
'length', {
'value': float(len(args)),
'writable': True,
'enumerable': False,
'configurable': True
}, False)
for i, e in enumerate(args):
obj.put(unicode(i), e)
return obj
#Function
class PyJsFunction(PyJs):
Class = 'Function'
source = '{ [native code] }'
IS_CONSTRUCTOR = True
def __init__(self,
code,
ctx,
params,
name,
space,
is_declaration,
definitions,
prototype=None):
self.prototype = prototype
self.own = {}
self.code = code
if type(
self.code
) == int: # just a label pointing to the beginning of the code.
self.is_native = False
else:
self.is_native = True # python function
self.ctx = ctx
self.params = params
self.arguments_in_params = 'arguments' in params
self.definitions = definitions
# todo remove this check later
for p in params:
assert p in self.definitions
self.name = name
self.space = space
self.is_declaration = is_declaration
#set own property length to the number of arguments
self.own['length'] = {
'value': float(len(params)),
'writable': False,
'enumerable': False,
'configurable': False
}
if name:
self.own['name'] = {
'value': name,
'writable': False,
'enumerable': False,
'configurable': True
}
if not self.is_native: # set prototype for user defined functions
# constructor points to this function
proto = space.NewObject()
proto.own['constructor'] = {
'value': self,
'writable': True,
'enumerable': False,
'configurable': True
}
self.own['prototype'] = {
'value': proto,
'writable': True,
'enumerable': False,
'configurable': False
}
# todo set up throwers on callee and arguments if in strict mode
def call(self, this, args=()):
''' Dont use this method from inside bytecode to call other bytecode. '''
if self.is_native:
_args = SpaceTuple(
args
) # we have to do that unfortunately to pass all the necessary info to the funcs
_args.space = self.space
return self.code(
this, _args
) # must return valid js object - undefined, null, float, unicode, bool, or PyJs
else:
return self.space.exe._call(self, this,
args) # will run inside bytecode
def has_instance(self, other):
# I am not sure here so instanceof may not work lol.
if not is_object(other):
return False
proto = self.get('prototype')
if not is_object(proto):
raise MakeError(
'TypeError',
'Function has non-object prototype in instanceof check')
while True:
other = other.prototype
if not other: # todo make sure that the condition is not None or null
return False
if other is proto:
return True
def create(self, args, space):
proto = self.get('prototype')
if not is_object(proto):
proto = space.ObjectPrototype
new = PyJsObject(prototype=proto)
res = self.call(new, args)
if is_object(res):
return res
return new
def _generate_my_context(self, this, args):
my_ctx = Scope(
dict(izip(self.params, args)), self.space, parent=self.ctx)
my_ctx.registers(self.definitions)
my_ctx.THIS_BINDING = this
if not self.arguments_in_params:
my_ctx.own['arguments'] = get_new_arguments_obj(args, self.space)
if not self.is_declaration and self.name and self.name not in my_ctx.own:
my_ctx.own[
self.
name] = self # this should be immutable binding but come on!
return my_ctx
class SpaceTuple:
def __init__(self, tup):
self.tup = tup
def __len__(self):
return len(self.tup)
def __getitem__(self, item):
return self.tup[item]
def __iter__(self):
return iter(self.tup)