PHP Classes

File: src/python/Xpresion.py

Recommend this page to a friend!
  Classes of Nikos M.  >  Xpresion PHP Parser Engine  >  src/python/Xpresion.py  >  Download  
File: src/python/Xpresion.py
Role: Auxiliary data
Content type: text/plain
Description: Auxiliary data
Class: Xpresion PHP Parser Engine
Parse expressions that may contain variables
Author: By
Last change:
Date: 2 years ago
Size: 90,075 bytes
 

Contents

Class file image Download
# -*- coding: UTF-8 -*- ## # # Xpresion # Simple eXpression parser engine with variables and custom functions support for PHP, Python, Node.js and Browser # @version: 1.0.1 # # https://github.com/foo123/Xpresion # ## import re, time, datetime, calendar, math, pprint from collections import namedtuple # https://github.com/foo123/GrammarTemplate def pad( s, n, z='0', pad_right=False ): ps = str(s) if pad_right: while len(ps) < n: ps += z else: while len(ps) < n: ps = z + ps return ps GUID = 0 def guid( ): global GUID GUID += 1 return pad(hex(int(time.time()))[2:],12)+'--'+pad(hex(GUID)[2:],4) def is_array( v ): return isinstance(v, (list,tuple)) def compute_alignment( s, i, l ): alignment = '' while i < l: c = s[i] if (" " == c) or ("\r" == c) or ("\t" == c) or ("\v" == c) or ("\0" == c): alignment += c i += 1 else: break return alignment def align( s, alignment ): l = len(s) if l and len(alignment): aligned = ''; for c in s: aligned += c if "\n" == c: aligned += alignment else: aligned = s return aligned def walk( obj, keys, keys_alt=None, obj_alt=None ): found = 0 if keys: o = obj l = len(keys) i = 0 found = 1 while i < l: k = keys[i] i += 1 if o is not None: if isinstance(o,(list,tuple)) and int(k)<len(o): o = o[int(k)] elif isinstance(o,dict) and (k in o): o = o[k] else: try: o = getattr(o, k) except AttributeError: found = 0 break else: found = 0 break if (not found) and keys_alt: o = obj l = len(keys_alt) i = 0 found = 1 while i < l: k = keys_alt[i] i += 1 if o is not None: if isinstance(o,(list,tuple)) and int(k)<len(o): o = o[int(k)] elif isinstance(o,dict) and (k in o): o = o[k] else: try: o = getattr(o, k) except AttributeError: found = 0 break else: found = 0 break if (not found) and (obj_alt is not None) and (obj_alt is not obj): if keys: o = obj_alt l = len(keys) i = 0 found = 1 while i < l: k = keys[i] i += 1 if o is not None: if isinstance(o,(list,tuple)) and int(k)<len(o): o = o[int(k)] elif isinstance(o,dict) and (k in o): o = o[k] else: try: o = getattr(o, k) except AttributeError: found = 0 break else: found = 0 break if (not found) and keys_alt: o = obj_alt l = len(keys_alt) i = 0 found = 1 while i < l: k = keys_alt[i] i += 1 if o is not None: if isinstance(o,(list,tuple)) and int(k)<len(o): o = o[int(k)] elif isinstance(o,dict) and (k in o): o = o[k] else: try: o = getattr(o, k) except AttributeError: found = 0 break else: found = 0 break return o if found else None class StackEntry: def __init__(self, stack=None, value=None): self.prev = stack self.value = value class TplEntry: def __init__(self, node=None, tpl=None ): if tpl: tpl.next = self self.node = node self.prev = tpl self.next = None def multisplit( tpl, delims, postop=False ): IDL = delims[0] IDR = delims[1] OBL = delims[2] OBR = delims[3] lenIDL = len(IDL) lenIDR = len(IDR) lenOBL = len(OBL) lenOBR = len(OBR) ESC = '\\' OPT = '?' OPTR = '*' NEG = '!' DEF = '|' COMMENT = '#' TPL = ':=' REPL = '{' REPR = '}' DOT = '.' REF = ':' ALGN = '@' #NOTALGN = '&' COMMENT_CLOSE = COMMENT+OBR default_value = None negative = 0 optional = 0 aligned = 0 localised = 0 l = len(tpl) delim1 = [IDL, lenIDL, IDR, lenIDR] delim2 = [OBL, lenOBL, OBR, lenOBR] delim_order = [None,0,None,0,None,0,None,0] postop = postop is True a = TplEntry({'type': 0, 'val': '', 'algn': ''}) cur_arg = { 'type' : 1, 'name' : None, 'key' : None, 'stpl' : None, 'dval' : None, 'opt' : 0, 'neg' : 0, 'algn' : 0, 'loc' : 0, 'start' : 0, 'end' : 0 } roottpl = a block = None opt_args = None subtpl = {} cur_tpl = None arg_tpl = {} start_tpl = None # hard-coded merge-sort for arbitrary delims parsing based on str len if delim1[1] < delim1[3]: s = delim1[0] delim1[2] = delim1[0] delim1[0] = s i = delim1[1] delim1[3] = delim1[1] delim1[1] = i if delim2[1] < delim2[3]: s = delim2[0] delim2[2] = delim2[0] delim2[0] = s i = delim2[1] delim2[3] = delim2[1] delim2[1] = i start_i = 0 end_i = 0 i = 0 while (4 > start_i) and (4 > end_i): if delim1[start_i+1] < delim2[end_i+1]: delim_order[i] = delim2[end_i] delim_order[i+1] = delim2[end_i+1] end_i += 2 else: delim_order[i] = delim1[start_i] delim_order[i+1] = delim1[start_i+1] start_i += 2 i += 2 while 4 > start_i: delim_order[i] = delim1[start_i] delim_order[i+1] = delim1[start_i+1] start_i += 2 i += 2 while 4 > end_i: delim_order[i] = delim2[end_i] delim_order[i+1] = delim2[end_i+1] end_i += 2 i += 2 stack = None s = '' i = 0 while i < l: c = tpl[i] if ESC == c: s += tpl[i+1] if i+1 < l else '' i += 2 continue delim = None if delim_order[0] == tpl[i:i+delim_order[1]]: delim = delim_order[0] elif delim_order[2] == tpl[i:i+delim_order[3]]: delim = delim_order[2] elif delim_order[4] == tpl[i:i+delim_order[5]]: delim = delim_order[4] elif delim_order[6] == tpl[i:i+delim_order[7]]: delim = delim_order[6] if IDL == delim: i += lenIDL if len(s): if 0 == a.node['type']: a.node['val'] += s else: a = TplEntry({'type': 0, 'val': s, 'algn': ''}, a) s = '' elif IDR == delim: i += lenIDR # argument argument = s s = '' p = argument.find(DEF) if -1 < p: default_value = argument[p+1:] argument = argument[0:p] else: default_value = None if postop: c = tpl[i] if i < l else '' else: c = argument[0] if OPT == c or OPTR == c: optional = 1 if OPTR == c: start_i = 1 end_i = -1 else: start_i = 0 end_i = 0 if postop: i += 1 if (i < l) and (NEG == tpl[i]): negative = 1 i += 1 else: negative = 0 else: if NEG == argument[1]: negative = 1 argument = argument[2:] else: negative = 0 argument = argument[1:] elif REPL == c: if postop: s = '' j = i+1 jl = l while (j < jl) and (REPR != tpl[j]): s += tpl[j] j += 1 i = j+1 else: s = '' j = 1 jl = len(argument) while (j < jl) and (REPR != argument[j]): s += argument[j] j += 1 argument = argument[j+1:] s = s.split(',') if len(s) > 1: start_i = s[0].strip() start_i = int(start_i,10) if len(start_i) else 0 end_i = s[1].strip() end_i = int(end_i,10) if len(end_i) else -1 optional = 1 else: start_i = s[0].strip() start_i = int(start_i,10) if len(start_i) else 0 end_i = start_i optional = 0 s = '' negative = 0 else: optional = 0 negative = 0 start_i = 0 end_i = 0 if negative and default_value is None: default_value = '' c = argument[0] if ALGN == c: aligned = 1 argument = argument[1:] else: aligned = 0 c = argument[0] if DOT == c: localised = 1 argument = argument[1:] else: localised = 0 p = argument.find(REF) template = argument.split(REF) if -1 < p else [argument,None] argument = template[0] template = template[1] p = argument.find(DOT) nested = argument.split(DOT) if -1 < p else None if cur_tpl and (cur_tpl not in arg_tpl): arg_tpl[cur_tpl] = {} if TPL+OBL == tpl[i:i+2+lenOBL]: # template definition i += 2 template = template if template and len(template) else 'grtpl--'+guid() start_tpl = template if cur_tpl and len(argument): arg_tpl[cur_tpl][argument] = template if not len(argument): continue # template definition only if (template is None) and cur_tpl and (cur_tpl in arg_tpl) and (argument in arg_tpl[cur_tpl]): template = arg_tpl[cur_tpl][argument] if optional and not cur_arg['opt']: cur_arg['name'] = argument cur_arg['key'] = nested cur_arg['stpl'] = template cur_arg['dval'] = default_value cur_arg['opt'] = optional cur_arg['neg'] = negative cur_arg['algn'] = aligned cur_arg['loc'] = localised cur_arg['start'] = start_i cur_arg['end'] = end_i # handle multiple optional arguments for same optional block opt_args = StackEntry(None, [argument,nested,negative,start_i,end_i,optional,localised]) elif optional: # handle multiple optional arguments for same optional block if (start_i != end_i) and (cur_arg['start'] == cur_arg['end']): # set as main arg a loop arg, if exists cur_arg['name'] = argument cur_arg['key'] = nested cur_arg['stpl'] = template cur_arg['dval'] = default_value cur_arg['opt'] = optional cur_arg['neg'] = negative cur_arg['algn'] = aligned cur_arg['loc'] = localised cur_arg['start'] = start_i cur_arg['end'] = end_i opt_args = StackEntry(opt_args, [argument,nested,negative,start_i,end_i,optional,localised]) elif (not optional) and (cur_arg['name'] is None): cur_arg['name'] = argument cur_arg['key'] = nested cur_arg['stpl'] = template cur_arg['dval'] = default_value cur_arg['opt'] = 0 cur_arg['neg'] = negative cur_arg['algn'] = aligned cur_arg['loc'] = localised cur_arg['start'] = start_i cur_arg['end'] = end_i # handle multiple optional arguments for same optional block opt_args = StackEntry(None, [argument,nested,negative,start_i,end_i,0,localised]) if 0 == a.node['type']: a.node['algn'] = compute_alignment(a.node['val'], 0, len(a.node['val'])) a = TplEntry({ 'type' : 1, 'name' : argument, 'key' : nested, 'stpl' : template, 'dval' : default_value, 'opt' : optional, 'algn' : aligned, 'loc' : localised, 'start' : start_i, 'end' : end_i }, a) elif OBL == delim: i += lenOBL if len(s): if 0 == a.node['type']: a.node['val'] += s else: a = TplEntry({'type': 0, 'val': s, 'algn': ''}, a) s = '' # comment if COMMENT == tpl[i]: j = i+1 jl = l while (j < jl) and (COMMENT_CLOSE != tpl[j:j+lenOBR+1]): s += tpl[j] j += 1 i = j+lenOBR+1 if 0 == a.node['type']: a.node['algn'] = compute_alignment(a.node['val'], 0, len(a.node['val'])) a = TplEntry({'type': -100, 'val': s}, a) s = '' continue # optional block stack = StackEntry(stack, [a, block, cur_arg, opt_args, cur_tpl, start_tpl]) if start_tpl: cur_tpl = start_tpl start_tpl = None cur_arg = { 'type' : 1, 'name' : None, 'key' : None, 'stpl' : None, 'dval' : None, 'opt' : 0, 'neg' : 0, 'algn' : 0, 'loc' : 0, 'start' : 0, 'end' : 0 } opt_args = None a = TplEntry({'type': 0, 'val': '', 'algn': ''}) block = a elif OBR == delim: i += lenOBR b = a cur_block = block prev_arg = cur_arg prev_opt_args = opt_args if stack: a = stack.value[0] block = stack.value[1] cur_arg = stack.value[2] opt_args = stack.value[3] cur_tpl = stack.value[4] start_tpl = stack.value[5] stack = stack.prev else: a = None if len(s): if 0 == b.node['type']: b.node['val'] += s else: b = TplEntry({'type': 0, 'val': s, 'algn': ''}, b) s = '' if start_tpl: subtpl[start_tpl] = TplEntry({ 'type' : 2, 'name' : prev_arg['name'], 'key' : prev_arg['key'], 'loc' : prev_arg['loc'], 'algn' : prev_arg['algn'], 'start' : prev_arg['start'], 'end' : prev_arg['end'], 'opt_args': None,#opt_args 'tpl' : cur_block }) start_tpl = None else: if 0 == a.node['type']: a.node['algn'] = compute_alignment(a.node['val'], 0, len(a.node['val'])) a = TplEntry({ 'type' : -1, 'name' : prev_arg['name'], 'key' : prev_arg['key'], 'loc' : prev_arg['loc'], 'algn' : prev_arg['algn'], 'start' : prev_arg['start'], 'end' : prev_arg['end'], 'opt_args': prev_opt_args, 'tpl' : cur_block }, a) else: c = tpl[i] i += 1 if "\n" == c: # note line changes to handle alignments if len(s): if 0 == a.node['type']: a.node['val'] += s else: a = TplEntry({'type': 0, 'val': s, 'algn': ''}, a) s = '' if 0 == a.node['type']: a.node['algn'] = compute_alignment(a.node['val'], 0, len(a.node['val'])) a = TplEntry({'type': 100, 'val': "\n"}, a) else: s += c if len(s): if 0 == a.node['type']: a.node['val'] += s else: a = TplEntry({'type': 0, 'val': s, 'algn': ''}, a) if 0 == a.node['type']: a.node['algn'] = compute_alignment(a.node['val'], 0, len(a.node['val'])) return [roottpl, subtpl] def optional_block( args, block, SUB=None, FN=None, index=None, alignment='', orig_args=None ): out = '' block_arg = None if -1 == block['type']: # optional block, check if optional variables can be rendered opt_vars = block['opt_args'] # if no optional arguments, render block by default if opt_vars and opt_vars.value[5]: while opt_vars: opt_v = opt_vars.value opt_arg = walk( args, opt_v[1], [str(opt_v[0])], None if opt_v[6] else orig_args ) if (block_arg is None) and (block['name'] == opt_v[0]): block_arg = opt_arg if ((0 == opt_v[2]) and (opt_arg is None)) or ((1 == opt_v[2]) and (opt_arg is not None)): return '' opt_vars = opt_vars.prev else: block_arg = walk( args, block['key'], [str(block['name'])], None if block['loc'] else orig_args ) arr = is_array( block_arg ) lenn = len(block_arg) if arr else -1 #if not block['algn']: alignment = '' if arr and (lenn > block['start']): rs = block['start'] re = lenn-1 if -1==block['end'] else min(block['end'],lenn-1) ri = rs while ri <= re: out += main( args, block['tpl'], SUB, FN, ri, alignment, orig_args ) ri += 1 elif (not arr) and (block['start'] == block['end']): out = main( args, block['tpl'], SUB, FN, None, alignment, orig_args ) return out def non_terminal( args, symbol, SUB=None, FN=None, index=None, alignment='', orig_args=None ): out = '' if symbol['stpl'] and ((SUB and (symbol['stpl'] in SUB)) or (symbol['stpl'] in GrammarTemplate.subGlobal) or (FN and ((symbol['stpl'] in FN) or ('*' in FN))) or ((symbol['stpl'] in GrammarTemplate.fnGlobal) or ('*' in GrammarTemplate.fnGlobal))): # using custom function or sub-template opt_arg = walk( args, symbol['key'], [str(symbol['name'])], None if symbol['loc'] else orig_args ) if (SUB and (symbol['stpl'] in SUB)) or (symbol['stpl'] in GrammarTemplate.subGlobal): # sub-template if (index is not None) and ((0 != index) or (symbol['start'] != symbol['end']) or (not symbol['opt'])) and is_array(opt_arg): opt_arg = opt_arg[ index ] if index < len(opt_arg) else None if (opt_arg is None) and (symbol['dval'] is not None): # default value if missing out = symbol['dval'] else: # try to associate sub-template parameters to actual input arguments tpl = SUB[symbol['stpl']].node if SUB and (symbol['stpl'] in SUB) else GrammarTemplate.subGlobal[symbol['stpl']].node tpl_args = {} if opt_arg is not None: if is_array(opt_arg): tpl_args[tpl['name']] = opt_arg else: tpl_args = opt_arg out = optional_block( tpl_args, tpl, SUB, FN, None, alignment if symbol['algn'] else '', args if orig_args is None else orig_args ) #if symbol['algn']: out = align(out, alignment) else:#elif fn: # custom function fn = None if FN and (symbol['stpl'] in FN): fn = FN[symbol['stpl']] elif FN and ('*' in FN): fn = FN['*'] elif symbol['stpl'] in GrammarTemplate.fnGlobal: fn = GrammarTemplate.fnGlobal[symbol['stpl']] elif '*' in GrammarTemplate.fnGlobal: fn = GrammarTemplate.fnGlobal['*'] if is_array(opt_arg): index = index if index is not None else symbol['start'] opt_arg = opt_arg[ index ] if index < len(opt_arg) else None if callable(fn): fn_arg = { #'value' : opt_arg, 'symbol' : symbol, 'index' : index, 'currentArguments' : args, 'originalArguments' : orig_args, 'alignment' : alignment } opt_arg = fn( opt_arg, fn_arg ) else: opt_arg = str(fn) out = symbol['dval'] if (opt_arg is None) and (symbol['dval'] is not None) else str(opt_arg) if symbol['algn']: out = align(out, alignment) elif symbol['opt'] and (symbol['dval'] is not None): # boolean optional argument out = symbol['dval'] else: # plain symbol argument opt_arg = walk( args, symbol['key'], [str(symbol['name'])], None if symbol['loc'] else orig_args ) # default value if missing if is_array(opt_arg): index = index if index is not None else symbol['start'] opt_arg = opt_arg[ index ] if index < len(opt_arg) else None out = symbol['dval'] if (opt_arg is None) and (symbol['dval'] is not None) else str(opt_arg) if symbol['algn']: out = align(out, alignment) return out def main( args, tpl, SUB=None, FN=None, index=None, alignment='', orig_args=None ): out = '' current_alignment = alignment while tpl: tt = tpl.node['type'] if -1 == tt: # optional code-block out += optional_block( args, tpl.node, SUB, FN, index, current_alignment if tpl.node['algn'] else alignment, orig_args ) elif 1 == tt: # non-terminal out += non_terminal( args, tpl.node, SUB, FN, index, current_alignment if tpl.node['algn'] else alignment, orig_args ) elif 0 == tt: # terminal current_alignment += tpl.node['algn'] out += tpl.node['val'] elif 100 == tt: # new line current_alignment = alignment out += "\n" + alignment #elif -100 == tt: # comment # # pass tpl = tpl.next return out class GrammarTemplate: """ GrammarTemplate for Python, https://github.com/foo123/GrammarTemplate """ VERSION = '3.0.0' defaultDelimiters = ['<','>','[',']'] fnGlobal = {} subGlobal = {} guid = guid multisplit = multisplit align = align main = main def __init__(self, tpl='', delims=None, postop=False): self.id = None self.tpl = None self.fn = {} # lazy init self._args = [ tpl, delims if delims else GrammarTemplate.defaultDelimiters, postop ] def __del__(self): self.dispose() def dispose(self): self.id = None self.tpl = None self.fn = None self._args = None return self def parse(self): if (self.tpl is None) and (self._args is not None): # lazy init self.tpl = GrammarTemplate.multisplit( self._args[0], self._args[1], self._args[2] ) self._args = None return self def render(self, args=None): # lazy init if self.tpl is None: self.parse( ) return GrammarTemplate.main( {} if None == args else args, self.tpl[0], self.tpl[1], self.fn ) # static CNT = 0 def createFunction( args, sourceCode, additional_symbols=dict() ): # http://code.activestate.com/recipes/550804-create-a-restricted-python-function-from-a-string/ global CNT CNT += 1 funcName = 'xpresion_dyna_func_' + str(CNT) # The list of symbols that are included by default in the generated # function's environment SAFE_SYMBOLS = [ "list", "dict", "enumerate", "tuple", "set", "long", "float", "object", "bool", "callable", "True", "False", "dir", "frozenset", "getattr", "hasattr", "abs", "cmp", "complex", "divmod", "id", "pow", "round", "slice", "vars", "hash", "hex", "int", "isinstance", "issubclass", "len", "map", "filter", "max", "min", "oct", "chr", "ord", "range", "reduce", "repr", "str", "type", "zip", "xrange", "None", "Exception", "KeyboardInterrupt" ] # Also add the standard exceptions __bi = __builtins__ if type(__bi) is not dict: __bi = __bi.__dict__ for k in __bi: if k.endswith("Error") or k.endswith("Warning"): SAFE_SYMBOLS.append(k) del __bi # Include the sourcecode as the code of a function funcName: s = "def " + funcName + "(%s):\n" % args s += sourceCode # this should be already properly padded # Byte-compilation (optional) byteCode = compile(s, "<string>", 'exec') # Setup the local and global dictionaries of the execution # environment for __TheFunction__ bis = dict() # builtins globs = dict() locs = dict() # Setup a standard-compatible python environment bis["locals"] = lambda: locs bis["globals"] = lambda: globs globs["__builtins__"] = bis globs["__name__"] = "SUBENV" globs["__doc__"] = sourceCode # Determine how the __builtins__ dictionary should be accessed if type(__builtins__) is dict: bi_dict = __builtins__ else: bi_dict = __builtins__.__dict__ # Include the safe symbols for k in SAFE_SYMBOLS: # try from current locals try: locs[k] = locals()[k] continue except KeyError: pass # Try from globals try: globs[k] = globals()[k] continue except KeyError: pass # Try from builtins try: bis[k] = bi_dict[k] except KeyError: # Symbol not available anywhere: silently ignored pass # Include the symbols added by the caller, in the globals dictionary globs.update(additional_symbols) # Finally execute the Function statement: eval(byteCode, globs, locs) # As a result, the function is defined as the item funcName # in the locals dictionary fct = locs[funcName] # Attach the function to the globals so that it can be recursive del locs[funcName] globs[funcName] = fct # Attach the actual source code to the docstring fct.__doc__ = sourceCode # return the compiled function object return fct def array_splice(arr, index, offset): l = len(arr) if l > 0: if index < 0: index += l index = min(index, l) #if offset < 0: offset = -offset index2 = index+offset if index2 < 0: index2 += l index2 = min(index2, l) ret = arr[index:index2] del arr[index:index2] return ret return [] def dummy( *args ): return None def evaluator_factory(evaluator_str,Fn,Cache): evaluator_factory = createFunction('Fn,Cache', "\n".join([ ' def evaluator(Var):', ' return ' + evaluator_str, ' return evaluator' ]), {'math':math,'Xpresion':Xpresion}) return evaluator_factory(Fn,Cache) default_date_locale = { 'meridian': { 'am':'am', 'pm':'pm', 'AM':'AM', 'PM':'PM' } ,'ordinal': { 'ord':{1:'st',2:'nd',3:'rd'}, 'nth':'th' } ,'timezone': [ 'UTC','EST','MDT' ] ,'timezone_short': [ 'UTC','EST','MDT' ] ,'day': [ 'Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday' ] ,'day_short': [ 'Sun','Mon','Tue','Wed','Thu','Fri','Sat' ] ,'month': [ 'January','February','March','April','May','June','July','August','September','October','November','December' ] ,'month_short': [ 'Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec' ] } def php_time( ): return int(time.time()) def php_date( format, timestamp=None ): global default_date_locale locale = default_date_locale # http://php.net/manual/en/datetime.formats.date.php # http://strftime.org/ # https://docs.python.org/2/library/time.html # adapted from http://brandonwamboldt.ca/python-php-date-class-335/ if timestamp is None: timestamp = php_time( ) utime = timestamp dtime = datetime.datetime.fromtimestamp(timestamp) D = { } w = dtime.weekday() W = dtime.isocalendar()[1] d = dtime.day dmod10 = d % 10 n = dtime.month Y = dtime.year g = int(dtime.strftime("%I")) G = int(dtime.strftime("%H")) meridian = dtime.strftime("%p") tzo = int(time.timezone / 60) atzo = abs(tzo) # Calculate and return Swatch Internet Time # http://code.activestate.com/recipes/578473-calculating-swatch-internet-time-or-beats/ lh, lm, ls = time.localtime()[3:6] beats = ((lh * 3600) + (lm * 60) + ls + time.timezone) / 86.4 if beats > 1000: beats -= 1000 elif beats < 0: beats += 1000 # Day -- D['d'] = str( d ).zfill(2) D['D'] = locale['day_short'][ w ] D['j'] = str( d ) D['l'] = locale['day'][ w ] D['N'] = str( w if 0 < w else 7 ) D['S'] = locale['ordinal']['ord'][ d ] if d in locale['ordinal']['ord'] else (locale['ordinal']['ord'][ dmod10 ] if dmod10 in locale['ordinal']['ord'] else locale['ordinal']['nth']) D['w'] = str( w ) D['z'] = str( dtime.timetuple().tm_yday ) # Week -- D['W'] = str( W ) # Month -- D['F'] = locale['month'][ n-1 ] D['m'] = str( n ).zfill(2) D['M'] = locale['month_short'][ n-1 ] D['n'] = str( n ) D['t'] = str( calendar.monthrange(Y, n)[1] ) # Year -- D['L'] = str( int(calendar.isleap(Y)) ) D['o'] = str(Y + (1 if n == 12 and W < 9 else (-1 if n == 1 and W > 9 else 0))) D['Y'] = str( Y ) D['y'] = str( Y )[2:] # Time -- D['a'] = locale['meridian'][meridian.lower()] if meridian.lower() in locale['meridian'] else meridian.lower() D['A'] = locale['meridian'][meridian] if meridian in locale['meridian'] else meridian D['B'] = str( int(beats) ).zfill(3) D['g'] = str( g ) D['G'] = str( G ) D['h'] = str( g ).zfill(2) D['H'] = str( G ).zfill(2) D['i'] = str( dtime.minute ).zfill(2) D['s'] = str( dtime.second ).zfill(2) D['u'] = str( dtime.microsecond ).zfill(6) # Timezone -- D['e'] = '' # TODO, missing D['I'] = str( dtime.dst() ) D['O'] = ('-' if tzo > 0 else '+')+str(int(atzo / 60) * 100 + atzo % 60).zfill(4) D['P'] = D['O'][:3]+':'+D['O'][3:] D['T'] = 'UTC' D['Z'] = str(-tzo*60) # Full Date/Time -- D['c'] = ''.join([ D['Y'],'-',D['m'],'-',D['d'],'\\',D['T'],D['H'],':',D['i'],':',D['s'],D['P'] ]) D['r'] = ''.join([ D['D'],', ',D['d'],' ',D['M'],' ',D['Y'],' ',D['H'],':',D['i'],':',D['s'],' ',D['O'] ]) D['U'] = str( utime ) formatted_datetime = '' for f in format: formatted_datetime += D[f] if f in D else f return formatted_datetime def parse_re_flags(s,i,l): flags = '' has_i = False has_g = False has_m = False seq = 0 i2 = i+seq not_done = True while i2 < l and not_done: ch = s[i2] i2 += 1 seq += 1 if 'i' == ch and not has_i: flags += 'i' has_i = True if 'm' == ch and not has_m: flags += 'm' has_m = True if 'g' == ch and not has_g: flags += 'g' has_g = True if seq >= 3 or (not has_i and not has_g and not has_m): not_done = False return flags NEWLINE = re.compile(r'\n\r|\r\n|\n|\r') SQUOTE = re.compile(r"'") # STATIC COMMA = ',' LPAREN = '(' RPAREN = ')' NONE = 0 DEFAULT = 1 LEFT = -2 RIGHT = 2 PREFIX = 2 INFIX = 4 POSTFIX = 8 T_DUM = 0 T_MIX = 1 T_DFT = T_MIX T_IDE = 16 T_VAR = 17 T_LIT = 32 T_NUM = 33 T_STR = 34 T_REX = 35 T_BOL = 36 T_DTM = 37 T_ARY = 38 T_OP = 128 T_N_OP = 129 T_POLY_OP = 130 T_FUN = 131 T_EMPTY = 1024 T_REGEXP = type(NEWLINE) class Node: # depth-first traversal def DFT(root, action=None, andDispose=False): # # one can also implement a symbolic solver here # by manipulating the tree to produce 'x' on one side # and the reverse operators/tokens on the other side # i.e by transposing the top op on other side of the '=' op and using the 'associated inverse operator' # in stack order (i.e most top op is transposed first etc.. until only the branch with 'x' stays on one side) # (easy when only one unknown in one state, more difficult for many unknowns # or one unknown in different states, e.g x and x^2 etc..) # andDispose = bool(andDispose is not False) if not action: action = Xpresion.render stack = [ root ] output = [ ] while len(stack): node = stack[ 0 ] if node.children and len(node.children): stack = node.children + stack node.children = None else: stack.pop(0) op = node.node arity = op.arity if (T_OP & op.type) and 0 == arity: arity = 1 # have already padded with empty token elif arity > len(output) and op.arity_min <= op.arity: arity = op.arity_min o = action(op, array_splice(output, -arity, arity)) output.append( o ) if andDispose: node.dispose( ) stack = None return output[ 0 ] def __init__(self, type, arity, node, children=None, pos=0): self.type = type self.arity = arity self.node = node self.children = children self.pos = pos self.op_parts = None self.op_def = None self.op_index = None def __del__(self): self.dispose() def dispose(self): c = self.children if c and len(c): for ci in c: if ci: ci.dispose( ) self.type = None self.arity = None self.pos = None self.node = None self.op_parts = None self.op_def = None self.op_index = None self.children = None return self def op_next(self, op, pos, op_queue, token_queue): num_args = 0 next_index = -1 try: next_index = self.op_parts.index( op.input ) except: next_index = -1 is_next = (0 == next_index) if is_next: if 0 == self.op_def[0][0]: num_args = Op.match_args(self.op_def[0][2], pos-1, op_queue, token_queue ) if num_args is False: is_next = False else: self.arity = num_args self.op_def.pop(0) if is_next: self.op_def.pop(0) self.op_parts.pop(0) return is_next def op_complete(self): return 0 == len(self.op_parts) def __str__(self, tab=""): out = [] n = self.node c = self.children if self.children else [] tab_tab = tab+" " for ci in c: out.append(ci.__str__(tab_tab)) if hasattr(n, 'parts') and n.parts: parts = " ".join(n.parts) else: parts = n.input return tab + ("\n"+tab).join([ "Node("+str(n.type)+"): " + str(parts), "Childs: [", tab +("\n" + tab).join(out), "]" ]) + "\n" class Alias: def get_entry(entries, id): if id and entries and (id in entries): # walk/bypass aliases, if any entry = entries[ id ] while isinstance(entry, Alias) and (entry.alias in entries): id = entry.alias # circular reference if entry == entries[ id ]: return False entry = entries[ id ] return entry return False def __init__(self, alias): self.alias = str(alias) def __del__(self): self.alias = None class Tok: def render(t): if isinstance(t, Tok): return t.render() return str(t) def __init__(self, type, input, output, value=None): self.type = type self.input = input self.output = output self.value = value self.priority = 1000 self.parity = 0 self.arity = 0 self.arity_min = 0 self.arity_max = 0 self.associativity = DEFAULT self.fixity = INFIX self.parenthesize = False self.revert = False def __del__(self): self.dispose() def dispose(self): self.type = None self.input = None self.output = None self.value = None self.priority = None self.parity = None self.arity = None self.arity_min = None self.arity_max = None self.associativity = None self.fixity = None self.parenthesize = None self.revert = None return self def setType(self, type): self.type = type return self def setParenthesize(self, bol): self.parenthesize = bool(bol) return self def setReverse(self, bol): self.revert = bool(bol) return self def render(self, args=None): token = self.output p = self.parenthesize lparen = Xpresion.LPAREN if p else '' rparen = Xpresion.RPAREN if p else '' if None==args: args=[] args.insert(0, self.input) if isinstance(token,GrammarTemplate): out = str(token.render( {'$':args} )) else: out = str(token) return lparen + out + rparen def node(self, args=None, pos=0): return Node(self.type, self.arity, self, args if args else None, pos) def __str__(self): return str(self.output) class Op(Tok): def Condition(f): if isinstance(f[0],str): try: f[0] = createFunction('curr,Xpresion', ' return '+f[0]) except BaseException: f[0] = None return [ f[0] if callable(f[0]) else None ,f[1] ] def parse_definition(op_def): parts = [] op = [] arity = 0 arity_min = 0 arity_max = 0 if isinstance(op_def, str): # assume infix, arity = 2; op_def = [1,op_def,1] else: op_def = list(op_def) for i in range(len(op_def)): if isinstance(op_def[i], str): parts.append(op_def[i]) op.append([1, i, op_def[i]]) else: op.append([0, i, op_def[i]]) num_args = abs(op_def[i]) arity += num_args arity_max += num_args arity_min += op_def[i] if 1 == len(parts) and 1 == len(op): op = [[0, 0, 1], [1, 1, parts[0]], [0, 2, 1]] arity = 2 arity_min = 2 arity_max = 2 type = T_OP else: type = T_N_OP if len(parts) > 1 else T_OP return [type, op, parts, arity, max(0, arity_min), arity_max] def match_args(expected_args, args_pos, op_queue, token_queue): tl = len(token_queue) t = tl-1 num_args = 0 num_expected_args = abs(expected_args) INF = -10 while num_args < num_expected_args or t >= 0: p2 = INF if t < 0 else token_queue[t].pos if args_pos == p2: num_args+=1 args_pos-=1 t-=1 else: break return num_expected_args if num_args >= num_expected_args else (0 if expected_args <= 0 else False) def __init__(self, input='', output='', otype=None, fixity=None, associativity=None, priority=None, ofixity=None): opdef = Op.parse_definition( input ) self.type = opdef[0] self.opdef = opdef[1] self.parts = opdef[2] if not isinstance(output, GrammarTemplate): output = GrammarTemplate(str(output)) super(Op, self).__init__(self.type, self.parts[0], output) self.fixity = fixity if fixity is not None else PREFIX self.associativity = associativity if associativity is not None else DEFAULT self.priority = priority if priority is not None else 1000 self.arity = opdef[3] #self.arity = arity self.otype = otype if otype is not None else T_MIX self.ofixity = ofixity if ofixity is not None else self.fixity self.parenthesize = False self.revert = False self.morphes = None def __del__(self): self.dispose() def dispose(self): super(Op, self).dispose() self.otype = None self.ofixity = None self.parts = None self.opdef = None self.morphes = None return self def Polymorphic(self, morphes=None): self.type = T_POLY_OP self.morphes = list(map(Op.Condition, morphes if morphes else [ ])) return self def morph(self, args): morphes = self.morphes l = len(morphes) i = 0 minop = morphes[0][1] found = False # [pos,token_queue,op_queue] if len(args) < 7: args.append(args[1][-1] if len(args[1]) else False) args.append(args[2][0] if len(args[2]) else False) args.append((args[4].pos+1==args[0]) if args[4] else False) deduced_type = 0 # T_DUM indt = len(args[1])-1 indo = 0 # try to inherit type from other tokens/ops if current type is T_DUM(0), eg for bracket operator while not deduced_type: if indt>=0 and indo<len(args[2]) and indo+1<len(args[2]) and isinstance(args[2][indo+1].node, Xpresion.Func): deduced_type = args[1][indt].type #args[1][indt].pos>args[2][indo].pos ? args[1][indt--].type : args[2][indo++].type indt -= 1 elif indo<len(args[2]): deduced_type = args[2][indo].type indo += 1 elif indt>=0: deduced_type = args[1][indt].type indt -= 1 else: break args.append(deduced_type) # {'${POS}':0,'${TOKS}':1,'${OPS}':2,'${TOK}':3,'${OP}':4,'${PREV_IS_OP}':5,'${DEDUCED_TYPE}':6,'Xpresion':7} #nargs = { # 'POS': args[0], # 'TOKS': args[1], # 'OPS': args[2], # 'TOK': args[3], # 'OP': args[4], # 'PREV_IS_OP' : args[5], # 'DEDUCED_TYPE': args[6]#, # #'Xpresion': args[7] #} nargs = namedtuple("stdClass", ['POS','TOKS','OPS','TOK','OP','PREV_IS_OP','DEDUCED_TYPE'])(*args) while i < l: op = morphes[i] i += 1 matched = bool(op[0]( nargs, Xpresion )) if True == matched: op = op[1] found = True break if op[1].priority >= minop.priority: minop = op[1] # try to return minimum priority operator, if none matched if not found: op = minop # nested polymorphic op, if any while T_POLY_OP == op.type: op = op.morph( args ) return op def render(self, args=None): output_type = self.otype op = self.output p = self.parenthesize lparen = Xpresion.LPAREN if p else '' rparen = Xpresion.RPAREN if p else '' comma = Xpresion.COMMA out_fixity = self.ofixity if None==args or not len(args): args=['',''] numargs = len(args) #if (T_DUM == output_type) and numargs: # output_type = args[ 0 ].type #args = list(map(Tok.render, args)) if isinstance(op, GrammarTemplate): out = lparen + str(op.render( {'$':args} )) + rparen elif INFIX == out_fixity: out = lparen + str(op).join(args) + rparen elif POSTFIX == out_fixity: out = lparen + comma.join(args) + rparen + str(op) else: # if PREFIX == out_fixity: out = str(op) + lparen + comma.join(args) + rparen return Tok(output_type, out, out) def validate(self, pos, op_queue, token_queue ): num_args = 0 msg = '' if 0 == self.opdef[0][0]: # expecting argument(s) num_args = Op.match_args(self.opdef[0][2], pos-1, op_queue, token_queue ) if num_args is False: msg = 'Operator "' + str(self.input) + '" expecting ' + str(self.opdef[0][2]) + ' prior argument(s)' return [num_args, msg] def node(self, args=None, pos=0, op_queue=None, token_queue=None): otype = self.otype if None==args: args = [] if self.revert: args = args[::-1] if (T_DUM == otype) and len(args): otype = args[ 0 ].type elif len(args): args[0].type = otype n = Node(otype, self.arity, self, args, pos) if T_N_OP == self.type and None != op_queue: n.op_parts = self.parts[1:] n.op_def = self.opdef[2:] if 0 == self.opdef[0][0] else self.opdef[1:] n.op_index = len(op_queue)+1 return n class Func(Op): def __init__(self, input='', output='', otype=None, priority=None, arity=None, associativity=None, ofixity=None): super(Func, self).__init__( [input, arity if arity is not None else 1] if isinstance(input, str) else input, output, otype if otype is not None else T_MIX, PREFIX, associativity if associativity is not None else RIGHT, priority if priority is not None else 1, ofixity if ofixity is not None else PREFIX ) self.type = T_FUN def __del__(self): self.dispose() class Fn: def __init__(self): self.INF = float("inf") self.NAN = float("nan") class Configuration: def __init__(self, conf=None): self.RE = {} self.BLOCKS = {} self.RESERVED = {} self.OPERATORS = {} self.FUNCTIONS = {} self.FN = Fn() if conf and isinstance(conf, dict): if 're' in conf: self.defRE(conf['re']) if 'blocks' in conf: self.defBlock(conf['blocks']) if 'reserved' in conf: self.defReserved(conf['reserved']) if 'operators' in conf: self.defOp(conf['operators']) if 'functions' in conf: self.defFunc(conf['functions']) if 'runtime' in conf: self.defRuntimeFunc(conf['runtime']) def __del__(self): self.dispose() def dispose(self): self.RE = None self.BLOCKS = None self.RESERVED = None self.OPERATORS = None self.FUNCTIONS = None self.FN = None return self def defRE(self, obj): if isinstance(obj,dict): for k in obj: self.RE[ k ] = obj[ k ] return self def defBlock(self, obj): if isinstance(obj,dict): for k in obj: self.BLOCKS[ k ] = obj[ k ] return self def defReserved(self, obj): if isinstance(obj,dict): for k in obj: self.RESERVED[ k ] = obj[ k ] return self def defOp(self, obj): if isinstance(obj,dict): for k in obj: op = obj[ k ] if not op: continue if isinstance(op, Alias) or isinstance(op, Op): self.OPERATORS[ k ] = op continue if ('polymorphic' in op) and (op['polymorphic']): def mapper(entry): if isinstance(entry,dict): func = entry['check'] op = entry['op'] else:#if isinstance(entry,(list,tuple)): func = entry[0] op = entry[1] op = op if isinstance(op, Op) else Op( op['input'], op['output'] if 'output' in op else '', op['otype'] if 'otype' in op else None, op['fixity'] if 'fixity' in op else None, op['associativity'] if 'associativity' in op else None, op['priority'] if 'priority' in op else None, op['ofixity'] if 'ofixity' in op else None ) return [func, op] self.OPERATORS[ k ] = Op().Polymorphic(list(map(mapper, op['polymorphic']))) else: self.OPERATORS[ k ] = Op( op['input'], op['output'] if 'output' in op else '', op['otype'] if 'otype' in op else None, op['fixity'] if 'fixity' in op else None, op['associativity'] if 'associativity' in op else None, op['priority'] if 'priority' in op else None, op['ofixity'] if 'ofixity' in op else None ) return self def defFunc(self, obj): if isinstance(obj,dict): for k in obj: op = obj[ k ] if not op: continue if isinstance(op, Alias) or isinstance(op, Func): self.FUNCTIONS[ k ] = op continue self.FUNCTIONS[ k ] = Func( op['input'], op['output'] if 'output' in op else '', op['otype'] if 'otype' in op else None, op['priority'] if 'priority' in op else None, op['arity'] if 'arity' in op else None, op['associativity'] if 'associativity' in op else None, op['ofixity'] if 'ofixity' in op else None ) return self def defRuntimeFunc(self, obj): if isinstance(obj,dict): #fix: TypeError: 'type' object does not support item assignment # use setattr for k in obj: setattr(self.FN, k, obj[ k ]) #for k in obj: self.FN[ k ] = obj[ k ] return self class Xpresion: """ Xpresion for Python, https://github.com/foo123/Xpresion """ VERSION = "1.0.1" COMMA = COMMA LPAREN = LPAREN RPAREN = RPAREN NONE = NONE DEFAULT = DEFAULT LEFT = LEFT RIGHT = RIGHT PREFIX = PREFIX INFIX = INFIX POSTFIX = POSTFIX T_DUM = T_DUM T_MIX = T_MIX T_DFT = T_DFT T_IDE = T_IDE T_VAR = T_VAR T_LIT = T_LIT T_NUM = T_NUM T_STR = T_STR T_REX = T_REX T_BOL = T_BOL T_DTM = T_DTM T_ARY = T_ARY T_OP = T_OP T_N_OP = T_N_OP T_POLY_OP = T_POLY_OP T_FUN = T_FUN T_EMPTY = T_EMPTY TYPES = { '0' : 'T_DUM', '1' : 'T_MIX', #'1' => 'T_DFT', '16' : 'T_IDE', '17' : 'T_VAR', '32' : 'T_LIT', '33' : 'T_NUM', '34' : 'T_STR', '35' : 'T_REX', '36' : 'T_BOL', '37' : 'T_DTM', '38' : 'T_ARY', '128' : 'T_OP', '129' : 'T_N_OP', '130' : 'T_POLY_OP', '131' : 'T_FUN', '1024' : 'T_EMPTY' } EMPTY_TOKEN = Tok(T_EMPTY, '', '') CONF = None _inited = False Tpl = GrammarTemplate Configuration = Configuration Node = Node Alias = Alias Tok = Tok Op = Op Func = Func Fn = Fn def reduce(token_queue, op_queue, nop_queue, current_op=None, pos=0, err=None): nop = None nop_index = 0 # # n-ary operatots (eg ternary) or composite operators # as operators with multi-parts # which use their own stack or equivalently # lock their place on the OP_STACK # until all the parts of the operator are # unified and collapsed # # Equivalently n-ary ops are like ops which relate NOT to # args but to other ops # # In this way the BRA_KET special op handling # can be made into an n-ary op with uniform handling # # TODO: maybe do some optimisation here when 2 operators can be combined into 1, etc.. # e.g not is => isnot if current_op: opc = current_op # polymorphic operator # get the current operator morph, based on current context if T_POLY_OP == opc.type: opc = opc.morph([pos,token_queue,op_queue]) # n-ary/multi-part operator, initial part # push to nop_queue/op_queue if T_N_OP == opc.type: validation = opc.validate(pos, op_queue, token_queue) if validation[0] is False: # operator is not valid in current state err['err'] = True err['msg'] = validation[1] return False n = opc.node(None, pos, op_queue, token_queue) n.arity = validation[0] nop_queue.insert( 0, n ) op_queue.insert( 0, n ) else: if len(nop_queue): nop = nop_queue[0] nop_index = nop.op_index # n-ary/multi-part operator, further parts # combine one-by-one, until n-ary operator is complete if nop and nop.op_next( opc, pos, op_queue, token_queue ): while len(op_queue) > nop_index: entry = op_queue.pop(0) op = entry.node arity = op.arity if (T_OP & op.type) and 0 == arity: arity = 1 # have already padded with empty token elif arity > len(token_queue) and op.arity_min <= op.arity: arity = op.arity_min n = op.node(array_splice(token_queue, -arity, arity), entry.pos) token_queue.append( n ) if nop.op_complete( ): nop_queue.pop(0) op_queue.pop(0) opc = nop.node nop.dispose( ) nop_index = nop_queue[0].op_index if len(nop_queue) else 0 else: return else: validation = opc.validate(pos, op_queue, token_queue) if validation[0] is False: # operator is not valid in current state err['err'] = True err['msg'] = validation[1] return False fixity = opc.fixity if POSTFIX == fixity: # postfix assumed to be already in correct order, # no re-structuring needed arity = opc.arity if arity > len(token_queue) and opc.arity_min <= len(token_queue): arity = opc.arity_min n = opc.node(array_splice(token_queue, -arity, arity), pos) token_queue.append( n ) elif PREFIX == fixity: # prefix assumed to be already in reverse correct order, # just push to op queue for later re-ordering op_queue.insert( 0, Node(opc.otype, opc.arity, opc, None, pos) ) if (T_OP & opc.type) and (0 == opc.arity): token_queue.append(Xpresion.EMPTY_TOKEN.node(None, pos+1)) else: # if INFIX == fixity: while len(op_queue) > nop_index: entry = op_queue.pop(0) op = entry.node if (op.priority < opc.priority) or (op.priority == opc.priority and (op.associativity < opc.associativity or (op.associativity == opc.associativity and op.associativity < 0))): arity = op.arity if (T_OP & op.type) and 0 == arity: arity = 1 # have already padded with empty token elif arity > len(token_queue) and op.arity_min <= op.arity: arity = op.arity_min n = op.node(array_splice(token_queue, -arity, arity), entry.pos) token_queue.append( n ) else: op_queue.insert( 0, entry ) break op_queue.insert( 0, Node(opc.otype, opc.arity, opc, None, pos) ) else: while len(op_queue): entry = op_queue.pop(0) op = entry.node arity = op.arity if (T_OP & op.type) and 0 == arity: arity = 1 # have already padded with empty token elif arity > len(token_queue) and op.arity_min <= op.arity: arity = op.arity_min n = op.node(array_splice(token_queue, -arity, arity), entry.pos) token_queue.append( n ) def parse_delimited_block(s, i, l, delim, is_escaped=True): p = delim esc = False ch = '' is_escaped = is_escaped is not False i += 1 while i < l: ch = s[i] i += 1 p += ch if delim == ch and not esc: break esc = ((not esc) and ('\\' == ch)) if is_escaped else False return p parse_re_flags = parse_re_flags def parse(xpr, conf): get_entry = Alias.get_entry reduce = Xpresion.reduce t_var_is_also_ident = 't_var' not in conf.RE err = 0 errmsg = '' errors = {'err': False, 'msg': ''} expr = str(xpr.source) l = len(expr) xpr._cnt = 0 xpr._symbol_table = { } xpr._cache = { } xpr.variables = { } AST = [ ] OPS = [ ] NOPS = [ ] t_index = 0 i = 0 while i < l: ch = expr[ i ] # use customized (escaped) delimited blocks here # TODO: add a "date" block as well with #..# block = get_entry(conf.BLOCKS, ch) if block: # string or regex or date ('"`#) v = block['parse'](expr, i, l, ch) if v is not False: i += len(v) if 'rest' in block: block_rest = block['rest'](expr, i, l) if not block_rest: block_rest = '' else: block_rest = '' i += len(block_rest) t = xpr.t_block( conf, v, block['type'], block_rest ) if t is not False: t_index+=1 AST.append( t.node(None, t_index) ) continue e = expr[ i: ] m = conf.RE['t_spc'].match(e) if m: # space i += len(m.group( 0 )) continue m = conf.RE['t_num'].match(e) if m: # number t = xpr.t_liter( conf, m.group( 1 ), T_NUM ) if t is not False: t_index+=1 AST.append( t.node(None, t_index) ) i += len(m.group( 0 )) continue m = conf.RE['t_ident'].match(e) if m: # ident, reserved, function, operator, etc.. t = xpr.t_liter( conf, m.group( 1 ), T_IDE ) # reserved keyword if t is not False: t_index+=1 AST.append( t.node(None, t_index) ) i += len(m.group( 0 )) continue t = xpr.t_op( conf, m.group( 1 ) ) # (literal) operator if t is not False: t_index+=1 reduce( AST, OPS, NOPS, t, t_index, errors ) if errors['err']: err = 1 errmsg = errors['msg'] break i += len(m.group( 0 )) continue if t_var_is_also_ident: t = xpr.t_var( conf, m.group( 1 ) ) # variables are also same identifiers if t is not False: t_index+=1 AST.append( t.node(None, t_index) ) i += len(m.group( 0 )) continue m = conf.RE['t_special'].match(e) if m: # special symbols.. v = m.group( 1 ) t = False while len(v) > 0: # try to match maximum length op/func t = xpr.t_op( conf, v ) # function, (non-literal) operator if t is not False: break v = v[0:-1] if t is not False: t_index+=1 reduce( AST, OPS, NOPS, t, t_index, errors ) if errors['err']: err = 1 errmsg = errors['msg'] break i += len(v) continue if not t_var_is_also_ident: m = conf.RE['t_var'].match(e) if m: # variables t = xpr.t_var( conf, m.group( 1 ) ) if t is not False: t_index+=1 AST.append( t.node(None, t_index) ) i += len(m.group( 0 )) continue m = conf.RE['t_nonspc'].match(e) if m: # other non-space tokens/symbols.. t = xpr.t_liter( conf, m.group( 1 ), T_LIT ) # reserved keyword if t is not False: t_index+=1 AST.append( t.node(None, t_index) ) i += len(m.group( 0 )) continue t = xpr.t_op( conf, m.group( 1 ) ) # function, other (non-literal) operator if t is not False: t_index+=1 reduce( AST, OPS, NOPS, t, t_index, errors ) if errors['err']: err = 1 errmsg = errors['msg'] break i += len(m.group( 0 )) continue #t = xpr.t_tok( conf, m.group( 1 ) ) #t_index+=1 #AST.append( t.node(None, t_index) ) # pass-through .. #i += len(m.group( 0 )) #continue err = 1 errmsg = 'Unknown token "'+m.group( 0 )+'"' # exit with error break if not err: reduce( AST, OPS, NOPS ) if (1 != len(AST)) or (len(OPS) > 0): err = 1 errmsg = 'Parse Error, Mismatched Parentheses or Operators' if not err: try: evaluator = xpr.compile( AST[0], conf ) except BaseException as e: err = 1 errmsg = 'Compilation Error, ' + str(e) + '' NOPS = None OPS = None AST = None xpr._symbol_table = None if err: evaluator = None xpr.variables = [ ] xpr._cnt = 0 xpr._cache = { } xpr.evaluatorString = '' xpr.evaluator = xpr.dummy_evaluator raise RuntimeError('Xpresion Error: ' + errmsg + ' at ' + expr) else: # make array xpr.variables = list( xpr.variables.keys() ) xpr.evaluatorString = evaluator[0] xpr.evaluator = evaluator[1] return xpr def render(tok, args=None): if None==args: args=[] return tok.render( args ) def GET(obj, keys=list()): if (not keys) or (not len(keys)): return obj o = obj c = len(keys) i = 0 while i < c: k = keys[i] i += 1 if o is None: break if isinstance(o,(list,tuple)): if int(k)<len(o): o = o[int(k)] else: break elif isinstance(o,dict): if k in o: o = o[k] else: break else: try: o = getattr(o, k) except AttributeError: break return o if i==c else None def defaultConfiguration(*args): if len(args): Xpresion.CONF = args[0] return Xpresion.CONF def __init__(self, expr=None, conf=None): self.source = None self.variables = None self.evaluatorString = None self.evaluator = None self._cnt = 0 self._cache = None self._symbol_table = None self.dummy_evaluator = None if (not conf) or not isinstance(conf,Configuration): conf = Xpresion.defaultConfiguration() self.source = str(expr) if expr else '' self.dummy_evaluator = dummy Xpresion.parse( self, conf ) def __del__(self): self.dispose() def dispose(self): self.dummy_evaluator = None self.source = None self.variables = None self.evaluatorString = None self.evaluator = None self._cnt = None self._symbol_table = None self._cache = None return self def compile(self, AST, conf=None): # depth-first traversal and rendering of Abstract Syntax Tree (AST) if not conf: conf = Xpresion.defaultConfiguration() evaluator_str = str(Node.DFT( AST, Xpresion.render, True )) return [evaluator_str, evaluator_factory(evaluator_str,conf.FN,self._cache)] def evaluate(self, data=dict()): return self.evaluator( data ) if callable(self.evaluator) else None def debug(self, data=None): out = [ 'Expression: ' + self.source, 'Variables : [' + ','.join(self.variables) + ']', 'Evaluator : ' + self.evaluatorString ] if None!=data: out.append('Data : ' + pprint.pformat(data, 4)) out.append('Result : ' + pprint.pformat(self.evaluate(data), 4)) return ("\n").join(out) def __str__(self): return '[Xpresion source]: ' + str(self.source) + '' def t_liter(self, conf, token, type): if T_NUM == type: return Tok(T_NUM, token, token) return Alias.get_entry(conf.RESERVED, token.lower( )) def t_block(self, conf, token, type, rest=''): if T_STR == type: return Tok(T_STR, token, token) elif T_REX == type: sid = 're_'+token+rest if sid in self._symbol_table: id = self._symbol_table[sid] else: self._cnt += 1 id = 're_' + str(self._cnt) flags = 0 if 'i' in rest: flags|= re.I if 'm' in rest: flags|= re.M self._cache[ id ] = re.compile(token[1:-1], flags) self._symbol_table[sid] = id return Tok(T_REX, token, 'Cache["'+id+'"]') return False def t_var(self, conf, token): parts = token.split('.', 1) main = parts[0] if main not in self.variables: self.variables[ main ] = main if 1 < len(parts): keys = '["' + '","'.join(parts[1].split('.')) + '"]' return Tok(T_VAR, token, 'Xpresion.GET(Var["' + main + '"],'+keys+')') else: return Tok(T_VAR, main, 'Var["' + main + '"]') #return Tok(T_VAR, token, 'Var["' + '"]["'.join(token.split('.')) + '"]') def t_op(self, conf, token): op = False op = Alias.get_entry(conf.FUNCTIONS, token) if op is False: op = Alias.get_entry(conf.OPERATORS, token) return op def t_tok(self, conf, token): return Tok(T_MIX, token, token) def init( ): if Xpresion._inited: return Xpresion._inited = True #def sqrt(v): # import math # return math.sqrt(v) def clamp(v, m, M): if m > M: return m if v > m else (M if v < M else v) else: return M if v > M else (m if v < m else v) def sum(*args): s = 0 values = args if len(values) and isinstance(values[0],(list,tuple)): values = values[0] for v in values: s += v return s def avg(*args): s = 0 values = args if len(values) and isinstance(values[0],(list,tuple)): values = values[0] l = len(values) for v in values: s += v return s/l if l > 0 else s def ary_merge(a1, a2): if not isinstance(a1,(list,tuple)): a1 = [a1] if not isinstance(a2,(list,tuple)): a2 = [a2] return a1 + a2 def ary_eq(a1, a2): #l = len(a1) #if l==len(a2): # for i in range(l): # if a1[i]!=a2[i]: return False #else: return False #return True return a1 == a2 # e.g https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence Xpresion.defaultConfiguration(Configuration({ # regular expressions for tokens # =============================== 're' : { 't_spc' : re.compile(r'^(\s+)') ,'t_nonspc' : re.compile(r'^(\S+)') ,'t_special' : re.compile(r'^([*.\-+\\\/\^\$\(\)\[\]|?<:>&~%!#@=_,;{}]+)') ,'t_num' : re.compile(r'^(\d+(\.\d+)?)') ,'t_ident' : re.compile(r'^([a-zA-Z_][a-zA-Z0-9_]*)\b') ,'t_var' : re.compile(r'^\$([a-zA-Z0-9_][a-zA-Z0-9_.]*)\b') } # block-type tokens (eg strings and regexes) # ========================================== ,'blocks' : { '\'': { 'type': T_STR, 'parse': Xpresion.parse_delimited_block } ,'"': Alias('\'') ,'`': { 'type': T_REX, 'parse': Xpresion.parse_delimited_block, 'rest': Xpresion.parse_re_flags } } # reserved keywords and literals # =============================== ,'reserved' : { 'null' : Tok(T_IDE, 'null', 'None') ,'false' : Tok(T_BOL, 'false', 'False') ,'true' : Tok(T_BOL, 'true', 'True') ,'infinity' : Tok(T_NUM, 'Infinity', 'Fn.INF') ,'nan' : Tok(T_NUM, 'NaN', 'Fn.NAN') # aliases ,'none' : Alias('null') ,'inf' : Alias('infinity') } # operators # ========== ,'operators' : { # bra-kets as n-ary operators # negative number of arguments, indicate optional arguments (experimental) '(' : { 'input' : ['(',-1,')'] ,'output' : '<$.0>' ,'otype' : T_DUM ,'fixity' : POSTFIX ,'associativity': RIGHT ,'priority' : 0 } ,')' : {'input':[-1,')']} ,'[' : { 'input' : ['[',-1,']'] ,'output' : '\\[<$.0>\\]' ,'otype' : T_ARY ,'fixity' : POSTFIX ,'associativity': RIGHT ,'priority' : 2 } ,']' : {'input':[-1,']']} ,',' : { 'input' : [1,',',1] ,'output' : '<$.0>,<$.1>' ,'otype' : T_DFT ,'fixity' : INFIX ,'associativity': LEFT ,'priority' : 103 # comma operator needs to have very low priority because it can break other expressions which are between commas } # n-ary (ternary) if-then-else operator ,'?' : { 'input' : [1,'?',1,':',1] ,'output' : '(<$.1> if <$.0> else <$.2>)' ,'otype' : T_MIX ,'fixity' : INFIX ,'associativity': RIGHT ,'priority' : 100 } ,':' : {'input':[1,':',1]} ,'!' : { 'input' : ['!',1] ,'output' : '(not <$.0>)' ,'otype' : T_BOL ,'fixity' : PREFIX ,'associativity': RIGHT ,'priority' : 10 } ,'~' : { 'input' : ['~',1] ,'output' : '~<$.0>' ,'otype' : T_NUM ,'fixity' : PREFIX ,'associativity': RIGHT ,'priority' : 10 } ,'^' : { 'input' : [1,'^',1] ,'output' : '(<$.0>**<$.1>)' ,'otype' : T_NUM ,'fixity' : INFIX ,'associativity': RIGHT ,'priority' : 11 } ,'*' : { 'input' : [1,'*',1] ,'output' : '(<$.0>*<$.1>)' ,'otype' : T_NUM ,'fixity' : INFIX ,'associativity': LEFT ,'priority' : 20 } ,'/' : { 'input' : [1,'/',1] ,'output' : '(<$.0>/<$.1>)' ,'otype' : T_NUM ,'fixity' : INFIX ,'associativity': LEFT ,'priority' : 20 } ,'%' : { 'input' : [1,'%',1] ,'output' : '(<$.0>%<$.1>)' ,'otype' : T_NUM ,'fixity' : INFIX ,'associativity': LEFT ,'priority' : 20 } # addition/concatenation/unary plus as polymorphic operators ,'+' : {'polymorphic':[ # array concatenation [ lambda curr,Xpresion: curr.TOK and (not curr.PREV_IS_OP) and (curr.DEDUCED_TYPE==Xpresion.T_ARY), { 'input' : [1,'+',1] ,'output' : 'Fn.ary_merge(<$.0>,<$.1>)' ,'otype' : T_ARY ,'fixity' : INFIX ,'associativity': LEFT ,'priority' : 25 } ] # string concatenation ,[ lambda curr,Xpresion: curr.TOK and (not curr.PREV_IS_OP) and (curr.DEDUCED_TYPE==Xpresion.T_STR), { 'input' : [1,'+',1] ,'output' : '(<$.0>+str(<$.1>))' ,'otype' : T_STR ,'fixity' : INFIX ,'associativity': LEFT ,'priority' : 25 } ] # numeric addition ,[ lambda curr,Xpresion: curr.TOK and not curr.PREV_IS_OP, { 'input' : [1,'+',1] ,'output' : '(<$.0>+<$.1>)' ,'otype' : T_NUM ,'fixity' : INFIX ,'associativity': LEFT ,'priority' : 25 } ] # unary plus ,[ lambda curr,Xpresion: (not curr.TOK) or curr.PREV_IS_OP, { 'input' : ['+',1] ,'output' : '<$.0>' ,'otype' : T_NUM ,'fixity' : PREFIX ,'associativity': RIGHT ,'priority' : 4 } ] ]} ,'-' : {'polymorphic':[ # numeric subtraction [ lambda curr,Xpresion: curr.TOK and not curr.PREV_IS_OP, { 'input' : [1,'-',1] ,'output' : '(<$.0>-<$.1>)' ,'otype' : T_NUM ,'fixity' : INFIX ,'associativity': LEFT ,'priority' : 25 } ] # unary negation ,[ lambda curr,Xpresion: (not curr.TOK) or curr.PREV_IS_OP, { 'input' : ['-',1] ,'output' : '(-<$.0>)' ,'otype' : T_NUM ,'fixity' : PREFIX ,'associativity': RIGHT ,'priority' : 4 } ] ]} ,'>>' : { 'input' : [1,'>>',1] ,'output' : '(<$.0>\\>\\><$.1>)' ,'otype' : T_NUM ,'fixity' : INFIX ,'associativity': LEFT ,'priority' : 30 } ,'<<' : { 'input' : [1,'<<',1] ,'output' : '(<$.0>\\<\\<<$.1>)' ,'otype' : T_NUM ,'fixity' : INFIX ,'associativity': LEFT ,'priority' : 30 } ,'>' : { 'input' : [1,'>',1] ,'output' : '(<$.0>\\><$.1>)' ,'otype' : T_BOL ,'fixity' : INFIX ,'associativity': LEFT ,'priority' : 35 } ,'<' : { 'input' : [1,'<',1] ,'output' : '(<$.0>\\<<$.1>)' ,'otype' : T_BOL ,'fixity' : INFIX ,'associativity': LEFT ,'priority' : 35 } ,'>=' : { 'input' : [1,'>=',1] ,'output' : '(<$.0>\\>=<$.1>)' ,'otype' : T_BOL ,'fixity' : INFIX ,'associativity': LEFT ,'priority' : 35 } ,'<=' : { 'input' : [1,'<=',1] ,'output' : '(<$.0>\\<=<$.1>)' ,'otype' : T_BOL ,'fixity' : INFIX ,'associativity': LEFT ,'priority' : 35 } ,'==' : {'polymorphic':[ # array equivalence [ lambda curr,Xpresion: curr.DEDUCED_TYPE==Xpresion.T_ARY, { 'input' : [1,'==',1] ,'output' : 'Fn.ary_eq(<$.0>,<$.1>)' ,'otype' : T_BOL ,'fixity' : INFIX ,'associativity': LEFT ,'priority' : 40 } ] # default equivalence ,[ lambda curr,Xpresion: True, { 'input' : [1,'==',1] ,'output' : '(<$.0>==<$.1>)' ,'otype' : T_BOL ,'fixity' : INFIX ,'associativity': LEFT ,'priority' : 40 } ] ]} ,'!=' : { 'input' : [1,'!=',1] ,'output' : '(<$.0>!=<$.1>)' ,'otype' : T_BOL ,'fixity' : INFIX ,'associativity': LEFT ,'priority' : 40 } ,'is' : { 'input' : [1,'is',1] ,'output' : '(<$.0> is <$.1>)' ,'otype' : T_BOL ,'fixity' : INFIX ,'associativity': LEFT ,'priority' : 40 } ,'matches': { 'input' : [1,'matches',1] ,'output' : 'Fn.match(<$.1>,<$.0>)' ,'otype' : T_BOL ,'fixity' : INFIX ,'associativity': NONE ,'priority' : 40 } ,'in' : { 'input' : [1,'in',1] ,'output' : 'Fn.contains(<$.1>,<$.0>)' ,'otype' : T_BOL ,'fixity' : INFIX ,'associativity': NONE ,'priority' : 40 } ,'&' : { 'input' : [1,'&',1] ,'output' : '(<$.0>&<$.1>)' ,'otype' : T_NUM ,'fixity' : INFIX ,'associativity': LEFT ,'priority' : 45 } ,'|' : { 'input' : [1,'|',1] ,'output' : '(<$.0>|<$.1>)' ,'otype' : T_NUM ,'fixity' : INFIX ,'associativity': LEFT ,'priority' : 46 } ,'&&' : { 'input' : [1,'&&',1] ,'output' : '(<$.0> and <$.1>)' ,'otype' : T_BOL ,'fixity' : INFIX ,'associativity': LEFT ,'priority' : 47 } ,'||' : { 'input' : [1,'||',1] ,'output' : '(<$.0> or <$.1>)' ,'otype' : T_BOL ,'fixity' : INFIX ,'associativity': LEFT ,'priority' : 48 } #------------------------------------------ # aliases #------------------------------------------- ,'or' : Alias( '||' ) ,'and' : Alias( '&&' ) ,'not' : Alias( '!' ) } # functional operators # ==================== ,'functions' : { 'min' : { 'input' : 'min' ,'output' : 'min(<$.0>)' ,'otype' : T_NUM } ,'max' : { 'input' : 'max' ,'output' : 'max(<$.0>)' ,'otype' : T_NUM } ,'pow' : { 'input' : 'pow' ,'output' : 'Fn.pow(<$.0>)' ,'otype' : T_NUM } ,'sqrt' : { 'input' : 'sqrt' ,'output' : 'math.sqrt(<$.0>)' ,'otype' : T_NUM } ,'len' : { 'input' : 'len' ,'output' : 'Fn.len(<$.0>)' ,'otype' : T_NUM } ,'int' : { 'input' : 'int' ,'output' : 'int(<$.0>)' ,'otype' : T_NUM } ,'float' : { 'input' : 'float' ,'output' : 'float(<$.0>)' ,'otype' : T_NUM } ,'str' : { 'input' : 'str' ,'output' : 'str(<$.0>)' ,'otype' : T_STR } ,'array' : { 'input' : 'array' ,'output' : 'Fn.ary(<$.0>)' ,'otype' : T_ARY } ,'clamp' : { 'input' : 'clamp' ,'output' : 'Fn.clamp(<$.0>)' ,'otype' : T_NUM } ,'sum' : { 'input' : 'sum' ,'output' : 'Fn.sum(<$.0>)' ,'otype' : T_NUM } ,'avg' : { 'input' : 'avg' ,'output' : 'Fn.avg(<$.0>)' ,'otype' : T_NUM } ,'time' : { 'input' : 'avg' ,'output' : 'Fn.time()' ,'otype' : T_NUM ,'arity' : 0 } ,'date' : { 'input' : 'date' ,'output' : 'Fn.date(<$.0>)' ,'otype' : T_STR } #--------------------------------------- # aliases #---------------------------------------- # ... } # runtime (implementation) functions # ================================== ,'runtime' : { 'pow' : lambda base, exponent: base ** exponent ,'clamp' : clamp ,'len' : lambda v: 0 if v is None else (len(v) if isinstance(v,(str,list,tuple,dict)) else 1) ,'sum' : sum ,'avg' : avg ,'ary' : lambda x: x if isinstance(x,list) else (list(x) if isinstance(x,tuple) else [x]) ,'ary_eq' : ary_eq ,'ary_merge': ary_merge ,'match' : lambda s, regex: bool(re.search(regex, s)) ,'contains' : lambda o, i: bool(i in o) ,'time' : php_time ,'date' : php_date } })) Xpresion.init( ) # if used with 'import *' __all__ = ['Xpresion']