summaryrefslogtreecommitdiff
path: root/grc/converter/cheetah_converter.py
diff options
context:
space:
mode:
Diffstat (limited to 'grc/converter/cheetah_converter.py')
-rw-r--r--grc/converter/cheetah_converter.py277
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