forked from mirrors/gecko-dev
		
	 55bc80584c
			
		
	
	
		55bc80584c
		
	
	
	
	
		
			
			This was done with |mach vendor python| followed by a manual backout of changes to unrelated packages. Differential Revision: https://phabricator.services.mozilla.com/D5532 --HG-- extra : moz-landing-system : lando
		
			
				
	
	
		
			354 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			354 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| from __future__ import absolute_import, print_function, unicode_literals
 | |
| 
 | |
| import re
 | |
| import json as json
 | |
| from .shared import JSONTemplateError, TemplateError, DeleteMarker, string, to_str
 | |
| from . import shared
 | |
| from .interpreter import ExpressionEvaluator
 | |
| from .six import viewitems
 | |
| import functools
 | |
| 
 | |
| operators = {}
 | |
| IDENTIFIER_RE = re.compile(r'[a-zA-Z_][a-zA-Z0-9_]*$')
 | |
| 
 | |
| 
 | |
| def operator(name):
 | |
|     def wrap(fn):
 | |
|         operators[name] = fn
 | |
|         return fn
 | |
|     return wrap
 | |
| 
 | |
| 
 | |
| def evaluateExpression(expr, context):
 | |
|     evaluator = ExpressionEvaluator(context)
 | |
|     return evaluator.parse(expr)
 | |
| 
 | |
| 
 | |
| _interpolation_start_re = re.compile(r'\$?\${')
 | |
| 
 | |
| 
 | |
| def interpolate(string, context):
 | |
|     mo = _interpolation_start_re.search(string)
 | |
|     if not mo:
 | |
|         return string
 | |
| 
 | |
|     result = []
 | |
|     evaluator = ExpressionEvaluator(context)
 | |
| 
 | |
|     while True:
 | |
|         result.append(string[:mo.start()])
 | |
|         if mo.group() != '$${':
 | |
|             string = string[mo.end():]
 | |
|             parsed, offset = evaluator.parseUntilTerminator(string, '}')
 | |
|             if isinstance(parsed, (list, dict)):
 | |
|                 raise TemplateError(
 | |
|                     "interpolation of '{}' produced an array or object".format(string[:offset]))
 | |
|             if to_str(parsed) == "null":
 | |
|                 result.append("")
 | |
|             else:
 | |
|                 result.append(to_str(parsed))
 | |
|             string = string[offset + 1:]
 | |
|         else:  # found `$${`
 | |
|             result.append('${')
 | |
|             string = string[mo.end():]
 | |
| 
 | |
|         mo = _interpolation_start_re.search(string)
 | |
|         if not mo:
 | |
|             result.append(string)
 | |
|             break
 | |
| 
 | |
|     return ''.join(result)
 | |
| 
 | |
| 
 | |
| def checkUndefinedProperties(template, allowed):
 | |
|     unknownKeys = []
 | |
|     combined = "|".join(allowed) + "$"
 | |
|     unknownKeys = [key for key in sorted(template)
 | |
|                    if not re.match(combined, key)]
 | |
|     if unknownKeys:
 | |
|         raise TemplateError(allowed[0].replace('\\', '') +
 | |
|                             " has undefined properties: " + " ".join(unknownKeys))
 | |
| 
 | |
| 
 | |
| @operator('$eval')
 | |
| def eval(template, context):
 | |
|     checkUndefinedProperties(template, ['\$eval'])
 | |
|     if not isinstance(template['$eval'], string):
 | |
|         raise TemplateError("$eval must be given a string expression")
 | |
|     return evaluateExpression(template['$eval'], context)
 | |
| 
 | |
| 
 | |
| @operator('$flatten')
 | |
| def flatten(template, context):
 | |
|     checkUndefinedProperties(template, ['\$flatten'])
 | |
|     value = renderValue(template['$flatten'], context)
 | |
|     if not isinstance(value, list):
 | |
|         raise TemplateError('$flatten value must evaluate to an array')
 | |
| 
 | |
|     def gen():
 | |
|         for e in value:
 | |
|             if isinstance(e, list):
 | |
|                 for e2 in e:
 | |
|                     yield e2
 | |
|             else:
 | |
|                 yield e
 | |
|     return list(gen())
 | |
| 
 | |
| 
 | |
| @operator('$flattenDeep')
 | |
| def flattenDeep(template, context):
 | |
|     checkUndefinedProperties(template, ['\$flattenDeep'])
 | |
|     value = renderValue(template['$flattenDeep'], context)
 | |
|     if not isinstance(value, list):
 | |
|         raise TemplateError('$flattenDeep value must evaluate to an array')
 | |
| 
 | |
|     def gen(value):
 | |
|         if isinstance(value, list):
 | |
|             for e in value:
 | |
|                 for sub in gen(e):
 | |
|                     yield sub
 | |
|         else:
 | |
|             yield value
 | |
| 
 | |
|     return list(gen(value))
 | |
| 
 | |
| 
 | |
| @operator('$fromNow')
 | |
| def fromNow(template, context):
 | |
|     checkUndefinedProperties(template, ['\$fromNow', 'from'])
 | |
|     offset = renderValue(template['$fromNow'], context)
 | |
|     reference = renderValue(
 | |
|         template['from'], context) if 'from' in template else context.get('now')
 | |
| 
 | |
|     if not isinstance(offset, string):
 | |
|         raise TemplateError("$fromNow expects a string")
 | |
|     return shared.fromNow(offset, reference)
 | |
| 
 | |
| 
 | |
| @operator('$if')
 | |
| def ifConstruct(template, context):
 | |
|     checkUndefinedProperties(template, ['\$if', 'then', 'else'])
 | |
|     condition = evaluateExpression(template['$if'], context)
 | |
|     try:
 | |
|         if condition:
 | |
|             rv = template['then']
 | |
|         else:
 | |
|             rv = template['else']
 | |
|     except KeyError:
 | |
|         return DeleteMarker
 | |
|     return renderValue(rv, context)
 | |
| 
 | |
| 
 | |
| @operator('$json')
 | |
| def jsonConstruct(template, context):
 | |
|     checkUndefinedProperties(template, ['\$json'])
 | |
|     value = renderValue(template['$json'], context)
 | |
|     return json.dumps(value, separators=(',', ':'), sort_keys=True, ensure_ascii=False)
 | |
| 
 | |
| 
 | |
| @operator('$let')
 | |
| def let(template, context):
 | |
|     checkUndefinedProperties(template, ['\$let', 'in'])
 | |
|     if not isinstance(template['$let'], dict):
 | |
|         raise TemplateError("$let value must be an object")
 | |
| 
 | |
|     subcontext = context.copy()
 | |
|     for k, v in template['$let'].items():
 | |
|         if not IDENTIFIER_RE.match(k):
 | |
|             raise TemplateError('top level keys of $let must follow /[a-zA-Z_][a-zA-Z0-9_]*/')
 | |
|         subcontext[k] = renderValue(v, context)
 | |
| 
 | |
|     try:
 | |
|         in_expression = template['in']
 | |
|     except KeyError:
 | |
|         raise TemplateError("$let operator requires an `in` clause")
 | |
|     return renderValue(in_expression, subcontext)
 | |
| 
 | |
| 
 | |
| @operator('$map')
 | |
| def map(template, context):
 | |
|     EACH_RE = 'each\([a-zA-Z_][a-zA-Z0-9_]*\)'
 | |
|     checkUndefinedProperties(template, ['\$map', EACH_RE])
 | |
|     value = renderValue(template['$map'], context)
 | |
|     if not isinstance(value, list) and not isinstance(value, dict):
 | |
|         raise TemplateError("$map value must evaluate to an array or object")
 | |
| 
 | |
|     is_obj = isinstance(value, dict)
 | |
| 
 | |
|     each_keys = [k for k in template if k.startswith('each(')]
 | |
|     if len(each_keys) != 1:
 | |
|         raise TemplateError(
 | |
|             "$map requires exactly one other property, each(..)")
 | |
|     each_key = each_keys[0]
 | |
|     each_var = each_key[5:-1]
 | |
|     each_template = template[each_key]
 | |
| 
 | |
|     def gen(val):
 | |
|         subcontext = context.copy()
 | |
|         for elt in val:
 | |
|             subcontext[each_var] = elt
 | |
|             elt = renderValue(each_template, subcontext)
 | |
|             if elt is not DeleteMarker:
 | |
|                 yield elt
 | |
|     if is_obj:
 | |
|         value = [{'key': v[0], 'val': v[1]} for v in value.items()]
 | |
|         v = dict()
 | |
|         for e in gen(value):
 | |
|             if not isinstance(e, dict):
 | |
|                 raise TemplateError(
 | |
|                     "$map on objects expects {0} to evaluate to an object".format(each_key))
 | |
|             v.update(e)
 | |
|         return v
 | |
|     else:
 | |
|         return list(gen(value))
 | |
| 
 | |
| 
 | |
| @operator('$match')
 | |
| def matchConstruct(template, context):
 | |
|     checkUndefinedProperties(template, ['\$match'])
 | |
| 
 | |
|     if not isinstance(template['$match'], dict):
 | |
|         raise TemplateError("$match can evaluate objects only")
 | |
| 
 | |
|     result = []
 | |
|     for condition in template['$match']:
 | |
|         if evaluateExpression(condition, context):
 | |
|             result.append(renderValue(template['$match'][condition], context))
 | |
| 
 | |
|     return result
 | |
| 
 | |
| 
 | |
| @operator('$merge')
 | |
| def merge(template, context):
 | |
|     checkUndefinedProperties(template, ['\$merge'])
 | |
|     value = renderValue(template['$merge'], context)
 | |
|     if not isinstance(value, list) or not all(isinstance(e, dict) for e in value):
 | |
|         raise TemplateError(
 | |
|             "$merge value must evaluate to an array of objects")
 | |
|     v = dict()
 | |
|     for e in value:
 | |
|         v.update(e)
 | |
|     return v
 | |
| 
 | |
| 
 | |
| @operator('$mergeDeep')
 | |
| def merge(template, context):
 | |
|     checkUndefinedProperties(template, ['\$mergeDeep'])
 | |
|     value = renderValue(template['$mergeDeep'], context)
 | |
|     if not isinstance(value, list) or not all(isinstance(e, dict) for e in value):
 | |
|         raise TemplateError(
 | |
|             "$mergeDeep value must evaluate to an array of objects")
 | |
| 
 | |
|     def merge(l, r):
 | |
|         if isinstance(l, list) and isinstance(r, list):
 | |
|             return l + r
 | |
|         if isinstance(l, dict) and isinstance(r, dict):
 | |
|             res = l.copy()
 | |
|             for k, v in viewitems(r):
 | |
|                 if k in l:
 | |
|                     res[k] = merge(l[k], v)
 | |
|                 else:
 | |
|                     res[k] = v
 | |
|             return res
 | |
|         return r
 | |
|     if len(value) == 0:
 | |
|         return {}
 | |
|     return functools.reduce(merge, value[1:], value[0])
 | |
| 
 | |
| 
 | |
| @operator('$reverse')
 | |
| def reverse(template, context):
 | |
|     checkUndefinedProperties(template, ['\$reverse'])
 | |
|     value = renderValue(template['$reverse'], context)
 | |
|     if not isinstance(value, list):
 | |
|         raise TemplateError("$reverse value must evaluate to an array of objects")
 | |
|     return list(reversed(value))
 | |
| 
 | |
| 
 | |
| @operator('$sort')
 | |
| def sort(template, context):
 | |
|     BY_RE = 'by\([a-zA-Z_][a-zA-Z0-9_]*\)'
 | |
|     checkUndefinedProperties(template, ['\$sort', BY_RE])
 | |
|     value = renderValue(template['$sort'], context)
 | |
|     if not isinstance(value, list):
 | |
|         raise TemplateError('$sorted values to be sorted must have the same type')
 | |
| 
 | |
|     # handle by(..) if given, applying the schwartzian transform
 | |
|     by_keys = [k for k in template if k.startswith('by(')]
 | |
|     if len(by_keys) == 1:
 | |
|         by_key = by_keys[0]
 | |
|         by_var = by_key[3:-1]
 | |
|         by_expr = template[by_key]
 | |
| 
 | |
|         def xform():
 | |
|             subcontext = context.copy()
 | |
|             for e in value:
 | |
|                 subcontext[by_var] = e
 | |
|                 yield evaluateExpression(by_expr, subcontext), e
 | |
|         to_sort = list(xform())
 | |
|     elif len(by_keys) == 0:
 | |
|         to_sort = [(e, e) for e in value]
 | |
|     else:
 | |
|         raise TemplateError('only one by(..) is allowed')
 | |
| 
 | |
|     # check types
 | |
|     try:
 | |
|         eltype = type(to_sort[0][0])
 | |
|     except IndexError:
 | |
|         return []
 | |
|     if eltype in (list, dict, bool, type(None)):
 | |
|         raise TemplateError('$sorted values to be sorted must have the same type')
 | |
|     if not all(isinstance(e[0], eltype) for e in to_sort):
 | |
|         raise TemplateError('$sorted values to be sorted must have the same type')
 | |
| 
 | |
|     # unzip the schwartzian transform
 | |
|     return list(e[1] for e in sorted(to_sort))
 | |
| 
 | |
| 
 | |
| def renderValue(template, context):
 | |
|     if isinstance(template, string):
 | |
|         return interpolate(template, context)
 | |
| 
 | |
|     elif isinstance(template, dict):
 | |
|         matches = [k for k in template if k in operators]
 | |
|         if matches:
 | |
|             if len(matches) > 1:
 | |
|                 raise TemplateError("only one operator allowed")
 | |
|             return operators[matches[0]](template, context)
 | |
| 
 | |
|         def updated():
 | |
|             for k, v in viewitems(template):
 | |
|                 if k.startswith('$$'):
 | |
|                     k = k[1:]
 | |
|                 elif k.startswith('$') and IDENTIFIER_RE.match(k[1:]):
 | |
|                     raise TemplateError(
 | |
|                         '$<identifier> is reserved; ues $$<identifier>')
 | |
|                 else:
 | |
|                     k = interpolate(k, context)
 | |
| 
 | |
|                 try:
 | |
|                     v = renderValue(v, context)
 | |
|                 except JSONTemplateError as e:
 | |
|                     if IDENTIFIER_RE.match(k):
 | |
|                         e.add_location('.{}'.format(k))
 | |
|                     else:
 | |
|                         e.add_location('[{}]'.format(json.dumps(k)))
 | |
|                     raise
 | |
|                 if v is not DeleteMarker:
 | |
|                     yield k, v
 | |
|         return dict(updated())
 | |
| 
 | |
|     elif isinstance(template, list):
 | |
|         def updated():
 | |
|             for i, e in enumerate(template):
 | |
|                 try:
 | |
|                     v = renderValue(e, context)
 | |
|                     if v is not DeleteMarker:
 | |
|                         yield v
 | |
|                 except JSONTemplateError as e:
 | |
|                     e.add_location('[{}]'.format(i))
 | |
|                     raise
 | |
|         return list(updated())
 | |
| 
 | |
|     else:
 | |
|         return template
 |