diff options
Diffstat (limited to 'grc/converter/cheetah_converter.py')
-rw-r--r-- | grc/converter/cheetah_converter.py | 277 |
1 files changed, 277 insertions, 0 deletions
diff --git a/grc/converter/cheetah_converter.py b/grc/converter/cheetah_converter.py new file mode 100644 index 0000000000..16fea32c99 --- /dev/null +++ b/grc/converter/cheetah_converter.py @@ -0,0 +1,277 @@ +# Copyright 2016 Free Software Foundation, Inc. +# This file is part of GNU Radio +# +# GNU Radio Companion is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# GNU Radio Companion is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +from __future__ import absolute_import, print_function + +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 + + elif delim_to_find and char in ')]}' and extra_close(): # end of substitution + 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')\(\)', '\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 |