gr-mcp/grc/converter/cheetah_converter.py

270 lines
8.1 KiB
Python

# Copyright 2016 Free Software Foundation, Inc.
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
import collections
import re
import string
delims = {'(': ')', '[': ']', '{': '}', '': ', #\\*:'}
identifier_start = '_' + string.ascii_letters + ''.join(delims.keys())
string_delims = '"\''
cheetah_substitution = re.compile(
r'^\$((?P<d1>\()|(?P<d2>\{)|(?P<d3>\[)|)'
r'(?P<arg>[_a-zA-Z][_a-zA-Z0-9]*(?:\.[_a-zA-Z][_a-zA-Z0-9]*)?)(?P<eval>\(\))?'
r'(?(d1)\)|(?(d2)\}|(?(d3)\]|)))$'
)
cheetah_inline_if = re.compile(
r'#if (?P<cond>.*) then (?P<then>.*?) ?else (?P<else>.*?) ?(#|$)')
class Python(object):
start = ''
end = ''
nested_start = ''
nested_end = ''
eval = ''
type = str # yaml_output.Eval
class FormatString(Python):
start = '{'
end = '}'
nested_start = '{'
nested_end = '}'
eval = ':eval'
type = str
class Mako(Python):
start = '${'
end = '}'
nested_start = ''
nested_end = ''
type = str
class Converter(object):
def __init__(self, names):
self.stats = collections.defaultdict(int)
self.names = set(names)
self.extended = set(self._iter_identifiers(names))
@staticmethod
def _iter_identifiers(names):
if not isinstance(names, dict):
names = {name: {} for name in names}
for key, sub_keys in names.items():
yield key
for sub_key in sub_keys:
yield '{}.{}'.format(key, sub_key)
def to_python(self, expr):
return self.convert(expr=expr, spec=Python)
def to_python_dec(self, expr):
converted = self.convert(expr=expr, spec=Python)
if converted and converted != expr:
converted = '${ ' + converted.strip() + ' }'
return converted
def to_format_string(self, expr):
return self.convert(expr=expr, spec=FormatString)
def to_mako(self, expr):
return self.convert(expr=expr, spec=Mako)
def convert(self, expr, spec=Python):
if not expr:
return ''
elif '$' not in expr:
return expr
try:
return self.convert_simple(expr, spec)
except ValueError:
pass
try:
if '#if' in expr and '\n' not in expr:
expr = self.convert_inline_conditional(expr, spec)
return self.convert_hard(expr, spec)
except ValueError:
return 'Cheetah! ' + expr
def convert_simple(self, expr, spec=Python):
match = cheetah_substitution.match(expr)
if not match:
raise ValueError('Not a simple substitution: ' + expr)
identifier = match.group('arg')
if identifier not in self.extended:
raise NameError('Unknown substitution {!r}'.format(identifier))
if match.group('eval'):
identifier += spec.eval
out = spec.start + identifier + spec.end
if '$' in out or '#' in out:
raise ValueError('Failed to convert: ' + expr)
self.stats['simple'] += 1
return spec.type(out)
def convert_hard(self, expr, spec=Python):
lines = '\n'.join(self.convert_hard_line(line, spec)
for line in expr.split('\n'))
if spec == Mako:
# no line-continuation before a mako control structure
lines = re.sub(r'\\\n(\s*%)', r'\n\1', lines)
return lines
def convert_hard_line(self, expr, spec=Python):
if spec == Mako:
if '#set' in expr:
ws, set_, statement = expr.partition('#set ')
return ws + '<% ' + self.to_python(statement) + ' %>'
if '#if' in expr:
ws, if_, condition = expr.partition('#if ')
return ws + '% if ' + self.to_python(condition) + ':'
if '#else if' in expr:
ws, elif_, condition = expr.partition('#else if ')
return ws + '% elif ' + self.to_python(condition) + ':'
if '#else' in expr:
return expr.replace('#else', '% else:')
if '#end if' in expr:
return expr.replace('#end if', '% endif')
if '#slurp' in expr:
expr = expr.split('#slurp', 1)[0] + '\\'
return self.convert_hard_replace(expr, spec)
def convert_hard_replace(self, expr, spec=Python):
counts = collections.Counter()
def all_delims_closed():
for opener_, closer_ in delims.items():
if counts[opener_] != counts[closer_]:
return False
return True
def extra_close():
for opener_, closer_ in delims.items():
if counts[opener_] < counts[closer_]:
return True
return False
out = []
delim_to_find = False
pos = 0
char = ''
in_string = None
while pos < len(expr):
prev, char = char, expr[pos]
counts.update(char)
if char in string_delims:
if not in_string:
in_string = char
elif char == in_string:
in_string = None
out.append(char)
pos += 1
continue
if in_string:
out.append(char)
pos += 1
continue
if char == '$':
pass # no output
elif prev == '$':
if char not in identifier_start: # not a substitution
out.append('$' + char) # now print the $ we skipped over
elif not delim_to_find: # start of a substitution
try:
delim_to_find = delims[char]
out.append(spec.start)
except KeyError:
if char in identifier_start:
delim_to_find = delims['']
out.append(spec.start)
out.append(char)
counts.clear()
counts.update(char)
else: # nested substitution: simply match known variable names
found = False
for known_identifier in self.names:
if expr[pos:].startswith(known_identifier):
found = True
break
if found:
out.append(spec.nested_start)
out.append(known_identifier)
out.append(spec.nested_end)
pos += len(known_identifier)
continue
elif delim_to_find and char in delim_to_find and all_delims_closed(): # end of substitution
out.append(spec.end)
if char in delims['']:
out.append(char)
delim_to_find = False
# end of substitution
elif delim_to_find and char in ')]}' and extra_close():
out.append(spec.end)
out.append(char)
delim_to_find = False
else:
out.append(char)
pos += 1
if delim_to_find == delims['']:
out.append(spec.end)
out = ''.join(out)
# fix: eval stuff
out = re.sub(r'(?P<arg>' + r'|'.join(self.extended) +
r')\(\)', r'\g<arg>', out)
self.stats['hard'] += 1
return spec.type(out)
def convert_inline_conditional(self, expr, spec=Python):
if spec == FormatString:
raise ValueError('No conditionals in format strings: ' + expr)
matcher = r'\g<then> if \g<cond> else \g<else>'
if spec == Python:
matcher = '(' + matcher + ')'
expr = cheetah_inline_if.sub(matcher, expr)
return spec.type(self.convert_hard(expr, spec))
class DummyConverter(object):
def __init__(self, names={}):
pass
def to_python(self, expr):
return expr
def to_format_string(self, expr):
return expr
def to_mako(self, expr):
return expr